Skip to content

fix(codegen): #138 — link imported enum constructors into codegen#602

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/inspiring-newton-dg5wov
Jun 20, 2026
Merged

fix(codegen): #138 — link imported enum constructors into codegen#602
hyperpolymath merged 1 commit into
mainfrom
claude/inspiring-newton-dg5wov

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Closes #138

Note: #138 itself was joint-closed (2026-05-18) when the front-end
seeded-builtins band-aid was removed. That removal correctly routed
Some/None/Ok/Err through the module path so check passes — but it
exposed a codegen regression that this PR fixes. Verified at HEAD
58dc2a0.

Symptom

A module that imports prelude's Option/Result and applies their
constructors type-checks but fails to compile — the WASM backend raises
UnboundVariable "...Some". This blocked the stdlib option/result
layer (and downstream consumers) from producing a runnable artifact.

Root cause

Codegen learns constructor tags only from TopType decls. Two import
paths dropped imported types before they reached that registration:

  • Core-Wasm Codegen.gen_imports — the path the default compile uses.
    It feeds the original (un-flattened) prog to generate_module and
    resolved imports natively, but wired up only TopFn (→ wasm import) and
    TopConst (→ global); imported types were silently dropped, so
    variant_tags had no SomeUnboundVariable. This is the path the
    reported failure hits.
  • Module_loader.flatten_imports — used by the prog_decls-iterating
    backends (Deno / JS / Julia / C / Rust / …). It carried only TopFn/
    TopConst.

(The task brief pointed at flatten_imports; in practice the reported
compile failure flows through gen_imports — empirically, before this PR
the Deno/JS/Julia/C/Rust backends already compiled the consumer; only the
WASM backend was broken. Both sites are fixed for completeness.)

