Skip to content

feat(codegen): thread imported TopConst across module boundaries (closes #107)#109

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/cross-module-const
May 12, 2026
Merged

feat(codegen): thread imported TopConst across module boundaries (closes #107)#109
hyperpolymath merged 1 commit into
mainfrom
feat/cross-module-const

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Closes #107.

Threads imported TopConst items through both code paths that previously dropped them, so use M::{some_const} now compiles end-to-end instead of failing at codegen with UnboundVariable.

Implementation

  • Codegen.gen_imports — matches TopFn and TopConst together. Imported consts: compile the initialiser inline against the importer's context, append a fresh global, register (local_name, -(global_idx + 1)) in func_indices (same negative-sentinel encoding as locally-declared consts, so use-site lookup is uniform).
  • Module_loader.flatten_imports — local-decl name suppression now covers fns and consts; public consts flow through the same alias-rename and dedup pipeline as fns, so all non-WASM back-ends pick up the fix unchanged.

WASM module-linking for globals isn't standard yet, so cross-module const identity is by value — each importer materialises its own copy of the constant. Fine because AffineScript consts are immutable.

Tests

E2E Xmod Other Codegens gains two cases (#107 regression):

  • flatten_imports inlines imported public consts — non-WASM path, asserts the imported const appears in prog_decls after flattening.
  • WASM gen_imports threads imported consts — full compile path, asserts the caller emits a global initialised to the imported const's value. The prior failure mode would have errored at compile_fixture_to_wasm itself.

Two new fixtures: PortNames.affine (exports pub const input_marker), cross_const_caller.affine (imports + uses it).

Spec updates

Inline-record-escape note

TopConst is an inline-record constructor. Capturing the whole record as a variable triggers OCaml's "type of the inlined record could escape" error. The match instead destructures the needed fields directly: | TopConst { tc_name; tc_value; _ } when tc_name.name = orig_name -> Some (\Const tc_value)`. The alias-rename path reconstructs by inline-literal — that form is allowed.

Test plan

  • dune build lib bin test clean.
  • E2E Xmod Other Codegens — all 4 tests pass (2 pre-existing + 2 new).
  • Full dune runtest clean on CI.

 #107)

Closes #107.

Before this commit, `Codegen.gen_imports` and
`Module_loader.flatten_imports` both pattern-matched only `TopFn` in
the imported module's `prog_decls`, silently dropping public `TopConst`
items. A `use M::{some_const}` that the typechecker accepted would
then fail at WASM codegen with `Codegen.UnboundVariable some_const`.

## Changes

### `lib/codegen.ml` — `gen_imports`

- Match `TopFn` and `TopConst` in declaration order; first match wins
  (same-name fn+const in one module would be a resolver error).
- For an imported `TopFn`: unchanged (emits the WASM
  `(import "<mod>" "<fn>" ...)` and registers the positive function
  index).
- For an imported `TopConst`: compile the initialiser against the
  importer's context via `gen_expr`, append a fresh global, register
  `(local_name, -(global_idx + 1))` in `func_indices` — same
  negative-sentinel encoding as locally-declared consts, so use-site
  lookup is uniform.
- Glob expansion (`use M::*`) likewise includes public consts.

WASM module-linking for globals isn't standard yet, so the const value
is inlined per importer. Identity is by value, which matches
AffineScript's immutability guarantee.

### `lib/module_loader.ml` — `flatten_imports`

- Local-decl name suppression set now covers fns AND consts.
- Public consts get inlined into the importer's `prog_decls` alongside
  public fns, with the same alias-renaming machinery for `use M::{c as
  d}`.
- Non-WASM back-ends (Rust, JS, OCaml, Lua, …) see imported consts as
  if they were locally declared, so they inherit the fix without
  per-target changes.

### Tests — `test/test_e2e.ml`

Two regression tests in the `E2E Xmod Other Codegens` group:

- `flatten_imports inlines imported public consts (#107)` — parses
  `cross_const_caller.affine`, runs the loader, asserts the imported
  `input_marker` const and `marker_plus` fn both appear in the
  flattened `prog_decls`.
- `WASM gen_imports threads imported consts (#107)` — compiles
  `PortNames.affine` then `cross_const_caller.affine`; asserts the
  caller emits at least one global initialised to the const's value
  (256). The earlier failure mode would have errored out at
  `compile_fixture_to_wasm` before the assertion.

Two new fixtures:
- `test/e2e/fixtures/PortNames.affine` — exports `pub const
  input_marker` + a fn that uses it.
- `test/e2e/fixtures/cross_const_caller.affine` — imports and uses
  both.

### Docs

- `docs/specs/SPEC.adoc` §8.4 — drops the "do not yet flow" bullet for
  consts; the cross-module flow table now lists const-as-inlined-value
  as supported.
- `docs/specs/codegen-environment.adoc` §5 — split into 5.1 (TopFn),
  5.2 (TopConst inlining algorithm + identity-by-value note), 5.3
  (glob expansion). §10 records #107 as closed and points at the
  regression tests.

## Inline-record-escape note

`TopConst` is declared with an inline record. The OCaml compiler
rejects `| TopConst tc when ...` followed by using `tc` as a value
("the type of the inlined record could escape"), so the match
destructures the needed fields directly: `| TopConst { tc_name;
tc_value; _ } when tc_name.name = orig_name -> Some (`Const tc_value)`.
The glob path destructures `{ tc_vis; tc_name; _ }`. Reconstruction in
the alias-rename path uses inline-literal construction, which is
allowed.
@hyperpolymath hyperpolymath merged commit a0af020 into main May 12, 2026
0 of 19 checks passed
@hyperpolymath hyperpolymath deleted the feat/cross-module-const branch May 12, 2026 21:26
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.

Cross-module imports: TopConst not threaded by gen_imports / flatten_imports

1 participant