Skip to content

Commit e2794c9

Browse files
Aiur: support non-tail match on a non-variable scrutinee (#435)
The match compiler hoists a non-variable match scrutinee into a fresh let (MatchCompiler.switch), so `let x = match foo(bar) {..}` simplified to `let x = (let w = foo(bar); match w {..})`. That buried the match one let deep, where Lower's non-tail-match detector — which only fires when a match is the immediate letVar/letWild RHS — couldn't see it, and lowering threw "Non-tail match in arbitrary position (not supported)". Fix in the Simple stage: mkLetFloating floats leading lets out of a let-RHS (`let x = (let w = e; rest); b` -> `let w = e; let x = rest; b`), restoring the invariant that a non-tail match sits directly in its letVar/letWild RHS. The hoisted w's are fresh match-compiler locals, so widening their scope cannot capture anything. Tests: ntm_match_on_call exercises a function-call scrutinee, folded into the non_tail_match aggregate in both the aiur and aiur-cross suites.
1 parent 3fe83fe commit e2794c9

3 files changed

Lines changed: 47 additions & 7 deletions

File tree

Ix/Aiur/Compiler/Simple.lean

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ Only valid when it doesn't shadow any other binding. Public so proofs in other
2727
modules can cite the definition (e.g., via unfolding). -/
2828
abbrev tmpVar : Local := .idx 0
2929

30+
/-- Build `let pat = v in b`, but first float any leading `let`s out of `v`:
31+
`let pat = (let w = e in rest) in b` ⤳ `let w = e in let pat = rest in b`.
32+
33+
The match compiler hoists a non-variable `match` scrutinee into a `let`
34+
(`MatchCompiler.switch`), so `let x = match foo(bar) {..}` simplifies to
35+
`let x = (let w = foo(bar) in match w {..})`. That buries the `match` one
36+
`let` deep, where `Lower`'s non-tail-match detector (which only fires when a
37+
`match` is the *immediate* `letVar`/`letWild` RHS) can't see it. Floating the
38+
hoisted `let`s outward restores the invariant: the `match` becomes the direct
39+
RHS again. The hoisted `w`s are fresh match-compiler locals, so widening their
40+
scope over `b` cannot capture anything. -/
41+
def mkLetFloating (τ : Typ) (e : Bool) (pat : Pattern) (v b : Term) : Term :=
42+
match v with
43+
| .let τ' e' pat' v' rest => .let τ' e' pat' v' (mkLetFloating τ e pat rest b)
44+
| _ => .let τ e pat v b
45+
termination_by sizeOf v
46+
decreasing_by decreasing_tactic
47+
3048
/-- `simplifyTypedTerm` walks a typed term, producing a term of the same shape
3149
whose `match`es have been pre-compiled down to the decision-tree form, and whose
3250
`let`s bind only simple locals or wildcards. It operates in the `CheckError`
@@ -38,11 +56,11 @@ def simplifyTypedTerm (decls : Source.Decls) : Term → Except CheckError Term
3856
| .let τ e (.var x) v b => do
3957
let v' ← simplifyTypedTerm decls v
4058
let b' ← simplifyTypedTerm decls b
41-
pure (.let τ e (.var x) v' b')
59+
pure (mkLetFloating τ e (.var x) v' b')
4260
| .let τ e .wildcard v b => do
4361
let v' ← simplifyTypedTerm decls v
4462
let b' ← simplifyTypedTerm decls b
45-
pure (.let τ e .wildcard v' b')
63+
pure (mkLetFloating τ e .wildcard v' b')
4664
| .let τ e pat v b => do
4765
let v' ← simplifyTypedTerm decls v
4866
let b' ← simplifyTypedTerm decls b
@@ -52,7 +70,7 @@ def simplifyTypedTerm (decls : Source.Decls) : Term → Except CheckError Term
5270
match MatchCompiler.decisionToTyped b'.typ tmp.typ tree with
5371
| some rewrite => rewrite
5472
| none => .match b'.typ b'.escapes tmp [(pat, b')]
55-
pure (.let τ e (.var tmpVar) v' body)
73+
pure (mkLetFloating τ e (.var tmpVar) v' body)
5674
| .match τ e scrut branches => do
5775
let scrut' ← simplifyTypedTerm decls scrut
5876
let branches' ← branches.attach.mapM fun pb =>

Tests/Aiur/Aiur.lean

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,15 @@ def toplevel := ⟦
552552
x + 1
553553
}
554554

555+
-- Non-tail match whose scrutinee is a function call (`let x = match foo(bar) {...}`).
556+
-- The scrutinee is hoisted into a fresh let by the match compiler; the
557+
-- continuation must still reach `matchContinue`. ntm_helper(x) = x*x+1,
558+
-- so a=0 -> 1 -> 100 -> 101.
559+
fn ntm_match_on_call(a: G) -> G {
560+
let x = match ntm_helper(a) { 1 => 100, 5 => 200, _ => a * a, };
561+
x + 1
562+
}
563+
555564
-- Non-tail match with store/load in branches (lookup gating)
556565
fn ntm_store_load(a: G) -> G {
557566
let x = match a { 0 => load(store(42)), _ => load(store(a)), };
@@ -777,8 +786,10 @@ def toplevel := ⟦
777786
let r19 = ntm_recursive_test();
778787
-- Nested early return (yields 0, sum unchanged)
779788
let r20 = ntm_nested(0, 0);
789+
-- Function-call scrutinee: 101 + 201 + 10 = 312
790+
let r21 = ntm_match_on_call(0) + ntm_match_on_call(2) + ntm_match_on_call(3);
780791
r1 + r2 + r3 + r4 + r5 + r6 + r7 + r8 + r9 + r10
781-
+ r11 + r12 + r13 + r14 + r15 + r16 + r17 + r18 + r19 + r20
792+
+ r11 + r12 + r13 + r14 + r15 + r16 + r17 + r18 + r19 + r20 + r21
782793
}
783794
784795

@@ -935,8 +946,8 @@ def aiurTestCases : List AiurTestCase := [
935946
.noIO `template_pair #[] #[10, 20],
936947
.noIO `template_nested #[] #[7],
937948

938-
-- Non-tail match: all patterns in one proof
939-
.noIO `non_tail_match #[] #[2281],
949+
-- Non-tail match: all patterns in one proof (incl. function-call scrutinee)
950+
.noIO `non_tail_match #[] #[2593],
940951
]
941952

942953
end

Tests/Aiur/Cross.lean

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,15 @@ def toplevel : Source.Toplevel := ⟦
541541
x + 1
542542
}
543543

544+
-- Non-tail match whose scrutinee is a function call (`let x = match foo(bar) {...}`).
545+
-- The scrutinee is hoisted into a fresh let by the match compiler; the
546+
-- continuation must still reach `matchContinue`. ntm_helper(x) = x*x+1,
547+
-- so a=0 -> 101, a=2 -> 201, a=3 -> 10.
548+
fn ntm_match_on_call(a: G) -> G {
549+
let x = match ntm_helper(a) { 1 => 100, 5 => 200, _ => a * a, };
550+
x + 1
551+
}
552+
544553
-- Pre-branch constant multiplied in a branch (no default)
545554
pub fn ntm_const_mul(a: G) -> G {
546555
let c = 5;
@@ -1058,8 +1067,10 @@ def toplevel : Source.Toplevel := ⟦
10581067
let r18 = ntm_refutable_let_in_match(0);
10591068
let r19 = ntm_recursive_test();
10601069
let r20 = ntm_nested(0, 0);
1070+
-- Function-call scrutinee: 101 + 201 + 10 = 312
1071+
let r21 = ntm_match_on_call(0) + ntm_match_on_call(2) + ntm_match_on_call(3);
10611072
r1 + r2 + r3 + r4 + r5 + r6 + r7 + r8 + r9 + r10
1062-
+ r11 + r12 + r13 + r14 + r15 + r16 + r17 + r18 + r19 + r20
1073+
+ r11 + r12 + r13 + r14 + r15 + r16 + r17 + r18 + r19 + r20 + r21
10631074
}
10641075
10651076

0 commit comments

Comments
 (0)