V2 grammar extern typecheck#77
Closed
hyperpolymath wants to merge 3 commits into
Closed
Conversation
First slice of the v2-grammar work in #43. The pest grammar's `qualified_name` rule was dot-separated only — `Foo.Bar.Baz`. The downstream `hypatia/ui/bridge` style used by `hyperpolymath/hypatia`'s `bridge.eph` would not parse, which is the first concrete error any consumer of the v2 grammar hits. Two changes: 1. **`ephapax.pest`** — `qualified_name` now admits `/` alongside `.` and `_`. Both separators are accepted at the grammar level; the path is stored verbatim in `Module.name`. No canonicalisation yet — that comes when the import resolver lands. 2. **`parse_surface_module`** — previously walked only `Rule::declaration`, silently dropping the `module Foo` header. Now extracts the module name from `Rule::module_decl` when present, falling back to the filename otherwise. Matches the pre-existing behaviour of `parse_module` (core parser). Tests added in both `surface::tests` and `tests::`: * `parse_module_with_slash_path` — `module hypatia/ui/bridge` parses; name preserved. * `parse_module_with_dot_path` — `module Foo.Bar.Baz` continues to parse; covers regression of the historical form. * `parse_module_without_decl_uses_filename` (surface only) — the filename-fallback path the parser already supported. Out of scope for this commit: * `extern "abi" { ... }` blocks — separate grammar + AST work, the second-largest gap behind `bridge.eph`. Tracked in #43. * `Rule::match_expr` dispatch in `parse_single_expr_core` (core parser only; surface parser already routes it correctly via `parse_match_expr`). Tracked in #43. * `Decl::Data` variant — `data` decls currently piggy-back on `parse_type_decl` in the core parser; the surface parser models them properly. Cleanup tracked in #43. After this lands, `bridge.eph`'s `module hypatia/ui/bridge` line parses; the next blocker the parser hits on that file is the `extern "gossamer" { ... }` block, which is the natural next PR. Hypatia's `build-gossamer-gui.yml` probe order is unchanged — the `::warning::` is still emitted while `extern` remains unsupported, since the workflow greps for `Parse error|expected EOI` which the `extern` rule still produces. Closes part of #43 (item 1 in the re-scoped ordering). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second slice of #43. After the slash-paths PR (#46), the next concrete parse error on `hyperpolymath/hypatia`'s `bridge.eph` is the `extern "gossamer" { type Window; fn window_open(…): Window; … }` block. This commit makes that parse. What lands: * **Pest grammar** (`ephapax-parser/src/ephapax.pest`): new `extern_block` / `extern_item` / `extern_type_item` / `extern_fn_item` rules. Top-level `declaration` rule extended to admit `extern_block`. `"extern"` added to `keyword` and `keyword_boundary` so identifiers can't shadow it. * **Core AST** (`ephapax-syntax`): new `Decl::Extern { abi, items }` variant plus `ExternItem { Type { name } | Fn { name, params, ret_ty } }` enum. Item params/ret carry core `Ty`. * **Surface AST** (`ephapax-surface`): mirror `SurfaceDecl::Extern { abi, items, span }` and `SurfaceExternItem` with the same shape but `SurfaceTy`. The span lives on the block, not on individual items, matching how `DataDecl` carries one block-level span. * **Core parser** (`lib.rs`): `parse_declaration` routes `Rule::extern_block` to a new `parse_extern_block` → `parse_extern_item` pair. The ABI string is unquoted and unescaped via a minimal helper that handles `\"` and `\\` (sufficient for ABI tags; extend if richer escapes appear). * **Surface parser** (`surface.rs`): same plumbing against `SurfaceExternItem`, types staying in `SurfaceTy` until the desugar pass. * **Desugar** (`ephapax-desugar`): `SurfaceDecl::Extern` lowers to `Decl::Extern` by walking each item and mapping its surface types through `desugar_ty`. Extern types pass through as-is (just the name). The block carries no body so there's no expression-level desugaring. * **Downstream stubs** so the workspace builds: every exhaustive match on `Decl` now handles `Decl::Extern` — typecheck (no-op for now, registered as TODO), affine/linear discipline (no body → nothing to check), wasm codegen (skipped — wasm imports are phase 2B), IR s-expr printer (renders the block with `extern-type` / `extern-fn` tagged sub-forms), LSP symbol extractor (`filter_map` to drop extern blocks from the outline — phase 2B will expose them as navigable symbols). Tests: * `parse_empty_extern_block` — `extern "gossamer" { }` round-trips with empty items. * `parse_extern_block_with_type_and_fn_items` — full hypatia shape: `type Window` + `fn window_open(title, body): Window`. * `parse_bridge_eph_shaped_prelude` — slash-pathed module + extern block + regular fn all in one file (the canonical bridge.eph prelude). * `test_parse_extern_block_core` — same shape through the core parser, asserting `ExternItem::Type` / `ExternItem::Fn` and param arity. * `desugar_extern_block` — `SurfaceDecl::Extern` → `Decl::Extern`, with `SurfaceTy::String(region)` → `Ty::String(region)` and `SurfaceTy::Base(I32)` → `Ty::Base(I32)` covered. `cargo test --workspace` passes. `cargo check --workspace` clean. Out of scope (phase 2B follow-up, tracked in #43): * **Typechecker**: register extern types as opaque nominal types and extern fns as ambient bindings in the module env so other decls can call them. * **Wasm codegen**: emit `(import "<abi>" "<name>" (func ...))` for fn items, treat type items as opaque (i32) externs. This is what actually unblocks `bridge.eph → bridge.wasm`. * **LSP**: surface extern items as navigable symbols. After phase 2B, the `compile-eph` / `compile-affine` clap aliases land as trivial 3-line additions (the original #36 ask), and hypatia's `build-gossamer-gui.yml` workflow flips from `::warning::` to an actual wasm artifact. Stacked on #46. Rebase target switches to `main` when #46 merges. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Third slice of #43. Phase 2A landed parse + AST + desugar for `extern "abi" { ... }` blocks; phase 2B-i wires the typechecker so regular fn bodies can call extern fns and refer to extern types, producing correct typing rather than silent no-ops. What lands: * **Extern types are nominal opaques** (`ephapax-desugar`). The `DataRegistry` grows an `extern_types: HashSet<SmolStr>` that the first pass of `desugar_module` populates from each `SurfaceDecl::Extern`'s `Type` items. `desugar_named_type` now short-circuits names in that set, returning `Ty::Var(name)` instead of erroring with `UnknownType`. Two distinct extern type names produce two distinct rigid type variables that never unify, so `Window` values can't be confused with `IpcChannel` etc. * **Extern fn signatures enter the type environment** (`ephapax-typing`). `type_check_module_inner`'s first pass now also walks `Decl::Extern { items }`: each `ExternItem::Fn` gets its params + ret_ty folded into a `Ty::Fun` chain and registered in `tc.ctx` with `BindingForm::Let`. Regular fn bodies that reference an extern fn name (or apply it) resolve through the same `Var(...)` lookup path as any other module-level binding, with full unification/inference applied. * **Extern fns are public exports** (`ModuleRegistry::register`). Cross-module imports also see the extern signatures, so a dependent module that imports this one type-checks calls into the extern API. * **Type items remain in the type namespace only.** Extern types have no value-level binding — `ipc_open` is callable, but `Window` itself isn't a value. This matches how `data` declarations behave (the constructors are values, the type is not). Tests: * `desugar_extern_types_resolve_to_ty_var` — `Window` and `IpcChannel` declared as extern types both resolve to their respective `Ty::Var` rigids; the two are unequal. * `typecheck_extern_nullary_fn_callable` — `entry()` whose body is just a reference to `open_handle` (a nullary extern fn) type-checks and the body's type matches the declared return. * `typecheck_extern_unary_fn_applied` — `entry(): Window = open(7)` where `open: I32 -> Window` is an extern fn type-checks; the argument I32 unifies with the parameter and the result flows back as the return. * `typecheck_extern_fn_arg_mismatch_rejected` — calling `open: I32 -> Window` with `Bool(true)` raises a typing error rather than silently succeeding. * `typecheck_distinct_extern_types_do_not_unify` — declaring `entry: Window` but returning a value of type `IpcChannel` fails (the two rigid `Ty::Var` types never unify). `cargo test --workspace` clean. Out of scope (phase 2B-ii follow-up, still in #43): * **Wasm codegen emits `(import "<abi>" "<name>" (func ...))`** directives for extern fn items. This is the architectural refactor — `FIRST_USER_FN` (currently a hardcoded `12`) becomes dynamic based on the number of collected imports; the `emit_imports` function grows to walk a collected import registry rather than hardcoding the 2 host imports. The pre-existing FFI import machinery has the same gap (FFI imports are collected via `ensure_ffi_import` but never written into the wasm import section) — 2B-ii fixes both at once. * **Linear discipline for extern fns**. If an extern fn returns a linear type (e.g. `Window!`), the caller must consume the result exactly once. The discipline-checker arms added in phase 2A skip extern decls — that remains correct for declarations themselves, but call-site enforcement on linear extern return types is part of 2B-ii. After phase 2B-ii lands, `hyperpolymath/hypatia`'s `bridge.eph` finally compiles to wasm with proper Gossamer-host imports, the `build-gossamer-gui.yml` workflow flips from `::warning::` to a real artifact, and the `compile-eph` / `compile-affine` clap aliases (the original ask in the now-closed #36) ship as the trivial 3-line addition they always wanted to be. Stacked on #47 (extern blocks parse+AST+desugar). Base rebases to `main` automatically once #46 and #47 land. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner
Author
|
Superseded — content already in |
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.
No description provided.