Skip to content

V2 grammar extern typecheck - #77

Closed
hyperpolymath wants to merge 3 commits into
mainfrom
v2-grammar-extern-typecheck
Closed

V2 grammar extern typecheck#77
hyperpolymath wants to merge 3 commits into
mainfrom
v2-grammar-extern-typecheck

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

No description provided.

hyperpolymathand others added 3 commits May 14, 2026 22:52
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>
@hyperpolymath

Copy link
Copy Markdown
OwnerAuthor

Superseded — content already in main. Rebasing v2-grammar-extern-typecheck onto current main yields zero delta: git skipped the typecheck/extern-abi commits as already-applied (patch-id match), and the slash-module-path work landed via #46. Feature verified present in main: parse_extern_block + extern "abi" { … } in ephapax-parser, ExternItem typechecking in ephapax-typing (also #75/#76 for LSP extern symbols). No remaining changes to merge; closing as delivered-by-other-PRs. Reopen only if a future diff against main reappears.

Sign up for freeto 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.

1 participant

@hyperpolymath