feat(wasm): emit extern fn imports + direct calls (clean — replaces #51)#56
Closed
hyperpolymath wants to merge 1 commit into
Closed
feat(wasm): emit extern fn imports + direct calls (clean — replaces #51)#56hyperpolymath wants to merge 1 commit into
hyperpolymath wants to merge 1 commit into
Conversation
…B-ii) Fourth slice of #43. Phase 2B-i wired the typechecker to accept extern items; phase 2B-ii makes the wasm codegen actually emit `(import "<abi>" "<name>" (func ...))` directives for each extern fn and route calls to them at the correct import index. This is the slice that finally produces a wasm artifact for `hyperpolymath/hypatia`'s `bridge.eph` — combined with #46/#47/#48, the file now parses, type-checks, AND lowers to a valid wasm module with Gossamer host imports. ### Architectural change: dynamic function indices The wasm function index space is `[imports..., function section...]`, so adding extern imports shifts every runtime helper and user-fn index forward by N. The pre-existing layout hard-coded `NUM_IMPORTS = 2` plus `FN_BUMP_ALLOC = 2`, `FN_STRING_NEW = 3`, ... `FIRST_USER_FN = 12` as `const u32`. This commit: * Replaces the absolute constants (`FN_BUMP_ALLOC`, `FIRST_USER_FN`, …) with **per-helper offset constants** (`OFFSET_BUMP_ALLOC`, `OFFSET_STRING_NEW`, …) plus accessor methods on `Codegen` (`fn_bump_alloc(&self) -> u32`, `first_user_fn(&self) -> u32`, …) that compute `import_count() + OFFSET_*` at call sites. * Adds `import_count()` = `NUM_BUILTIN_IMPORTS + extern_imports.len()`. * Touches 51 call sites mechanically — every `FN_*` reference is now `self.fn_*()`, every `FIRST_USER_FN` is `self.first_user_fn()`, and the closure lambda index calculation (`NUM_IMPORTS + NUM_RUNTIME_FNS + …`) is now `self.first_user_fn() + …`. ### New collection pass `collect_extern_imports(&mut self, ast)` runs BEFORE `collect_user_fns`. It walks `Decl::Extern { items }`: * For each `ExternItem::Fn`, registers the wasm type signature, appends to `extern_imports`, and records the import index in `extern_fn_indices: HashMap<String, u32>`. * For each `ExternItem::Type`, records the name in `extern_type_names` (currently only used for diagnostics — type items have no wasm-level representation; codegen treats them as opaque `i32` handles). ### `emit_imports` rewrite Previously emitted exactly the 2 hard-coded host imports. Now: 1. `(import "env" "print_i32" (func ...))` — index 0 2. `(import "env" "print_string" (func ...))` — index 1 3. ... iterate `self.extern_imports`, each becomes `(import "<abi>" "<name>" (func <type_idx>))` at the next index ### Direct-call routing for extern fns `compile_app` now uses a new `flatten_app_chain` helper to recognise curried `App(App(App(f, a), b), c)` chains and emit a single direct `Call(import_idx)` with all args on the stack when `f` is an extern fn. The same flattener also handles single-arg user fn calls (the original direct-call path); multi-arg user fns continue to take the indirect closure path for now (a separate follow-up can promote them, with care around closure semantics). ### Out of scope / known gaps * **FFI imports are still not emitted** — `ensure_ffi_import` collects symbols into `ffi_imports: HashMap<String, u32>` but `emit_imports` does not iterate that map. The pre-existing `next_import_idx = 100` sparse-allocation strategy is left in place. Fixing FFI requires a pre-pass over user fn bodies to discover all `__ffi(...)` calls before user fn indices are assigned. Filed as phase 2C work — not on hypatia's path (bridge.eph uses extern blocks, not raw `__ffi`). * **Linear discipline at extern call sites**. If an extern fn returns a linear type, the caller must consume the result exactly once. The discipline checker already runs over fn bodies; this commit doesn't add any new check, so linear extern returns work-by-coincidence today (the type-checker sees them via the env binding, and `let!` enforces the linear consumption). A targeted test should land in phase 2C. ### Tests * `extern_block_emits_wasm_imports` — full end-to-end: module with `extern "gossamer" { type Window; fn window_open(...): I32; fn window_close(...): () }` plus a user fn that calls `window_open(1, 2)`, compiled to wasm, validated with `wasmparser::Validator`, then the import section is parsed and asserted to contain `(env, print_i32)`, `(env, print_string)`, `(gossamer, window_open)`, `(gossamer, window_close)` in order. * `module_without_extern_keeps_two_builtin_imports_only` — regression cover: the dynamic-import refactor must not change baseline modules. Module with no extern emits exactly the 2 host imports. * `extern_fn_call_uses_correct_import_index` — asserts the index recorded in `extern_fn_indices` for the first extern fn is `NUM_BUILTIN_IMPORTS` (= 2, right after the 2 host imports). `cargo test -p ephapax-wasm` → 66/66 (+3). Full workspace test sweep clean. ### Effect on hypatia Combined with #46 + #47 + #48, `bridge.eph` now lowers to a valid wasm module: the `extern "gossamer"` block emits real `(import "gossamer" "<name>")` directives, and the user-fn calls into those (e.g. `ipc_open(...)`) emit direct `Call(<import_idx>)` instructions. The `build-gossamer-gui.yml` workflow's `Compile bridge.eph` step should now produce `src/ui/public/assets/wasm/hypatia_gui.wasm` instead of firing the `::warning::` for unsupported grammar. Once this stack lands, the `compile-eph` / `compile-affine` clap aliases (original #36) ship as the trivial 3-line addition they always were. Stacked on #48. Rebases to `main` automatically once #46, #47, #48 land. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 15, 2026
Owner
Author
|
Superseded by the clean cherry-pick PR opened against current main. The base chain (feat/extern-typecheck-fresh) went stale after the cascade. |
hyperpolymath
added a commit
that referenced
this pull request
May 15, 2026
…#56) (#58) Clean cherry-pick of the wasm commit onto a fresh branch off current main. Replaces #56 (which was based on a now-stale chain). This is the slice that finally produces a wasm artifact for hypatia's bridge.eph: extern fns now emit `(import "<abi>" "<name>" (func ...))` directives and call sites route to the right import indices. Closes part of #43 (v2 grammar phase 2B-ii). Co-authored-by: Claude Opus 4.7 <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.
Clean replacement for #51 (blocked by cascade-damaged diff). Cherry-picked f7f289b onto a fresh branch stacked on feat/extern-typecheck-fresh.
Close #51 alongside merging this.
Closes part of #43 (v2 grammar phase 2B-ii) — the wasm-output unblocker for hypatia's bridge.eph.