Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/ephapax-cli/tests/v2_grammar_phase_f.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
//
// Phase F regressions for hyperpolymath/ephapax#43. Implicit-`in` between
// sequential `let` bindings — the deepest grammar gap bridge.eph relied
// on.
//
// The grammar adds a new `block_expr` form, tried *before* the legacy
// `let_expr` in the `single_expr` choice. A `block_expr` is
// `sequential_let+ ~ expression`, where each `sequential_let` has the
// shape `("let" | "let!") ~ let_binder ~ (":" ~ ty)? ~ "=" ~ block_rhs`
// without a trailing `in` keyword. The parser folds them at parse time:
//
// let a = 10
// let b = 20
// a + b
//
// becomes
//
// Let { name: a, value: 10, body:
// Let { name: b, value: 20, body:
// a + b } }

use ephapax_desugar::desugar;
use ephapax_parser::parse_surface_module;
use ephapax_typing::type_check_module;

const IMPLICIT_IN: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/v2-grammar/fixtures/implicit-in.eph"
));

const IMPLICIT_IN_TUPLE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/v2-grammar/fixtures/implicit-in-tuple.eph"
));

fn compile_to_wasm(source: &str, name: &str) -> Vec<u8> {
let surface = parse_surface_module(source, name).expect("must parse");
let core = desugar(&surface).expect("must desugar");
type_check_module(&core).expect("must type-check");
ephapax_wasm::compile_module(&core).expect("must codegen")
}

#[test]
fn implicit_in_chain_compiles() {
let wasm = compile_to_wasm(IMPLICIT_IN, "implicit-in");
wasmparser::validate(&wasm).expect("wasm validates");
}

#[test]
fn implicit_in_with_tuple_binders_compiles() {
let wasm = compile_to_wasm(IMPLICIT_IN_TUPLE, "implicit-in-tuple");
wasmparser::validate(&wasm).expect("wasm validates");
}

#[test]
fn legacy_explicit_in_still_compiles() {
// Regression: don't break the legacy form. The grammar tries
// `block_expr` first, fails on the `in` keyword after the rhs, rolls
// back, then `let_expr` matches.
let source = "module test\n\
fn entry(): I32 = let x = 1 in let y = 2 in x\n";
let wasm = compile_to_wasm(source, "legacy-in");
wasmparser::validate(&wasm).expect("wasm validates");
}

#[test]
fn implicit_in_let_lin_chain_parses() {
// `let!` (linear) form bridge.eph uses — `let! (ch2, msg) = ipc_recv(ch)`
// followed by more lets. We only assert PARSE here; typing the linear
// form requires extern fns this fixture doesn't have.
let source = "module test\n\
fn use_pair(p: (I32, I32)): I32 =\n\
let! (a, b) = p\n\
let c = a\n\
c\n";
let _ = parse_surface_module(source, "let-lin-block").expect("must parse");
}
46 changes: 46 additions & 0 deletions src/ephapax-cli/tests/v2_grammar_phase_gh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
//
// Phase G + H regressions for hyperpolymath/ephapax#43:
//
// G — `extern "abi" { type Foo }` items register as opaque types in the
// desugar registry; `SurfaceTy::Named { name: "Foo" }` resolves to
// `Ty::Base(I32)` (host handle representation).
//
// H — `Unit` and `Bytes` are built-in type-name aliases. `Unit` is the
// type-position spelling of the literal `()`; `Bytes` resolves to
// `I32` until a stdlib `Bytes` ADT lands.

use ephapax_desugar::desugar;
use ephapax_parser::parse_surface_module;
use ephapax_typing::type_check_module;

const EXTERN_ABSTRACT: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../tests/v2-grammar/fixtures/extern-abstract-types.eph"
));

#[test]
fn extern_abstract_types_desugar_to_i32_handles() {
let surface = parse_surface_module(EXTERN_ABSTRACT, "extern-abstract").expect("must parse");
let core = desugar(&surface).expect("must desugar");
type_check_module(&core).expect("must type-check");
let wasm = ephapax_wasm::compile_module(&core).expect("must codegen");
wasmparser::validate(&wasm).expect("wasm validates");
}

#[test]
fn unit_alias_resolves_to_base_unit() {
let source = "module test\n\
fn noop(): Unit = ()\n";
let surface = parse_surface_module(source, "unit-alias").expect("must parse");
let _ = desugar(&surface).expect("Unit must alias to ()");
}

#[test]
fn bytes_alias_resolves_to_i32() {
let source = "module test\n\
fn id(b: Bytes): Bytes = b\n";
let surface = parse_surface_module(source, "bytes-alias").expect("must parse");
let core = desugar(&surface).expect("Bytes must alias to I32");
type_check_module(&core).expect("must type-check");
}
75 changes: 72 additions & 3 deletions src/ephapax-desugar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ pub struct DataRegistry {
constructors: HashMap<SmolStr, ConstructorInfo>,
/// Data type name → (params, constructors)
types: HashMap<SmolStr, (Vec<SmolStr>, Vec<ConstructorDef>)>,
/// Extern-declared opaque types — `extern "abi" { type Foo }`. They
/// have no constructors visible to the surface language; their core
/// representation is `I32` (the host runtime treats them as handles
/// or pointers). Listed separately from `types` because they don't
/// participate in `Construct`/`Match` desugaring.
extern_types: HashMap<SmolStr, ()>,
}

impl DataRegistry {
Expand Down Expand Up @@ -151,6 +157,13 @@ impl DataRegistry {
);
}

