Skip to content

Commit 94e0eb5

Browse files
hyperpolymathclaude
andcommitted
feat(syntax,parser): core match_expr dispatch + Pattern AST (#61)
Adds `ExprKind::Match { scrutinee, arms }` and `MatchArm { pattern, guard, body }` to `ephapax-syntax`, extends `Pattern` with `Literal` and `Constructor` variants (joining the existing Wildcard/Var/Pair/ Tuple/Unit), and adds `parse_match_expr_core` + `parse_pattern_core` to the core parser. `Rule::match_expr` is now dispatched from both `parse_expression` and `parse_single_expr_core`, so the example from the issue — `match x of | Some(v) => v | None => 0 end` — parses through the core parser direct path instead of failing with `"Unexpected single_expr: match_expr"`. This is option 1 from the issue (structured AST + Pattern, not desugar-to-Case). The runtime path (surface → desugar → core) continues to lower its own `SurfaceExprKind::Match` to nested `ExprKind::Case`, so the surface pipeline is unaffected. The new core variant exists for the core-parser-direct path and tooling consumers that want the structured shape. Downstream consumers (11 exhaustive `match ExprKind` sites across the workspace): Walking arms (subexpression-correct, sound for analyses): - ephapax-analysis/src/{escape,free_vars,liveness}.rs — walk scrutinee + each arm body with pattern's bound_vars in scope - ephapax-linear/src/{linear,affine}.rs — walk subexprs with pattern bindings; TODO marker for proper N-arm branch-agreement - ephapax-wasm/src/lib.rs free-vars collector — walk subexprs Stub-erroring (forward-compatible, honest about scope): - ephapax-typing/src/lib.rs — new `TypeError::NotYetSupportedInCore` variant; the typechecker tells callers to use the surface path - ephapax-wasm/src/lib.rs compile_expr — emits `unreachable` - ephapax-interp/src/lib.rs — returns `RuntimeError::Unimplemented` Structured render: - ephapax-ir/src/lib.rs — `(match scrutinee (arm <pattern> <body>) ...)` with a `pattern_to_sexpr` helper Pass-through (find_let_binding_span): - ephapax-lsp/src/main.rs — recurses through scrutinee + arms Tests (parser-level, hits AC): - `test_parse_core_match_constructor_and_var_patterns` — the AC example with Some(v)/None - `test_parse_core_match_wildcard_pattern` — `_` + literal arms - `test_parse_module_with_core_match` — the AC example wrapped in a module declaration, dispatched via parse_expression Workspace lib tests: 294 pass, 0 fail. Closes #61. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 931cae5 commit 94e0eb5

12 files changed

Lines changed: 497 additions & 3 deletions

File tree

ephapax-linear/src/affine.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,29 @@ impl AffineChecker {
311311
}
312312
}
313313
}
314+
315+
// Core `match`: affine allows arms to disagree on
316+
// consumption (the non-consuming arm implicitly drops),
317+
// so a simple walk-with-pattern-binding mirrors the
318+
// existing arm-disagreement freedom for `if` and `case`.
319+
// TODO(ephapax#61 follow-up): N-arm `merge_affine_branches`
320+
// for tighter cross-arm reasoning.
321+
ExprKind::Match { scrutinee, arms } => {
322+
self.walk_expr(scrutinee);
323+
for arm in arms {
324+
let bound = arm.pattern.bound_vars();
325+
for v in &bound {
326+
self.ctx.bind(v.clone(), BindingForm::Let, None);
327+
}
328+
if let Some(guard) = &arm.guard {
329+
self.walk_expr(guard);
330+
}
331+
self.walk_expr(&arm.body);
332+
for v in bound.iter().rev() {
333+
self.ctx.unbind(v);
334+
}
335+
}
336+
}
314337
}
315338
}
316339

ephapax-linear/src/linear.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,31 @@ impl LinearChecker {
374374
}
375375
}
376376
}
377+
378+
// Core `match`: walk scrutinee + each arm body with the
379+
// pattern's bound vars in scope. TODO(ephapax#61 follow-up):
380+
// implement proper N-arm branch-agreement checking so a
381+
// linear var consumed in only some arms is rejected. For
382+
// now we simply walk subexpressions to keep the discipline
383+
// checker sound on the structural fragment; programs that
384+
// produce `ExprKind::Match` today come only from the core
385+
// parser direct path and have no existing test coverage.
386+
ExprKind::Match { scrutinee, arms } => {
387+
self.walk_expr(scrutinee);
388+
for arm in arms {
389+
let bound = arm.pattern.bound_vars();
390+
for v in &bound {
391+
self.ctx.bind(v.clone(), BindingForm::Let, None);
392+
}
393+
if let Some(guard) = &arm.guard {
394+
self.walk_expr(guard);
395+
}
396+
self.walk_expr(&arm.body);
397+
for v in bound.iter().rev() {
398+
self.ctx.unbind(v);
399+
}
400+
}
401+
}
377402
}
378403
}
379404

src/ephapax-analysis/src/escape.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,16 @@ impl EscapeAnalysis {
195195
}
196196
}
197197

198+
ExprKind::Match { scrutinee, arms } => {
199+
Self::analyze_expr(scrutinee, escaping, in_escaping_context);
200+
for arm in arms {
201+
if let Some(guard) = &arm.guard {
202+
Self::analyze_expr(guard, escaping, in_escaping_context);
203+
}
204+
Self::analyze_expr(&arm.body, escaping, in_escaping_context);
205+
}
206+
}
207+
198208
// Literals and string allocations never escape
199209
ExprKind::Lit(_) | ExprKind::StringNew { .. } => {}
200210
}

