fix(codegen): #138 — link imported enum constructors into codegen#602
Merged
Conversation
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
🔍 Hypatia Security ScanFindings: 45 issues detected
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #138
Symptom
A module that imports prelude's
Option/Resultand applies theirconstructors type-checks but fails to compile — the WASM backend raises
UnboundVariable "...Some". This blocked the stdliboption/resultlayer (and downstream consumers) from producing a runnable artifact.
Root cause
Codegen learns constructor tags only from
TopTypedecls. Two importpaths dropped imported types before they reached that registration:
Codegen.gen_imports— the path the defaultcompileuses.It feeds the original (un-flattened)
progtogenerate_moduleandresolved imports natively, but wired up only
TopFn(→ wasm import) andTopConst(→ global); imported types were silently dropped, sovariant_tagshad noSome→UnboundVariable. This is the path thereported failure hits.
Module_loader.flatten_imports— used by theprog_decls-iteratingbackends (Deno / JS / Julia / C / Rust / …). It carried only
TopFn/TopConst.(The task brief pointed at
flatten_imports; in practice the reportedcompilefailure flows throughgen_imports— empirically, before this PRthe Deno/JS/Julia/C/Rust backends already compiled the consumer; only the
WASM backend was broken. Both sites are fixed for completeness.)
Fix
Codegen.gen_importsnow registers the constructor tags / structlayouts of imported public types, reusing
gen_decl's local-typeregistration so imported and local types share exactly one code path.
Module_loader.flatten_importsnow inlines imported publicTopTypedecls — a separate namespace from fn/const (own dedup table, so a
local
fn Foocan't suppress an importedtype 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 orany of its constructors (so
use prelude::{Some}works without namingOption);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):Emitted WASM is valid and runtime-correct (via node
WebAssembly):wrap(5)→ boxedSome(tag 0, payload 5);empty()→None(tag 1);Ok/Errtags 0/1. Module imports onlywasi_snapshot_preview1.fd_write— constructors are compiled inline, no spurious imports.
All-backend matrix on the consumer (before → after):
UnboundVariable "Some"Tests
cross-module constructor linking, Wasm (#138)in
test/test_stdlib_aot.ml— the counterpart to the test: multi-module integration test using several stdlib modules together #137 Deno-pathintegration. Feeds the imported-constructor decl shape to all three
variant_tagsconsumers (with-arg construction, zero-arg, constructorpatterns).
dune test: 458 tests, all green (incl. ci: add stdlib-wide AOT compile-smoke gate #136 AOT smoke over 34 stdlibfiles and test: multi-module integration test using several stdlib modules together #137 integration — no regression).
dune runtest conformance: green.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.affine→UnsupportedFeature "Only variable and wildcard patterns supported in tuple patterns". With thisPR they get past the cleanup: remove b895374 seeded Some/None/Ok/Err builtins band-aid #138
UnboundVariableand reach this separatetuple-pattern codegen gap.
matchof a zero-arg variant (None) against aconstructor-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