/// Register an opaque type from an `extern "abi" { type Foo }` block.
/// Subsequent `SurfaceTy::Named { name: "Foo" }` references resolve
/// to `Ty::Base(BaseTy::I32)` (host handle representation).
pub fn register_extern_type(&mut self, name: SmolStr) {
self.extern_types.insert(name, ());
}

/// Look up a constructor by name.
fn get_ctor(&self, name: &str) -> Option<&ConstructorInfo> {
self.constructors.get(name)
Expand All @@ -160,6 +173,11 @@ impl DataRegistry {
fn get_type_ctors(&self, name: &str) -> Option<&(Vec<SmolStr>, Vec<ConstructorDef>)> {
self.types.get(name)
}

/// `true` if `name` was declared as an extern opaque type.
fn is_extern_type(&self, name: &str) -> bool {
self.extern_types.contains_key(name)
}
}

// =========================================================================
Expand Down Expand Up @@ -193,10 +211,20 @@ impl Desugarer {
/// First pass: collect all data declarations into the registry.
/// Second pass: desugar all declarations.
pub fn desugar_module(&mut self, module: &SurfaceModule) -> Result<Module, DesugarError> {
// First pass: register all data types
// First pass: register all data types AND extern opaque types so
// subsequent `desugar_ty` calls (against fn signatures, etc.) can
// resolve `Window` / `IpcChannel` / etc. as `I32` handles.
for decl in &module.decls {
if let SurfaceDecl::Data(data) = decl {
self.registry.register(data);
match decl {
SurfaceDecl::Data(data) => self.registry.register(data),
SurfaceDecl::Extern(block) => {
for item in &block.items {
if let ephapax_surface::ExternItem::Type(name) = item {
self.registry.register_extern_type(name.clone());
}
}
}
_ => {}
}
}

Expand Down Expand Up @@ -498,6 +526,47 @@ impl Desugarer {
/// `Option(I32)` → `() + I32`
/// `Result(I32, Bool)` → `I32 + Bool`
fn desugar_named_type(&self, name: &SmolStr, args: &[SurfaceTy]) -> Result<Ty, DesugarError> {
// Built-in type aliases that aren't (yet) keywords in the lexer.
// `Unit` is the type-position spelling of the literal `()`; bridge.eph
// and other ML-adjacent corpora write it freely.
if name.as_str() == "Unit" {
if !args.is_empty() {
return Err(DesugarError::TypeArityMismatch {
name: name.to_string(),
expected: 0,
got: args.len(),
});
}
return Ok(Ty::Base(BaseTy::Unit));
}
// `Bytes` is the conventional name for a host-managed buffer. Until
// the stdlib publishes a real `Bytes` ADT, treat it as an I32 handle
// (the wasm host passes pointer/length pairs across `__ffi` calls;
// for direct extern-fn signatures the handle alone is enough).
if name.as_str() == "Bytes" {
if !args.is_empty() {
return Err(DesugarError::TypeArityMismatch {
name: name.to_string(),
expected: 0,
got: args.len(),
});
}
return Ok(Ty::Base(BaseTy::I32));
}

// Opaque extern types resolve to `I32` (host handle / pointer).
// They take no type arguments — extern types are monomorphic.
if self.registry.is_extern_type(name.as_str()) {
if !args.is_empty() {
return Err(DesugarError::TypeArityMismatch {
name: name.to_string(),
expected: 0,
got: args.len(),
});
}
return Ok(Ty::Base(BaseTy::I32));
}

let (params, ctors) = self.registry.get_type_ctors(name.as_str()).ok_or_else(|| {
DesugarError::UnknownType {
name: name.to_string(),
Expand Down
39 changes: 38 additions & 1 deletion src/ephapax-parser/src/ephapax.pest
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,20 @@ expression = {
seq_expr = { single_expr ~ (";" ~ single_expr)* }

single_expr = {
let_lin_expr
// Implicit-`in` block: a chain of `let`/`let!` bindings without `in`
// keywords, followed by a final result expression. Each binding's body
// is "everything that comes after it inside this block." Tried first so
// that bridge-eph-style sequences like
//
// let! (ch2, msg_bytes) = ipc_recv(ch)
// let msg: Msg = decode_msg(msg_bytes)
// run(ch2, msg)
//
// parse without explicit `in` keywords. PEG ordering means the legacy
// `let x = e in body` form still works — if the parser sees `in` after
// the rhs, this rule fails and `let_expr` (next in the list) succeeds.
block_expr
| let_lin_expr
| let_expr
| lambda_expr
| if_expr
Expand All @@ -178,6 +191,30 @@ single_expr = {
| or_expr
}

// One or more `sequential_let`s followed by a trailing result expression.
// The trailing expression can itself be any `expression` (which folds back
// through `seq_expr` / `single_expr`), so nested block forms and explicit
// `let ... in ...` both compose normally.
block_expr = { sequential_let+ ~ expression }

sequential_let = {
("let!" | "let") ~ let_binder ~ (":" ~ ty)? ~ "=" ~ block_rhs
}

// A `sequential_let`'s rhs may be any expression form *except* a top-level
// `let`/`let!` — those participate in the block via the next iteration of
// `sequential_let`. Parenthesised lets and lets inside lambdas still parse
// because their parens / lambda contexts route through `expression` again.
block_rhs = {
lambda_expr
| if_expr
| region_expr
| match_expr
| case_expr
| handle_expr
| or_expr
}

// Pattern matching: match x of | None => 0 | Some(v) => v end
match_expr = {
"match" ~ expression ~ "of"
Expand Down
Loading