Skip to content

feat(parser,desugar): extern "abi" { … } blocks (v2 grammar phase 2A of #43) - #47

Closed
hyperpolymath wants to merge 1 commit into
v2-grammar-slash-pathsfrom
v2-grammar-extern-blocks
Closed

feat(parser,desugar): extern "abi" { … } blocks (v2 grammar phase 2A of #43)#47
hyperpolymath wants to merge 1 commit into
v2-grammar-slash-pathsfrom
v2-grammar-extern-blocks

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

Second slice of #43 (v2 grammar). After #46 lands the slash-path fix, the next concrete parse error on hyperpolymath/hypatia's bridge.eph is the extern "gossamer" { … } block. This PR makes that parse.

Stacked on #46. Base will switch to main automatically when #46 merges.

Changes

  • Grammar (ephapax.pest): new extern_block / extern_item / extern_type_item / extern_fn_item rules. declaration admits extern_block. "extern" is now a keyword (added to keyword and keyword_boundary rules so identifiers can't shadow it).
  • Core AST (ephapax-syntax): new Decl::Extern { abi, items } + ExternItem { Type | Fn }. Item params/ret use core Ty.
  • Surface AST (ephapax-surface): mirror SurfaceDecl::Extern { abi, items, span } and SurfaceExternItem. Span lives on the block (matching DataDecl).
  • Core parser (lib.rs): parse_extern_block + parse_extern_item routed from parse_declaration. ABI string is unquoted and unescaped via a minimal helper handling \" and \\.
  • Surface parser (surface.rs): same plumbing against SurfaceExternItem, types staying surface until desugar.
  • Desugar (ephapax-desugar): SurfaceDecl::ExternDecl::Extern by lowering item types through desugar_ty.
  • Downstream stubs so the workspace builds: every exhaustive match on Decl now handles Decl::Extern. Each one is a clearly marked TODO for phase 2B with a brief rationale.

Phase 2A scope

This is parse + AST + desugar only. Items survive into the core Module but downstream consumers currently treat them as no-ops:

ConsumerPhase 2A behaviourPhase 2B target
typecheckerskipregister externs as ambient bindings (opaque types, declared fn sigs)
affine/linear disciplineskipno body → no rule applies; correct as-is
wasm codegenskipemit (import "<abi>" "<name>" (func ...))
LSP outlinefilter_map drops themexpose extern items as navigable symbols
IR s-expr printerrenders (extern "abi" ((extern-type T) (extern-fn name (params) ret) ...))round-trip safe

Tests

  • parse_empty_extern_blockextern "gossamer" { } parses to a SurfaceDecl::Extern with the right ABI and zero items.
  • parse_extern_block_with_type_and_fn_itemstype Window + fn window_open(title: String, body: String): Window, asserts both item kinds and param arity.
  • parse_bridge_eph_shaped_prelude — full canonical prelude: module hypatia/ui/bridge\n\nextern "gossamer" { ... }\n\nfn entry(): I32 = 0.
  • test_parse_extern_block_core — same shape through the core parser into Decl::Extern / ExternItem.
  • desugar_extern_blockSurfaceDecl::ExternDecl::Extern, with SurfaceTy::String(region) and SurfaceTy::Base(I32) covered.

cargo test --workspace clean; cargo check --workspace clean.

Effect on hypatia's build-gossamer-gui workflow

The workflow greps for Parse error|expected EOI to emit the friendly ::warning:: about v2 grammar. With this PR + #46, bridge.eph parses cleanly and reaches the type-checker. Phase 2A leaves the type-checker as a no-op for externs, so the file gets a benign "OK, nothing to check" pass. The next phase (2B) wires actual typecheck + wasm-import codegen so compile bridge.eph -o bridge.wasm finally produces a wasm artifact and the workflow emits the file instead of the warning.

Test plan

  • cargo test -p ephapax-parser (51/51)
  • cargo test -p ephapax-desugar (17/17 + new desugar_extern_block)
  • cargo test --workspace (everything green)
  • cargo check --workspace (clean)
  • CI green
  • After feat(parser): admit slash-separated module paths (v2 grammar, step 1 of #43) #46 merges: rebase auto-applies; re-run CI
  • Manual smoke: build a binary from this branch, run ephapax compile bridge.eph — confirm it now parses + typechecks (will not yet emit wasm; phase 2B).

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>
@hyperpolymath

Copy link
Copy Markdown
OwnerAuthor

Reopening — auto-closed by GitHub when its base branch v2-grammar-slash-paths was deleted on #46 merge. Re-pointing base to main so this becomes the new bottom of the canonical stack #43.

hyperpolymath added a commit that referenced this pull request May 15, 2026
Clean replacement for #47/#53 (which were blocked by sha-different
slash-paths commit on main). Cherry-picked f9f8657 onto a fresh branch
off current main — single-commit PR, no force-push needed.
Close#53 alongside merging this.
Closes part of #43 (v2 grammar phase 2A).
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@hyperpolymath
hyperpolymath deleted the v2-grammar-extern-blocks branch May 20, 2026 20:47
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