src/ephapax-analysis/src/free_vars.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,20 @@ impl FreeVarAnalysis {
194194
}
195195
}
196196

197+
ExprKind::Match { scrutinee, arms } => {
198+
Self::collect(scrutinee, free, bound);
199+
for arm in arms {
200+
let mut arm_bound = bound.clone();
201+
for v in arm.pattern.bound_vars() {
202+
arm_bound.insert(v);
203+
}
204+
if let Some(guard) = &arm.guard {
205+
Self::collect(guard, free, &mut arm_bound);
206+
}
207+
Self::collect(&arm.body, free, &mut arm_bound);
208+
}
209+
}
210+
197211
ExprKind::Lit(_) | ExprKind::StringNew { .. } => {}
198212
}
199213
}

src/ephapax-analysis/src/liveness.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,21 @@ impl LivenessAnalysis {
200200
Self::compute(body, live);
201201
}
202202

203+
ExprKind::Match { scrutinee, arms } => {
204+
for arm in arms.iter().rev() {
205+
let mut arm_live = HashSet::new();
206+
Self::compute(&arm.body, &mut arm_live);
207+
if let Some(guard) = &arm.guard {
208+
Self::compute(guard, &mut arm_live);
209+
}
210+
for v in arm.pattern.bound_vars() {
211+
arm_live.remove(&v);
212+
}
213+
live.extend(arm_live);
214+
}
215+
Self::compute(scrutinee, live);
216+
}
217+
203218
ExprKind::Lit(_) | ExprKind::StringNew { .. } => {}
204219
}
205220
}

src/ephapax-interp/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,9 @@ impl Interpreter {
477477
"effect handle not yet implemented in interpreter".into(),
478478
))
479479
}
480+
ExprKind::Match { .. } => Err(RuntimeError::Unimplemented(
481+
"core ExprKind::Match not yet implemented in interpreter — use the surface (parse_surface_module → desugar) path which lowers match to nested Case".into(),
482+
)),
480483
}
481484
}
482485

src/ephapax-ir/src/lib.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,61 @@ fn expr_to_sexpr(expr: &Expr) -> SExpr {
548548
}
549549
SExpr::List(elems)
550550
}
551+
// `(match scrutinee (arm <pattern> <body>) ...)` — added
552+
// ephapax#61 so the structured `match` survives IR round-trip.
553+
// Guards render as `(arm <pattern> (guard <e>) <body>)`.
554+
ExprKind::Match { scrutinee, arms } => {
555+
let mut elems =
556+
vec![SExpr::Atom("match".into()), expr_to_sexpr(scrutinee)];
557+
for arm in arms {
558+
let mut arm_elems = vec![
559+
SExpr::Atom("arm".into()),
560+
pattern_to_sexpr(&arm.pattern),
561+
];
562+
if let Some(guard) = &arm.guard {
563+
arm_elems.push(SExpr::List(vec![
564+
SExpr::Atom("guard".into()),
565+
expr_to_sexpr(guard),
566+
]));
567+
}
568+
arm_elems.push(expr_to_sexpr(&arm.body));
569+
elems.push(SExpr::List(arm_elems));
570+
}
571+
SExpr::List(elems)
572+
}
573+
}
574+
}
575+
576+
fn pattern_to_sexpr(pattern: &ephapax_syntax::Pattern) -> SExpr {
577+
use ephapax_syntax::Pattern;
578+
match pattern {
579+
Pattern::Wildcard => SExpr::Atom("_".into()),
580+
Pattern::Unit => SExpr::List(vec![SExpr::Atom("unit".into())]),
581+
Pattern::Var(v) => SExpr::List(vec![
582+
SExpr::Atom("var".into()),
583+
SExpr::Atom(escape_atom(v)),
584+
]),
585+
Pattern::Literal(lit) => {
586+
SExpr::List(vec![SExpr::Atom("lit".into()), lit_to_sexpr(lit)])
587+
}
588+
Pattern::Pair(l, r) => SExpr::List(vec![
589+
SExpr::Atom("pair".into()),
590+
pattern_to_sexpr(l),
591+
pattern_to_sexpr(r),
592+
]),
593+
Pattern::Tuple(ps) => {
594+
let mut elems = vec![SExpr::Atom("tuple".into())];
595+
elems.extend(ps.iter().map(pattern_to_sexpr));
596+
SExpr::List(elems)
597+
}
598+
Pattern::Constructor { ctor, args } => {
599+
let mut elems = vec![
600+
SExpr::Atom("ctor".into()),
601+
SExpr::Atom(escape_atom(ctor)),
602+
];
603+
elems.extend(args.iter().map(pattern_to_sexpr));
604+
SExpr::List(elems)
605+
}
551606
}
552607
}
553608

src/ephapax-lsp/src/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,16 @@ fn find_let_binding_span(expr: &Expr, target: &str) -> Option<Span> {
732732
.iter()
733733
.find_map(|c| find_let_binding_span(&c.body, target))
734734
}),
735+
ExprKind::Match { scrutinee, arms } => {
736+
find_let_binding_span(scrutinee, target).or_else(|| {
737+
arms.iter().find_map(|arm| {
738+
arm.guard
739+
.as_deref()
740+
.and_then(|g| find_let_binding_span(g, target))
741+
.or_else(|| find_let_binding_span(&arm.body, target))
742+
})
743+
})
744+
}
735745
ExprKind::Var(_) | ExprKind::Lit(_) | ExprKind::StringNew { .. } => None,
736746
}
737747
}

0 commit comments

Comments
 (0)