Fix

  1. Codegen.gen_imports now registers the constructor tags / struct
    layouts of imported public types, reusing gen_decl's local-type
    registration so imported and local types share exactly one code path.
  2. Module_loader.flatten_imports now inlines imported public TopType
    decls — a separate namespace from fn/const (own dedup table, so a
    local fn Foo can't suppress an imported type Foo), local-wins,
    deduped across paths, and ordered before consumers so the
    single-pass codegen sees a type before any function that uses it.

Selection: use M::{..} carries a type when the list names the type or
any of its constructors
(so use prelude::{Some} works without naming
Option); use M / use M::* carry all public types.

Scope: directly-imported constructors lower on every backend.
Transitive re-export (a module re-exposing names it itself imported)
stays unimplemented — noted in docs/history/MODULE-SYSTEM-PROGRESS.md.

Verify — before / after

Minimal consumer.affine (use prelude::{Option, Some, None} + Some(x)/None):

# BEFORE (HEAD 58dc2a0)
$ affinescript check   consumer.affine   # Type checking passed
$ affinescript compile consumer.affine   # Code generation error:
                                         #   (Codegen.UnboundVariable
                                         #    "Function or variable not found: Some")

# AFTER
$ affinescript check   consumer.affine            # Type checking passed
$ affinescript compile -o consumer.wasm c.affine  # Compiled -> consumer.wasm (WASM)

Emitted WASM is valid and runtime-correct (via node WebAssembly):
wrap(5) → boxed Some (tag 0, payload 5); empty()None (tag 1);
Ok/Err tags 0/1. Module imports only wasi_snapshot_preview1.fd_write
— constructors are compiled inline, no spurious imports.

All-backend matrix on the consumer (before → after):

backend before after
wasm (default) FAIL UnboundVariable "Some" OK
deno.js / jl / c / rs / js / cjs OK OK

Tests

Out of scope (do not conflate with #138)

Pre-existing core-Wasm gaps that reproduce with purely local enums and
are independent of cross-module linking:

  • stdlib/option.affine / result.affineUnsupportedFeature "Only variable and wildcard patterns supported in tuple patterns". With this
    PR they get past the cleanup: remove b895374 seeded Some/None/Ok/Err builtins band-aid #138 UnboundVariable and reach this separate
    tuple-pattern codegen gap.
  • Mixed-representation match of a zero-arg variant (None) against a
    constructor-with-args arm returns the wrong value at runtime — reproduces
    with a local Opt = Som(Int) | Non.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8


Generated by Claude Code

A module that imports prelude's Option/Result and applies their
constructors type-checked but failed to compile: codegen raised
UnboundVariable "...Some". Removing the b895374 seeded-builtins band-aid
(front-end half of #138) correctly routed Some/None/Ok/Err through the
module path, but the backends learn variant tags only from TopType decls,
and imported types never reached them.

- Codegen.gen_imports (core-Wasm — the path `compile` uses): previously
  wired up only TopFn (-> wasm import) and TopConst (-> global) and dropped
  imported types. Now also registers the constructor tags / struct layouts
  of imported public types, reusing gen_decl's local-type registration so
  imported and local types share one code path.
- Module_loader.flatten_imports (Deno / JS / Julia / C / Rust / ...): now
  inlines imported public TopType decls (separate namespace from fn/const,
  local-wins, deduped, ordered before consumers) so the prog_decls-iterating
  backends register them too.

Scope: directly-imported constructors lower on every backend. Transitive
re-export stays unimplemented. Unrelated pre-existing core-Wasm gaps
(tuple-pattern codegen; mixed-representation match of a zero-arg variant)
reproduce with purely local enums and are out of scope.

Adds a Wasm-path multi-module regression test (test_stdlib_aot.ml), the
counterpart to the #137 Deno-path integration.

Closes #138

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 45 issues detected

Severity Count
🔴 Critical 2
🟠 High 27
🟡 Medium 16

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action denoland/setup-deno@v2 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in scorecard-enforcer.yml",
    "type": "scorecard_publish_with_run_step",
    "file": "scorecard-enforcer.yml",
    "action": "split_scorecard_publish_job",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Issue in instant-sync.yml",
    "type": "secret_action_without_presence_gate",
    "file": "instant-sync.yml",
    "action": "peter-evans/repository-dispatch",
    "rule_module": "workflow_audit",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affinescript-cli/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (2 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/packages/affine-vscode/mod.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "Shell execution -- validate input before passing to shell (1 occurrences, CWE-78)",
    "type": "js_exec_sync",
    "file": "/home/runner/work/affinescript/affinescript/affinescript-vite/src/affine-plugin-improved.js",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "high"
  },
  {
    "reason": "expect() in hot path (32 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/wasm_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "expect() in hot path (29 occurrences, CWE-754)",
    "type": "expect_in_hot_path",
    "file": "/home/runner/work/affinescript/affinescript/affinescriptiser/src/codegen/affine_gen.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (2 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/panic.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  },
  {
    "reason": "unsafe block -- requires SAFETY comment (1 occurrences, CWE-676)",
    "type": "unsafe_block",
    "file": "/home/runner/work/affinescript/affinescript/runtime/src/alloc.rs",
    "action": "flag",
    "rule_module": "code_safety",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath hyperpolymath marked this pull request as ready for review June 20, 2026 05:15
@hyperpolymath hyperpolymath merged commit ac98c81 into main Jun 20, 2026
8 checks passed
@hyperpolymath hyperpolymath deleted the claude/inspiring-newton-dg5wov branch June 20, 2026 05:15
hyperpolymath added a commit that referenced this pull request Jun 20, 2026
…setup action) (#603)

## Make CI standalone (no external-repo reusable workflows on the gating
path)

Follow-up to the #602 investigation: the PR-gating `build`/`lint`/`test`
never ran because several workflows were in `startup_failure`. This
makes the **gating** CI self-contained — no dependency on the external
`hyperpolymath/standards` reusable workflows and no third-party
toolchain action — so it runs regardless of that repo's state or an org
"allowed actions" policy.

### Root cause found
The estate **`spark-theatre-gate.yml`** carries a documented note
(BP008): **a `concurrency:` block in a reusable-workflow *caller*, when
the reusable also declares concurrency on the same key, is rejected at
run-creation → `startup_failure`** (no check-run is ever emitted).
`governance.yml` and `secret-scanner.yml` both still had caller-level
`concurrency:` blocks calling estate reusables → that is why they
startup-failed, while `hypatia`/`spark` (no stacking) passed. The
standalone replacements here are **normal** workflows, so their
concurrency blocks are safe.

### Changes
| Workflow | Before | After |
|---|---|---|
| **ci.yml** | `ocaml/setup-ocaml` (third-party) + first-party pins with
**fictional** version comments (`checkout # v6.0.3`, `upload-artifact #
v7.0.1`) | Self-hosted OCaml via `apt + opam` (`ocaml-system`,
base-compiler fallback; dune-project needs ≥4.14); only first-party
`actions/*` at real major tags |
| **governance.yml** | calls
`hyperpolymath/standards/.../governance-reusable.yml@main` | local
`tools/ci/governance-standalone.sh` (Jekyll ban, MPL-1.0 SPDX-header
ban, **PR-delta** DOC-FORMAT) |
| **secret-scanner.yml** | calls
`hyperpolymath/standards/.../secret-scanner-reusable.yml` + `secrets:
inherit` | local `tools/ci/secret-scan-standalone.sh` (pure-shell,
high-confidence patterns, no secrets) |
| **scorecard.yml** | calls
`hyperpolymath/standards/.../scorecard-reusable.yml` | direct
`ossf/scorecard-action` (mirrors the already-direct
`scorecard-enforcer.yml`) |

New: `tools/ci/governance-standalone.sh`,
`tools/ci/secret-scan-standalone.sh` (both `chmod +x`).

### Verification (local)
```
tools/ci/governance-standalone.sh        → PASS   (Jekyll: none; MPL-1.0 headers: none)
tools/ci/governance-standalone.sh main   → PASS   (no newly-added docs/ .md)
tools/ci/secret-scan-standalone.sh       → PASS   (no high-confidence secrets in tracked files)
python3 -c yaml.safe_load on all 4 workflows → valid
bash -n on both scripts → ok
```
The gates were calibrated against the tree first: a naïve gate would
false-fail (59 pre-existing `docs/*.md`, 14 `MPL-1.0` *mentions* in
policy/docs), so the checks are header-aware (0 actual `SPDX: MPL-1.0`
headers) and DOC-FORMAT is **delta-only** (matches the canonical "PR
that adds a docs/ .md" semantics; pre-existing files are never
retro-flagged).

### Left intentionally (not "fully" standalone — by design)
- **hypatia-scan.yml**, **spark-theatre-gate.yml** — estate-proprietary
scanners (neurosymbolic / SPARK), **currently passing**. No public
standalone equivalent exists; stubbing them would *lose* real coverage.
Left calling the estate reusable.
- **mirror.yml** — cross-forge mirroring is inherently external; not a
CI gate.
- **release.yml** — its `ocaml/setup-ocaml` runs on a **cross-platform
matrix (Linux + macOS)**; a Linux-only `apt` inline setup would break
the macOS release builds, and the publish path isn't testable here. Not
PR-gating.
- Standard community actions elsewhere (`github/codeql-action`,
`denoland/setup-deno`, `dtolnay/rust-toolchain`,
`haskell-actions/setup`, `peter-evans/repository-dispatch`) — normal,
non-estate; inlining toolchain setups (esp. cross-platform) is a larger
follow-up. Happy to do these if you want.

### Notes / trade-offs for review
- First-party `actions/*` are pinned to **major tags** (`@v4`) rather
than SHAs, because the prior SHA pins were labelled with nonexistent
versions. `scorecard-enforcer.yml`'s `check-critical` job warns
(non-blocking) on `@vN` pins. Re-pin to verified upstream SHAs if you
prefer; first-party actions are always allowed under any allowlist.
- The self-hosted OCaml step bumps the affected jobs' `timeout-minutes`
10 → 25 to cover the toolchain install.
- I can't exercise GitHub runner behaviour from here; CI on this PR is
the real test. If `ci.yml` still startup-fails after this, the remaining
cause is isolated to first-party actions and points at an org
Actions-policy/platform issue (owner-side).
- Alternative considered: the **minimal** fix for
governance/secret-scanner is simply removing the stacked `concurrency:`
block (keeps the canonical estate checks). This PR goes standalone per
request, but that option remains if you'd rather keep estate coverage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Lz7pRcec2Z3tVtaAhvB3M8)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cleanup: remove b895374 seeded Some/None/Ok/Err builtins band-aid

2 participants