From 43775fda7544809bf0129f54d16552dcf7e91a0a Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Fri, 3 Jul 2026 22:45:23 +0800 Subject: [PATCH 01/15] WIP --- .../hkmc2/codegen/CompilationPipeline.scala | 2 +- .../test/mlscript/codegen/ConfigDirective.mls | 18 ++-- .../mlscript/codegen/TailRecFormerFailure.mls | 55 ++++------ .../src/test/mlscript/deforest/cyclic.mls | 51 +++------ .../test/mlscript/tailrec/MultiArgLists.mls | 102 +++++++++--------- .../src/test/mlscript/tailrec/TailRecOpt.mls | 43 ++++---- 6 files changed, 123 insertions(+), 148 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index 8694ed0906..55657e5d87 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -55,8 +55,8 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): else prog runPass("ClassParamFlattener")(ClassParamFlattener.apply) runPass("ReflectionInstrumenter")(ReflectionInstrumenter(using summon).apply) - runPass("TailRecOpt")(TailRecOpt().transform) preOptimizeHook(result) + runPass("TailRecOpt")(TailRecOpt().transform) runPass("WorkerWrapper")(WorkerWrapper(symbolsToPreserve, otl, printer)) runPass("BlockSimplifier")(BlockSimplifier(symbolsToPreserve, otl, printer).apply) runPass("DeadParamElim")(otl.givenIn(DeadParamElim.apply)) diff --git a/hkmc2/shared/src/test/mlscript/codegen/ConfigDirective.mls b/hkmc2/shared/src/test/mlscript/codegen/ConfigDirective.mls index 07f7773926..2d1537fc1b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ConfigDirective.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ConfigDirective.mls @@ -2,9 +2,9 @@ // Test 1: #config global directive - test_before has tailRecOpt (default) -:sir +:soir fun test_before(n, acc) = if n == 0 then acc else test_before(n - 1, n + acc) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define test_before⁰ as fun test_before¹(n, acc) { //│ loop loopLabel: //│ let scrut, tmp, tmp1; @@ -19,7 +19,7 @@ fun test_before(n, acc) = if n == 0 then acc else test_before(n - 1, n + acc) //│ set acc = tmp1; //│ continue loopLabel //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -48,9 +48,9 @@ fun test_after(n, acc) = if n == 0 then acc else test_after(n - 1, n + acc) // Test 3: Re-enable tail recursion optimization #config(tailRecOpt: true) -:sir +:soir fun test_reenabled(n, acc) = if n == 0 then acc else test_reenabled(n - 1, n + acc) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define test_reenabled⁰ as fun test_reenabled¹(n, acc) { //│ loop loopLabel: //│ let scrut, tmp, tmp1; @@ -65,7 +65,7 @@ fun test_reenabled(n, acc) = if n == 0 then acc else test_reenabled(n - 1, n + a //│ set acc = tmp1; //│ continue loopLabel //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -92,9 +92,9 @@ fun foo(n, acc) = if n == 0 then acc else foo(n - 1, n + acc) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— // Test 5: bar (no annotation) should still have tailRecOpt -:sir +:soir fun bar(n, acc) = if n == 0 then acc else bar(n - 1, n + acc) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define bar⁰ as fun bar¹(n, acc) { //│ loop loopLabel: //│ let scrut, tmp, tmp1; @@ -109,7 +109,7 @@ fun bar(n, acc) = if n == 0 then acc else bar(n - 1, n + acc) //│ set acc = tmp1; //│ continue loopLabel //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/codegen/TailRecFormerFailure.mls b/hkmc2/shared/src/test/mlscript/codegen/TailRecFormerFailure.mls index d7c254ed83..804a65e63c 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/TailRecFormerFailure.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/TailRecFormerFailure.mls @@ -6,95 +6,82 @@ open Stack // Note: tail-rec optimized -:sir +:soir fun go(x) = if x is Cons(_, _) then "Z" n then go([]) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define go⁰ as fun go¹(x) { //│ loop loopLabel: -//│ let n, arg$Cons$0$, arg$Cons$1$, tmp; +//│ let tmp; //│ match x //│ Cons⁰ => -//│ set arg$Cons$0$ = x.head⁰; -//│ set arg$Cons$1$ = x.tail⁰; //│ return "Z" //│ else -//│ set n = x; //│ set tmp = []; //│ set x = tmp; //│ continue loopLabel //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— // Note: tail-rec optimized -:sir +:soir fun go(x) = if x is Cons(_, Nil) then "Z" n then go([]) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define go² as fun go³(x) { //│ loop loopLabel: -//│ let n, arg$Cons$0$, arg$Cons$1$, tmp, tmp1; +//│ let arg$Cons$1$, tmp, tmp1; //│ match x //│ Cons⁰ => -//│ set arg$Cons$0$ = x.head⁰; //│ set arg$Cons$1$ = x.tail⁰; //│ match arg$Cons$1$ //│ Nil⁰ => //│ return "Z" //│ else -//│ set n = x; //│ set tmp = []; //│ set x = tmp; //│ continue loopLabel //│ end //│ else -//│ set n = x; //│ set tmp1 = []; //│ set x = tmp1; //│ continue loopLabel //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— // Note: Used not to be tail-rec optimized; now fixed -:sir +:soir :patMatConsequentSharingThreshold 1 // ^ The default value allowed duplicating "Z" in the above version fun go(x) = if x is Cons(_, Nil) then "Z" n then go([]) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define go⁴ as fun go⁵(x) { //│ loop loopLabel: -//│ let n, arg$Cons$0$, arg$Cons$1$, tmp; -//│ block σ: -//│ match x -//│ Cons⁰ => -//│ set arg$Cons$0$ = x.head⁰; -//│ set arg$Cons$1$ = x.tail⁰; -//│ match arg$Cons$1$ -//│ Nil⁰ => -//│ return "Z" -//│ else -//│ break σ -//│ end -//│ else -//│ break σ -//│ end -//│ set n = x; +//│ let arg$Cons$1$, tmp; +//│ match x +//│ Cons⁰ => +//│ set arg$Cons$1$ = x.tail⁰; +//│ match arg$Cons$1$ +//│ Nil⁰ => +//│ return "Z" +//│ else +//│ end +//│ end //│ set tmp = []; //│ set x = tmp; //│ continue loopLabel -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— - diff --git a/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls b/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls index 2f579b10a3..c805ac207b 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls @@ -67,7 +67,7 @@ maxOpt(id(1 :: 2 :: Nil)) :deforest -:sir +:soir :expect Some(2) fun modSome(x, m) = if x is Some(n) and @@ -83,60 +83,43 @@ modSome(Some(17), 3) //│ deforest > match: x⁰ //│ deforest > fields: x⁰.x¹ //│ deforest > <<< fusing <<< -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let tmp, modSome_12$modSome, modSome_12$x_Some, modSome_12$x_rest, Some_x; -//│ @private -//│ define modSome_12$modSome as fun modSome_12$modSome⁰(x, m) { -//│ let n, scrut, arg$Some$0$, tmp1, tmp2; -//│ return x(modSome⁰, m, n, scrut, arg$Some$0$, tmp1, tmp2) -//│ }; +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ let modSome_12$x_Some$worker; //│ @affine(1) @private -//│ define modSome_12$x_Some as fun modSome_12$x_Some⁰(Some_x1)(fv_modSome, fv_m, fv_n, fv_scrut, fv_arg$Some$0$, fv_tmp, fv_tmp1) { -//│ set fv_arg$Some$0$ = Some_x1; -//│ set fv_n = fv_arg$Some$0$; -//│ set fv_scrut = >=⁰(fv_n, fv_m); +//│ define modSome_12$x_Some$worker as fun modSome_12$x_Some$worker⁰(Some_x, fv_m, fv_scrut, fv_tmp) { +//│ set fv_scrut = >=⁰(Some_x, fv_m); //│ match fv_scrut //│ true => -//│ let Some_x2; -//│ set fv_tmp = -⁰(fv_n, fv_m); -//│ set Some_x2 = fv_tmp; -//│ set fv_tmp1 = modSome_12$x_Some⁰(Some_x2); -//│ return modSome_12$modSome⁰(fv_tmp1, fv_m) +//│ set fv_tmp = -⁰(Some_x, fv_m); +//│ return modSome_12$x_Some$worker⁰(fv_tmp, fv_m, undefined, undefined) //│ else -//│ return Some⁰(fv_n) +//│ return Some⁰(Some_x) //│ end //│ }; -//│ @private -//│ define modSome_12$x_rest as fun modSome_12$x_rest⁰() { -//│ return null -//│ }; -//│ define modSome¹ as fun modSome⁰(x, m) { +//│ define modSome⁰ as fun modSome¹(x, m) { //│ loop loopLabel: -//│ let n, scrut, arg$Some$0$, tmp1, tmp2; +//│ let scrut, arg$Some$0$, tmp, tmp1; //│ match x //│ Some¹ => //│ set arg$Some$0$ = x.x¹; -//│ set n = arg$Some$0$; -//│ set scrut = >=⁰(n, m); +//│ set scrut = >=⁰(arg$Some$0$, m); //│ match scrut //│ true => -//│ set tmp1 = -⁰(n, m); -//│ set tmp2 = Some⁰(tmp1); -//│ set x = tmp2; +//│ set tmp = -⁰(arg$Some$0$, m); +//│ set tmp1 = Some⁰(tmp); +//│ set x = tmp1; //│ continue loopLabel //│ else -//│ return Some⁰(n) +//│ return Some⁰(arg$Some$0$) //│ end //│ None⁰ => //│ return None⁰ //│ else //│ throw new globalThis⁰.Error⁰("match error") //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; -//│ set Some_x = 17; -//│ set tmp = modSome_12$x_Some⁰(Some_x); -//│ return modSome_12$modSome⁰(tmp, 3) +//│ return modSome_12$x_Some$worker⁰(17, 3, undefined, undefined) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = Some(2) diff --git a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls index b6b5c3d99d..57a7473d91 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls @@ -12,13 +12,13 @@ sum(100, 0) // a multi-param-list function, so the self-recursive tail call is recognized // and optimized by TailRecOpt. :expect 500500 -:sir +:soir @tailrec fun loop(x)(y) = if x == 0 then y else loop(x - 1)(y + x) loop(1000)(0) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let loop$tailrec; //│ @private //│ define loop$tailrec as fun loop$tailrec⁰(x, y) { @@ -35,51 +35,66 @@ loop(1000)(0) //│ set y = tmp1; //│ continue loopLabel //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ @inline @tailrec //│ define loop⁰ as fun loop¹(x)(y) { return loop$tailrec⁰(x, y) }; -//│ return loop¹(1000)(0) +//│ return loop$tailrec⁰(1000, 0) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 500500 // Test: Function with multiple parameter lists, non-annotated (should still optimize to a loop) -:sir +:soir :expect 100 fun count(start)(stop) = if start == stop then start else count(start + 1)(stop) count(0)(100) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let count$tailrec; +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ let count$tailrec, start, inlinedVal; //│ @private -//│ define count$tailrec as fun count$tailrec⁰(start, stop) { +//│ define count$tailrec as fun count$tailrec⁰(start1, stop) { //│ loop loopLabel: //│ let scrut, tmp; -//│ set scrut = Predef⁰.equals⁰(start, stop); +//│ set scrut = Predef⁰.equals⁰(start1, stop); //│ match scrut //│ true => -//│ return start +//│ return start1 //│ else -//│ set tmp = +⁰(start, 1); -//│ set start = tmp; +//│ set tmp = +⁰(start1, 1); +//│ set start1 = tmp; //│ continue loopLabel //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ @inline -//│ define count⁰ as fun count¹(start)(stop) { -//│ return count$tailrec⁰(start, stop) +//│ define count⁰ as fun count¹(start1)(stop) { +//│ return count$tailrec⁰(start1, stop) //│ }; -//│ return count¹(0)(100) +//│ set start = 0; +//│ block inlinedLbl: +//│ loop loopLabel: +//│ let scrut, tmp; +//│ set scrut = Predef⁰.equals⁰(start, 100); +//│ match scrut +//│ true => +//│ set inlinedVal = start; +//│ break inlinedLbl +//│ else +//│ set tmp = +⁰(start, 1); +//│ set start = tmp; +//│ continue loopLabel +//│ end +//│ unreachable /* Rest of abortive labelled block */ +//│ return inlinedVal //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 100 // Test: Mutually recursive functions with multiple parameter lists -:sir +:soir fun even(n)(dummy) = if n == 0 then [true, dummy] else odd(n - 1)(dummy + 1) fun odd(n)(dummy) = if n == 0 then [false, dummy] else even(n - 1)(dummy + 1) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let even_odd; //│ define even_odd as fun even_odd⁰(id, param0, param1) { //│ loop loopLabel: @@ -117,9 +132,11 @@ fun odd(n)(dummy) = if n == 0 then [false, dummy] else even(n - 1)(dummy + 1) //│ end //│ end //│ }; +//│ @inline //│ define even⁰ as fun even¹(n)(dummy) { //│ return even_odd⁰(0, n, dummy) //│ }; +//│ @inline //│ define odd⁰ as fun odd¹(n)(dummy) { return even_odd⁰(1, n, dummy) }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -136,13 +153,13 @@ even(9)(0) // Spreads // This is the same as a test in `TailRecOpt.mls` but with some parameters separated and changed. -:sir +:soir fun f(n)(a, b, c, d, ...e)(m) = if n > 0 then @tailcall g(n)(m + 1) else [e, m] fun g(n)(m) = @tailcall f(n - 1)(...[1, 2], 3, ...[3, 4], ...[5, n], 7, n)(m) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let f_g; //│ define f_g as fun f_g⁰(id, param0, param1, param2, param3, param4, param5, param6) { //│ loop loopLabel: @@ -171,9 +188,9 @@ fun g(n)(m) = //│ set argList = [...tmp1, 3, ...tmp2, ...tmp3, 7, param0_tmp]; //│ set sliceRes = runtime⁰.Tuple⁰.slice⁰(argList, 4, 0); //│ set param1 = argList.0; -//│ set param2 = argList.1; -//│ set param3 = argList.2; -//│ set param4 = argList.3; +//│ do argList.1; +//│ do argList.2; +//│ do argList.3; //│ set param5 = sliceRes; //│ set param6 = param1_tmp; //│ set id = 0; @@ -186,6 +203,7 @@ fun g(n)(m) = //│ define f⁰ as fun f¹(n)(a, b, c, d, ...e)(m) { //│ return f_g⁰(0, n, a, b, c, d, e, m) //│ }; +//│ @inline //│ define g⁰ as fun g¹(n)(m) { //│ return f_g⁰(1, n, m, undefined, undefined, undefined, undefined, undefined) //│ }; @@ -196,32 +214,30 @@ fun g(n)(m) = g(10)(0) //│ = [[4, 5, 1, 7, 1], 9] -:sir +:soir fun f(a)(...z) = if z is [] then a [x, ...xs] then f(a + 1)(...xs) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let f$tailrec; //│ @private //│ define f$tailrec as fun f$tailrec⁰(a, z) { //│ loop loopLabel: -//│ let x, xs, middleElements, element0$, tmp; +//│ let middleElements, tmp; //│ match z //│ Array(0) => //│ return a //│ Array(1+) => -//│ set element0$ = runtime⁰.Tuple⁰.get⁰(z, 0); +//│ do runtime⁰.Tuple⁰.get⁰(z, 0); //│ set middleElements = runtime⁰.Tuple⁰.slice⁰(z, 1, 0); -//│ set xs = middleElements; -//│ set x = element0$; //│ set tmp = +⁰(a, 1); //│ set a = tmp; -//│ set z = xs; +//│ set z = middleElements; //│ continue loopLabel //│ else //│ throw new globalThis⁰.Error⁰("match error") //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ @inline //│ define f² as fun f³(a)(...z) { return f$tailrec⁰(a, z) }; @@ -234,10 +250,10 @@ f(0)(1, 2, 3, 4) // Ensure that the internal loop and the outer-facing definition have different param symbol IDs. -:lot +:olot :noInline fun f(a)(b) = f(a)(b) -//│ —————————————| Lowered IR Tree |———————————————————————————————————————————————————————————————————— +//│ ————————————| Optimized IR Tree |——————————————————————————————————————————————————————————————————— //│ Program: //│ main = Scoped(syms = {member:f$tailrec¹}): //│ body = Define: @@ -245,18 +261,12 @@ fun f(a)(b) = f(a)(b) //│ sym = member:f$tailrec¹ //│ dSym = term:f$tailrec² //│ params = Ls of -//│ ParamList: -//│ params = Ls of -//│ Param: -//│ sym = a⁰ -//│ modulefulness = Modulefulness of N -//│ Param: -//│ sym = b⁰ -//│ modulefulness = Modulefulness of N +//│ ParamList: ‹empty› //│ body = Label: //│ label = label:loopLabel⁰ //│ loop = true //│ body = Continue of label:loopLabel⁰ +//│ rest = Unreachable of "Rest of abortive labelled block" //│ rest = Define: \ //│ defn = FunDefn: //│ sym = member:f⁴ @@ -265,23 +275,19 @@ fun f(a)(b) = f(a)(b) //│ ParamList: //│ params = Ls of //│ Param: -//│ sym = a¹ +//│ sym = a⁰ //│ modulefulness = Modulefulness of N //│ ParamList: //│ params = Ls of //│ Param: -//│ sym = b¹ +//│ sym = b⁰ //│ modulefulness = Modulefulness of N //│ body = Return of Call: //│ fun = MemberRef{disamb=term:f$tailrec²}: //│ bms = member:f$tailrec¹ //│ disamb = term:f$tailrec² //│ argss = Ls of -//│ Ls of -//│ Arg: -//│ value = SimpleRef of a¹ -//│ Arg: -//│ value = SimpleRef of b¹ +//│ Nil //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls index 46015ac3e9..6e000968cf 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls @@ -8,7 +8,7 @@ fun sum(n) = sum_impl(n, 0) sum(20000) //│ = 200010000 -:sir +:soir :expect 50000 fun g(a, b, c, d) = f(a, b, c + d) fun f(a, b, c) = @@ -17,7 +17,7 @@ fun f(a, b, c) = b > 0 then g(a, b - 1, c, 2) else c f(10000, 20000, 0) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let g_f; //│ define g_f as fun g_f⁰(id, param0, param1, param2, param3) { //│ loop loopLabel: @@ -59,8 +59,10 @@ f(10000, 20000, 0) //│ define g⁰ as fun g¹(a, b, c, d) { //│ return g_f⁰(0, a, b, c, d) //│ }; -//│ define f⁰ as fun f¹(a, b, c) { return g_f⁰(1, a, b, c, undefined) }; -//│ return f¹(10000, 20000, 0) +//│ define f⁰ as fun f¹(a, b, c) { +//│ return g_f⁰(1, a, b, c, undefined) +//│ }; +//│ return g_f⁰(1, 10000, 20000, 0, undefined) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 50000 @@ -142,13 +144,13 @@ fun f(a, ...z) = if z is f(0, 1, 2, 3, 4) //│ = 4 -:sir +:soir fun f(n, a, b, c, d, ...e) = if n > 0 then @tailcall g(n - 1) else e fun g(n) = @tailcall f(n, ...[1, 2], 3, ...[3, 4], ...[5, n], 7, n) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let f_g; //│ define f_g as fun f_g⁰(id, param0, param1, param2, param3, param4, param5) { //│ loop loopLabel: @@ -173,10 +175,10 @@ fun g(n) = //│ set argList = [param0, ...tmp, 3, ...tmp1, ...tmp2, 7, param0]; //│ set sliceRes = runtime⁰.Tuple⁰.slice⁰(argList, 5, 0); //│ set param0 = argList.0; -//│ set param1 = argList.1; -//│ set param2 = argList.2; -//│ set param3 = argList.3; -//│ set param4 = argList.4; +//│ do argList.1; +//│ do argList.2; +//│ do argList.3; +//│ do argList.4; //│ set param5 = sliceRes; //│ set id = 0; //│ continue loopLabel @@ -206,7 +208,7 @@ fun f(x) = @tailcall f(x) @tailcall g() //│ ╔══[COMPILATION ERROR] This call is not in tail position. -//│ ║ l.205: @tailcall f(x) +//│ ║ l.207: @tailcall f(x) //│ ╙── ^^^^ :lift @@ -216,12 +218,12 @@ fun f(x) = @tailcall g() // Ensure y is set using x_tmp and x is assigned to x_tmp -:sir +:soir fun f(x, y) = if x == 0 then 0 else f(x - 1, x) -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define f⁴ as fun f⁵(x, y) { //│ loop loopLabel: //│ let scrut, tmp; @@ -230,14 +232,11 @@ fun f(x, y) = //│ true => //│ return 0 //│ else -//│ let x_tmp; //│ set tmp = -⁰(x, 1); -//│ set x_tmp = x; //│ set x = tmp; -//│ set y = x_tmp; //│ continue loopLabel //│ end -//│ end +//│ unreachable /* Rest of abortive labelled block */ //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -293,10 +292,10 @@ module A with @tailcall g(x - 1) A.f(10000) //│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.292: fun g(x) = if x < 0 then 0 else @tailcall f(x) +//│ ║ l.291: fun g(x) = if x < 0 then 0 else @tailcall f(x) //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.293: @tailcall g(x - 1) +//│ ║ l.292: @tailcall g(x - 1) //│ ╙── ^^^^^^^^ //│ ═══[RUNTIME ERROR] RangeError: Maximum call stack size exceeded @@ -323,12 +322,12 @@ class A with fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) (new A).f(10) //│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.322: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ║ l.321: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) //│ ╙── ^^^^^^^^ //│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. -//│ ║ l.322: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ║ l.321: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.323: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) +//│ ║ l.322: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) //│ ╙── ^^^^^^^^ //│ = 0 From da0d7b75d6c22467d5ef663e3bcb007755e242ce Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Fri, 3 Jul 2026 23:06:01 +0800 Subject: [PATCH 02/15] WIP --- .../src/main/scala/hkmc2/codegen/Block.scala | 10 + .../hkmc2/codegen/CompilationPipeline.scala | 9 + .../src/test/mlscript-compile/Runtime.mjs | 187 ++++++++++-------- .../src/test/mlscript/deforest/cyclic.mls | 19 +- .../deforest/eta-expansion/recursive.mls | 67 ++++--- .../src/test/mlscript/tailrec/Annots.mls | 19 +- .../src/test/mlscript/tailrec/Errors.mls | 104 +++++++--- .../test/mlscript/tailrec/MultiArgLists.mls | 10 + .../src/test/mlscript/tailrec/Simple.mls | 25 ++- .../src/test/mlscript/tailrec/TailRecOpt.mls | 28 ++- 10 files changed, 334 insertions(+), 144 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index a991599478..3af5109724 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala @@ -969,6 +969,16 @@ case class Call(fun: Path, argss: NELs[Ls[Arg]])(val metadata: CallMetadata) ext case fd: FunDefn => argss.lengthCompare(fd.params.length) < 0 case _ => false case _ => false + // `metadata` lives in a secondary constructor list, so case-class equality + // would otherwise ignore annotations such as @tailcall. + override def equals(obj: Any): Bool = obj match + case that: Call => + fun == that.fun && + argss == that.argss && + metadata == that.metadata + case _ => false + override def hashCode: Int = + (fun, argss, metadata).hashCode object Call: diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index 55657e5d87..63ae869e43 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -56,9 +56,18 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): runPass("ClassParamFlattener")(ClassParamFlattener.apply) runPass("ReflectionInstrumenter")(ReflectionInstrumenter(using summon).apply) preOptimizeHook(result) + + // * We run this pass here first, before inlining so that the @tailrec/@tailcall annotations + // * can be properly checked. runPass("TailRecOpt")(TailRecOpt().transform) + runPass("WorkerWrapper")(WorkerWrapper(symbolsToPreserve, otl, printer)) runPass("BlockSimplifier")(BlockSimplifier(symbolsToPreserve, otl, printer).apply) runPass("DeadParamElim")(otl.givenIn(DeadParamElim.apply)) + // * More tailrec opportunities might be revealed after WorkerWrapper + BlockSimplifier, + // * which might bring split curried recursive calls (such as those coming out of Deforest + EtaExpansion) + // * into proper tail positions. + runPass("TailRecOpt")(TailRecOpt().transform) + result diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs index eb9f1fbaf2..7acfe4eca2 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs @@ -610,96 +610,127 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { static [definitionMetadata] = ["class", "Int31", [null]]; }); } - static handleEffects_handleEffect_resume(id, param0, param1) { + static handleEffect_resume_handleEffects_handleEffect_resume(id, param0, param1, param2) { loopLabel: while (true) { switch (id) { case 0: - lbl: while (true) { - let nxt, scrut; - if (param0 instanceof Runtime.EffectSig.class) { - nxt = Runtime.handleEffect(param0); - scrut = param0 === nxt; - if (scrut === true) { - Runtime.curEffect = param0; - return null - } - param0 = nxt; - continue lbl; - } - return param0; + { + let param0_tmp; + param0_tmp = param0; + param0 = 1; + param1 = param0_tmp; + param2 = undefined; + id = 2; + continue loopLabel; } case 1: { - let prevHandlerFrame, scrut, handlerFrame, saved, old, scrut1, scrut2, scrut3, tmp, tmp1, tmp2, tmp3; - prevHandlerFrame = param0.contTrace; - lbl1: while (true) { - let scrut4, scrut5; - scrut4 = prevHandlerFrame.nextHandler !== null; - if (scrut4 === true) { - scrut5 = prevHandlerFrame.nextHandler.handler !== param0.handler; - if (scrut5 === true) { - prevHandlerFrame = prevHandlerFrame.nextHandler; - continue lbl1 - } - } - break; - } - scrut = prevHandlerFrame.nextHandler === null; - if (scrut === true) { - return param0 - } - handlerFrame = prevHandlerFrame.nextHandler; - saved = new Runtime.ContTrace.class(handlerFrame.next, param0.contTrace.last, handlerFrame.nextHandler, param0.contTrace.lastHandler, false); - param0.contTrace.last = handlerFrame; - param0.contTrace.lastHandler = handlerFrame; - handlerFrame.next = null; - handlerFrame.nextHandler = null; - Runtime.curEffect = null; - old = Runtime.stackDepth; - try { - tmp1 = Runtime.stackDepth + 2; - Runtime.stackDepth = tmp1; - tmp2 = Runtime.resume(param0.contTrace); - tmp3 = runtime.safeCall(param0.handlerFun(tmp2)); - tmp = tmp3; - } finally { - Runtime.stackDepth = old; - } - scrut1 = Runtime.curEffect !== null; - if (scrut1 === true) { - param0 = Runtime.curEffect; - scrut2 = saved.next !== null; - if (scrut2 === true) { - param0.contTrace.last.next = saved.next; - param0.contTrace.last = saved.last; - } - scrut3 = saved.nextHandler !== null; - if (scrut3 === true) { - param0.contTrace.lastHandler.nextHandler = saved.nextHandler; - param0.contTrace.lastHandler = saved.lastHandler; - return param0 - } - return param0; - } - return Runtime.resumeContTrace(saved, tmp); + let param1_tmp, param0_tmp; + param1_tmp = param1; + param0_tmp = param0; + param0 = 2; + param1 = param0_tmp; + param2 = param1_tmp; + id = 2; + continue loopLabel; } case 2: - { - let scrut, tmp; - scrut = param0.resumed; - if (scrut === true) { - throw runtime.safeCall(globalThis.Error("Multiple resumption")) + loopLabel1: while (true) { + switch (param0) { + case 0: + lbl: while (true) { + let nxt, scrut; + if (param1 instanceof Runtime.EffectSig.class) { + nxt = Runtime.handleEffect(param1); + scrut = param1 === nxt; + if (scrut === true) { + Runtime.curEffect = param1; + return null + } + param1 = nxt; + continue lbl; + } + return param1; + } + case 1: + { + let prevHandlerFrame, scrut, handlerFrame, saved, old, scrut1, scrut2, scrut3, tmp, tmp1, tmp2, tmp3; + prevHandlerFrame = param1.contTrace; + lbl1: while (true) { + let scrut4, scrut5; + scrut4 = prevHandlerFrame.nextHandler !== null; + if (scrut4 === true) { + scrut5 = prevHandlerFrame.nextHandler.handler !== param1.handler; + if (scrut5 === true) { + prevHandlerFrame = prevHandlerFrame.nextHandler; + continue lbl1 + } + } + break; + } + scrut = prevHandlerFrame.nextHandler === null; + if (scrut === true) { + return param1 + } + handlerFrame = prevHandlerFrame.nextHandler; + saved = new Runtime.ContTrace.class(handlerFrame.next, param1.contTrace.last, handlerFrame.nextHandler, param1.contTrace.lastHandler, false); + param1.contTrace.last = handlerFrame; + param1.contTrace.lastHandler = handlerFrame; + handlerFrame.next = null; + handlerFrame.nextHandler = null; + Runtime.curEffect = null; + old = Runtime.stackDepth; + try { + tmp1 = Runtime.stackDepth + 2; + Runtime.stackDepth = tmp1; + tmp2 = Runtime.resume(param1.contTrace); + tmp3 = runtime.safeCall(param1.handlerFun(tmp2)); + tmp = tmp3; + } finally { + Runtime.stackDepth = old; + } + scrut1 = Runtime.curEffect !== null; + if (scrut1 === true) { + param1 = Runtime.curEffect; + scrut2 = saved.next !== null; + if (scrut2 === true) { + param1.contTrace.last.next = saved.next; + param1.contTrace.last = saved.last; + } + scrut3 = saved.nextHandler !== null; + if (scrut3 === true) { + param1.contTrace.lastHandler.nextHandler = saved.nextHandler; + param1.contTrace.lastHandler = saved.lastHandler; + return param1 + } + return param1; + } + return Runtime.resumeContTrace(saved, tmp); + } + case 2: + { + let scrut, tmp; + scrut = param1.resumed; + if (scrut === true) { + throw runtime.safeCall(globalThis.Error("Multiple resumption")) + } + param1.resumed = true; + tmp = Runtime.resumeContTrace(param1, param2); + param1 = tmp; + param0 = 0; + continue loopLabel1; + } } - param0.resumed = true; - tmp = Runtime.resumeContTrace(param0, param1); - param0 = tmp; - id = 0; - continue loopLabel; + break; } + break; } break; } } + static handleEffects_handleEffect_resume(id, param0, param1) { + return Runtime.handleEffect_resume_handleEffects_handleEffect_resume(2, id, param0, param1) + } static get unreachable() { throw runtime.safeCall(globalThis.Error("unreachable")); } @@ -1080,11 +1111,11 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { return Runtime.handleEffects_handleEffect_resume(0, cur, undefined) } static handleEffect(cur) { - return Runtime.handleEffects_handleEffect_resume(1, cur, undefined) + return Runtime.handleEffect_resume_handleEffects_handleEffect_resume(0, cur, undefined, undefined) } static resume(contTrace) { return (value) => { - return Runtime.handleEffects_handleEffect_resume(2, contTrace, value) + return Runtime.handleEffect_resume_handleEffects_handleEffect_resume(1, contTrace, value, undefined) } } static resumeContTrace(contTrace, value) { diff --git a/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls b/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls index c805ac207b..1b6f31a044 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls @@ -87,13 +87,18 @@ modSome(Some(17), 3) //│ let modSome_12$x_Some$worker; //│ @affine(1) @private //│ define modSome_12$x_Some$worker as fun modSome_12$x_Some$worker⁰(Some_x, fv_m, fv_scrut, fv_tmp) { -//│ set fv_scrut = >=⁰(Some_x, fv_m); -//│ match fv_scrut -//│ true => -//│ set fv_tmp = -⁰(Some_x, fv_m); -//│ return modSome_12$x_Some$worker⁰(fv_tmp, fv_m, undefined, undefined) -//│ else -//│ return Some⁰(Some_x) +//│ loop loopLabel: +//│ set fv_scrut = >=⁰(Some_x, fv_m); +//│ match fv_scrut +//│ true => +//│ set fv_tmp = -⁰(Some_x, fv_m); +//│ set Some_x = fv_tmp; +//│ set fv_scrut = undefined; +//│ set fv_tmp = undefined; +//│ continue loopLabel +//│ else +//│ return Some⁰(Some_x) +//│ end //│ end //│ }; //│ define modSome⁰ as fun modSome¹(x, m) { diff --git a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls index 137bd7e07b..862635409c 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls @@ -36,16 +36,20 @@ sum(enum(5), 0) //│ let enum_13$enum$worker; //│ @private //│ define enum_13$enum$worker as fun enum_13$enum$worker⁰(x, eta$0$1) { -//│ let scrut, tmp; -//│ set scrut = <⁰(x, 0); -//│ match scrut -//│ true => -//│ return eta$0$1 -//│ else -//│ let fv_tmp; -//│ set tmp = -⁰(x, 1); -//│ set fv_tmp = +⁰(x, eta$0$1); -//│ return enum_13$enum$worker⁰(tmp, fv_tmp) +//│ loop loopLabel: +//│ let scrut, tmp; +//│ set scrut = <⁰(x, 0); +//│ match scrut +//│ true => +//│ return eta$0$1 +//│ else +//│ let fv_tmp; +//│ set tmp = -⁰(x, 1); +//│ set fv_tmp = +⁰(x, eta$0$1); +//│ set x = tmp; +//│ set eta$0$1 = fv_tmp; +//│ continue loopLabel +//│ end //│ end //│ }; //│ define enum⁰ as fun enum¹(x) { @@ -108,26 +112,39 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ eta-expansion > flatten_31$flatten: [1, 6] //│ eta-expansion > <<< eta-expansion targets shapes <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, flatten_31$flatten, flatten_31_append_19$append$worker, flatten_31$flatten$worker; +//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, flatten_31$flatten, flatten_31_append_19$append$worker, flatten_31$flatten$worker, flatten_31$flatten_flatten_31$flatten$worker; +//│ define flatten_31$flatten_flatten_31$flatten$worker as fun flatten_31$flatten_flatten_31$flatten$worker⁰(id, param0) { +//│ loop loopLabel: +//│ match id +//│ 0 => +//│ set id = 1; +//│ continue loopLabel +//│ 1 => +//│ let arg$Cons$0$, arg$Cons$1$; +//│ match param0 +//│ Nil⁰ => +//│ return 0 +//│ Cons¹ => +//│ let ys; +//│ set arg$Cons$0$ = param0.h⁰; +//│ set arg$Cons$1$ = param0.t⁰; +//│ set ys = flatten_31$flatten⁰(arg$Cons$1$); +//│ return flatten_31_append_19$append$worker⁰(arg$Cons$0$, ys) +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end +//│ else +//│ end +//│ end +//│ end +//│ }; //│ @inline @private //│ define flatten_31$flatten as fun flatten_31$flatten⁰(xss)() { -//│ return flatten_31$flatten$worker⁰(xss) +//│ return flatten_31$flatten_flatten_31$flatten$worker⁰(0, xss) //│ }; //│ @private //│ define flatten_31$flatten$worker as fun flatten_31$flatten$worker⁰(xss) { -//│ let arg$Cons$0$, arg$Cons$1$; -//│ match xss -//│ Nil⁰ => -//│ return 0 -//│ Cons¹ => -//│ let ys; -//│ set arg$Cons$0$ = xss.h⁰; -//│ set arg$Cons$1$ = xss.t⁰; -//│ set ys = flatten_31$flatten⁰(arg$Cons$1$); -//│ return flatten_31_append_19$append$worker⁰(arg$Cons$0$, ys) -//│ else -//│ throw new globalThis⁰.Error⁰("match error") -//│ end +//│ return flatten_31$flatten_flatten_31$flatten$worker⁰(1, xss) //│ }; //│ @private //│ define flatten_31_append_19$append$worker as fun flatten_31_append_19$append$worker⁰(xs, ys) { diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls b/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls index ddac47cf5d..d9f0d6bfd7 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls @@ -23,6 +23,11 @@ class A @tailrec fun f = g fun g = f +//│ FAILURE: Unexpected warning +//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.24: fun f = g +//│ ╙── ^ :w @tailcall @@ -38,7 +43,7 @@ fun test = @tailcall f //│ ╔══[WARNING] This annotation has no effect. //│ ╟── This annotation is not supported on reference terms. -//│ ║ l.38: @tailcall f +//│ ║ l.43: @tailcall f //│ ╙── ^ //│ f = 0 @@ -48,7 +53,10 @@ class A with fun f() = g() fun g() = f() //│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. -//│ ║ l.48: fun f() = g() +//│ ║ l.53: fun f() = g() +//│ ╙── ^ +//│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. +//│ ║ l.53: fun f() = g() //│ ╙── ^ :w @@ -63,7 +71,10 @@ module A with @tailrec fun f = 2 //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.64: fun f = 2 +//│ ║ l.72: fun f = 2 +//│ ╙── ^ +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.72: fun f = 2 //│ ╙── ^ :w @@ -87,5 +98,5 @@ fun test = @tailcall 1 + 2 //│ ╔══[WARNING] This annotation has no effect. //│ ╟── The @tailcall annotation has no effect on calls to built-in symbols. -//│ ║ l.87: @tailcall 1 + 2 +//│ ║ l.98: @tailcall 1 + 2 //│ ╙── ^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls index 6d59725bbf..ffbc409ebc 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls @@ -9,12 +9,15 @@ fun f(x) = //│ ╔══[COMPILATION ERROR] This call is not in tail position. //│ ║ l.7: @tailcall f(x) //│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] This call is not in tail position. +//│ ║ l.7: @tailcall f(x) +//│ ╙── ^^^^ :e fun f(x) = 2 fun g(x) = @tailcall f(x) //│ ╔══[COMPILATION ERROR] This call is not optimized as it does not directly recurse through its parent function. -//│ ║ l.15: fun g(x) = @tailcall f(x) +//│ ║ l.18: fun g(x) = @tailcall f(x) //│ ╙── ^^^^ :e @@ -22,11 +25,16 @@ fun g(x) = @tailcall f(x) g(x) fun g(x) = f(x); f(x) //│ ╔══[COMPILATION ERROR] This function is not tail recursive. -//│ ║ l.21: @tailrec fun f(x) = +//│ ║ l.24: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. -//│ ║ l.23: fun g(x) = f(x); f(x) +//│ ║ l.26: fun g(x) = f(x); f(x) //│ ╙── ^^^^ +//│ FAILURE: Unexpected warning +//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.24: @tailrec fun f(x) = +//│ ╙── ^ :e @tailrec fun f(x) = @@ -37,10 +45,16 @@ fun g(x) = fun h(x) = g(x) //│ ╔══[COMPILATION ERROR] This function is not tail recursive. -//│ ║ l.32: @tailrec fun f(x) = +//│ ║ l.40: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. -//│ ║ l.35: f(x) +//│ ║ l.43: f(x) +//│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] This function is not tail recursive. +//│ ║ l.40: @tailrec fun f(x) = +//│ ║ ^ +//│ ╟── It could self-recurse through this call, which is not a tail call. +//│ ║ l.43: f(x) //│ ╙── ^^^^ :e @@ -52,11 +66,16 @@ fun g(x) = fun h(x) = f(x) //│ ╔══[COMPILATION ERROR] This function is not tail recursive. -//│ ║ l.47: @tailrec fun f(x) = +//│ ║ l.61: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. -//│ ║ l.50: h(x) +//│ ║ l.64: h(x) //│ ╙── ^^^^ +//│ FAILURE: Unexpected warning +//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.61: @tailrec fun f(x) = +//│ ╙── ^ :e module A with @@ -65,14 +84,20 @@ module A with @tailcall f(x - 1) @tailcall f(x - 1) //│ ╔══[COMPILATION ERROR] This call is not in tail position. -//│ ║ l.65: @tailcall f(x - 1) +//│ ║ l.84: @tailcall f(x - 1) +//│ ╙── ^^^^^^^^ +//│ ╔══[COMPILATION ERROR] This call is not in tail position. +//│ ║ l.84: @tailcall f(x - 1) //│ ╙── ^^^^^^^^ :w @tailrec fun f = 2 //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.73: fun f = 2 +//│ ║ l.95: fun f = 2 +//│ ╙── ^ +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.95: fun f = 2 //│ ╙── ^ :w @@ -80,8 +105,11 @@ module A with @tailrec fun f() = 2 //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.81: fun f() = 2 -//│ ╙── ^ +//│ ║ l.106: fun f() = 2 +//│ ╙── ^ +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.106: fun f() = 2 +//│ ╙── ^ :fixme // TODO: support @tailrec @@ -89,8 +117,11 @@ fun foo() = Foo.bar() module Foo with fun bar() = foo() //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.88: fun foo() = Foo.bar() -//│ ╙── ^^^ +//│ ║ l.116: fun foo() = Foo.bar() +//│ ╙── ^^^ +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.116: fun foo() = Foo.bar() +//│ ╙── ^^^ :e fun f(x) = @@ -98,8 +129,11 @@ fun f(x) = fun g() = g() g() //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.98: fun g() = g() -//│ ╙── ^ +//│ ║ l.129: fun g() = g() +//│ ╙── ^ +//│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. +//│ ║ l.129: fun g() = g() +//│ ╙── ^ :e fun scan(idx, acc) = @@ -107,7 +141,10 @@ fun scan(idx, acc) = fun go(idx, tok) = scan(idx, tok) go(idx, acc) //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.107: fun go(idx, tok) = scan(idx, tok) +//│ ║ l.141: fun go(idx, tok) = scan(idx, tok) +//│ ╙── ^^ +//│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. +//│ ║ l.141: fun go(idx, tok) = scan(idx, tok) //│ ╙── ^^ :e @@ -118,10 +155,16 @@ fun scan(idx, acc) = fun go(idx, tok) = scan(idx, tok) go(idx, acc) //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.116: fun scan(idx, acc) = +//│ ║ l.153: fun scan(idx, acc) = //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.118: fun go(idx, tok) = scan(idx, tok) +//│ ║ l.155: fun go(idx, tok) = scan(idx, tok) +//│ ╙── ^^ +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.153: fun scan(idx, acc) = +//│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. +//│ ║ l.155: fun go(idx, tok) = scan(idx, tok) //│ ╙── ^^ :e @@ -129,21 +172,30 @@ fun scan(idx, acc) = @config(tailRecOpt: false) fun foo() = foo() //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.130: fun foo() = foo() +//│ ║ l.173: fun foo() = foo() +//│ ╙── ^^^ +//│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. +//│ ║ l.173: fun foo() = foo() //│ ╙── ^^^ :e fun f = @tailcall id(2) //│ ╔══[COMPILATION ERROR] Only functions in this compilation unit may be marked @tailcall. -//│ ║ l.137: @tailcall id(2) +//│ ║ l.183: @tailcall id(2) +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Only functions in this compilation unit may be marked @tailcall. +//│ ║ l.183: @tailcall id(2) //│ ╙── ^^^^^ :e fun f(x)(y) = @tailcall f(x) //│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. -//│ ║ l.144: @tailcall f(x) +//│ ║ l.193: @tailcall f(x) +//│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. +//│ ║ l.193: @tailcall f(x) //│ ╙── ^^^^ :e @@ -152,8 +204,14 @@ fun f(x)() = fun g(x)(y) = @tailcall f(x) //│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. -//│ ║ l.153: @tailcall f(x) +//│ ║ l.205: @tailcall f(x) //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This call is not optimized as it does not directly recurse through its parent function. -//│ ║ l.151: @tailcall g(x)(0) +//│ ║ l.203: @tailcall g(x)(0) //│ ╙── ^^^^^^^ +//│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. +//│ ║ l.205: @tailcall f(x) +//│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. +//│ ║ l.205: @tailcall f(x) +//│ ╙── ^^^^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls index 57a7473d91..f4e7a7997b 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls @@ -18,6 +18,11 @@ fun loop(x)(y) = if x == 0 then y else loop(x - 1)(y + x) loop(1000)(0) +//│ FAILURE: Unexpected warning +//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.17: fun loop(x)(y) = +//│ ╙── ^^^^ //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let loop$tailrec; //│ @private @@ -298,6 +303,11 @@ fun loop(x)(y) = if x == 0 then y else loop(x - 1)(y + x) fun f = loop(1000)(0) +//│ FAILURE: Unexpected warning +//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.302: fun loop(x)(y) = +//│ ╙── ^^^^ //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ @tailrec @inline //│ define loop² as fun loop³(x)(y) { diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls index 6a86654700..9da1cdc133 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls @@ -5,6 +5,11 @@ open annotations :sjs @tailrec fun f = f +//│ FAILURE: Unexpected warning +//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.7: fun f = f +//│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let f; f = function f() { loopLabel: while (true) { continue loopLabel; } }; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -14,15 +19,28 @@ module Foo with @tailrec fun bar = bar //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.15: fun bar = bar +//│ ║ l.20: fun bar = bar +//│ ╙── ^^^ +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.20: fun bar = bar //│ ╙── ^^^ @tailrec fun f() = f() +//│ FAILURE: Unexpected warning +//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.29: fun f() = f() +//│ ╙── ^ module Foo with @tailrec fun bar() = bar() +//│ FAILURE: Unexpected warning +//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.38: fun bar() = bar() +//│ ╙── ^^^ fun f(x) = f(x + 1) @@ -52,7 +70,10 @@ module A with module B with fun g(x) = A.f(x * 2) //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.51: fun f(x) = B.g(x + 1) +//│ ║ l.69: fun f(x) = B.g(x + 1) +//│ ╙── ^ +//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ║ l.69: fun f(x) = B.g(x + 1) //│ ╙── ^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls index 6e000968cf..cf12b5f77c 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls @@ -210,6 +210,9 @@ fun f(x) = //│ ╔══[COMPILATION ERROR] This call is not in tail position. //│ ║ l.207: @tailcall f(x) //│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] This call is not in tail position. +//│ ║ l.207: @tailcall f(x) +//│ ╙── ^^^^ :lift fun f(x) = @@ -292,10 +295,16 @@ module A with @tailcall g(x - 1) A.f(10000) //│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.291: fun g(x) = if x < 0 then 0 else @tailcall f(x) +//│ ║ l.294: fun g(x) = if x < 0 then 0 else @tailcall f(x) +//│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. +//│ ║ l.295: @tailcall g(x - 1) +//│ ╙── ^^^^^^^^ +//│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. +//│ ║ l.294: fun g(x) = if x < 0 then 0 else @tailcall f(x) //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.292: @tailcall g(x - 1) +//│ ║ l.295: @tailcall g(x - 1) //│ ╙── ^^^^^^^^ //│ ═══[RUNTIME ERROR] RangeError: Maximum call stack size exceeded @@ -322,12 +331,21 @@ class A with fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) (new A).f(10) //│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.321: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ║ l.330: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ╙── ^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. +//│ ║ l.330: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ╙── ^ +//│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. +//│ ║ l.331: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) +//│ ╙── ^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. +//│ ║ l.330: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) //│ ╙── ^^^^^^^^ //│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. -//│ ║ l.321: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ║ l.330: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.322: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) +//│ ║ l.331: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) //│ ╙── ^^^^^^^^ //│ = 0 From d8598fbf4dd9978b9bbc5228975a1dd551bed5a9 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 10:13:18 +0800 Subject: [PATCH 03/15] Control TailRecOpt annotation checks --- .../hkmc2/codegen/CompilationPipeline.scala | 4 +- .../main/scala/hkmc2/codegen/TailRecOpt.scala | 51 ++++----- .../src/test/mlscript/tailrec/Annots.mls | 19 +--- .../src/test/mlscript/tailrec/Errors.mls | 104 ++++-------------- .../test/mlscript/tailrec/MultiArgLists.mls | 76 +++++++++++-- .../src/test/mlscript/tailrec/Simple.mls | 25 +---- .../src/test/mlscript/tailrec/TailRecOpt.mls | 28 +---- 7 files changed, 128 insertions(+), 179 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index 63ae869e43..47d8119922 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -59,7 +59,7 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): // * We run this pass here first, before inlining so that the @tailrec/@tailcall annotations // * can be properly checked. - runPass("TailRecOpt")(TailRecOpt().transform) + runPass("TailRecOpt")(TailRecOpt(true).transform) runPass("WorkerWrapper")(WorkerWrapper(symbolsToPreserve, otl, printer)) runPass("BlockSimplifier")(BlockSimplifier(symbolsToPreserve, otl, printer).apply) @@ -68,6 +68,6 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): // * More tailrec opportunities might be revealed after WorkerWrapper + BlockSimplifier, // * which might bring split curried recursive calls (such as those coming out of Deforest + EtaExpansion) // * into proper tail positions. - runPass("TailRecOpt")(TailRecOpt().transform) + runPass("TailRecOpt")(TailRecOpt(false).transform) result diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index 433c886b02..ceb6386d46 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -68,7 +68,7 @@ connected component are tail calls. */ // This optimization assumes the lifter has been run. -class TailRecOpt(using State, TL, Raise): +class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): type AccessMap = Map[ScopedInfo, AccessInfo] @@ -116,20 +116,20 @@ class TailRecOpt(using State, TL, Raise): case TailCallShape(r, c) => getFun(r) match case Some(value) => if c.argss.size != value.params.size then - if c.metadata.explicitTailCall then + if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only fully applied calls may be marked @tailcall." -> c.toLoc :: Nil)) else edges ::= CallEdge.TailCall(f.dSym, r)(c) case None => - if c.metadata.explicitTailCall then + if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only functions in this compilation unit may be marked @tailcall." -> c.toLoc :: Nil)) case Return(c: Call) => - if c.metadata.explicitTailCall then + if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only direct calls in tail position may be marked @tailcall." -> c.toLoc :: Nil)) case _ => super.applyBlock(b) override def applyResult(r: Result): Unit = r match case c: Call => - if c.metadata.explicitTailCall then + if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"This call is not in tail position." -> c.toLoc :: Nil)) c match case CallToFun(r) => edges ::= CallEdge.NormalCall(f.dSym, r)(c) @@ -150,7 +150,7 @@ class TailRecOpt(using State, TL, Raise): val cg = buildCallGraph(fs).filter: c => val cond = defnSyms.contains(c.f1) && defnSyms.contains(c.f2) c.match - case c: CallEdge.TailCall if c.call.metadata.explicitTailCall && !cond => + case c: CallEdge.TailCall if checkAnnotations && c.call.metadata.explicitTailCall && !cond => raise(ErrorReport( msg"This tail call exits the current scope and is not optimized." -> c.call.toLoc :: Nil)) case _ => @@ -167,7 +167,7 @@ class TailRecOpt(using State, TL, Raise): .groupBy: c => val s1 = sccMap(c.f1) val s2 = sccMap(c.f2) - if s1 =/= s2 && c.call.metadata.explicitTailCall then + if checkAnnotations && s1 =/= s2 && c.call.metadata.explicitTailCall then raise(ErrorReport( msg"This call is not optimized as it does not directly recurse through its parent function." -> c.call.toLoc :: Nil)) -1 @@ -248,12 +248,12 @@ class TailRecOpt(using State, TL, Raise): val nonTailCalls = nonTailCallsLs.toMap if nonTailCallsLs.sizeCompare(calls) === 0 then - for f <- funs if f.tailRec do + for f <- funs if checkAnnotations && f.tailRec do raise(WarningReport(msg"This function does not directly self-recurse, but is marked @tailrec." -> f.dSym.toLoc :: Nil)) return (N, funs) if !nonTailCalls.isEmpty then - for f <- funs if f.tailRec do + for f <- funs if checkAnnotations && f.tailRec do val reportLoc = nonTailCalls.get(f.dSym) match // always display a call to f, if possible case Some(value) => value.toLoc @@ -570,10 +570,10 @@ class TailRecOpt(using State, TL, Raise): new BlockTraverserShallow(): for f <- c.methods do applyBlock(f.body) - if f.tailRec then + if checkAnnotations && f.tailRec then raise(ErrorReport(msg"Class methods may not yet be marked @tailrec." -> f.dSym.toLoc :: Nil)) override def applyResult(r: Result): Unit = r match - case c: Call if c.metadata.explicitTailCall => + case c: Call if checkAnnotations && c.metadata.explicitTailCall => raise(ErrorReport(msg"Calls from class methods cannot yet be marked @tailcall." -> c.toLoc :: Nil)) case _ => super.applyResult(r) @@ -659,20 +659,21 @@ class TailRecOpt(using State, TL, Raise): optFNew.foldLeft(transformer.applyBlock(b)): case (acc, f) => Define(f, acc)) - // Report @tailrec on functions that weren't processed by the optimization above, - // e.g. nested functions or functions with @config(tailRecOpt: false). - // Class/module methods are handled separately by optClasses and are skipped here. - val tailRecFunSyms = tailRecFuns.map(_.dSym).toSet - new BlockTraverser: - override def applyFunDefn(fun: FunDefn): Unit = - if fun.tailRec && !tailRecFunSyms.contains(fun.dSym) then - raise(ErrorReport( - msg"This @tailrec function was not processed by the tail-call optimizer." -> fun.dSym.toLoc :: Nil)) - super.applyFunDefn(fun) - override def applyDefn(defn: Defn): Unit = defn match - case _: ClsLikeDefn => () - case _ => super.applyDefn(defn) - .applyBlock(result) + if checkAnnotations then + // Report @tailrec on functions that weren't processed by the optimization above, + // e.g. nested functions or functions with @config(tailRecOpt: false). + // Class/module methods are handled separately by optClasses and are skipped here. + val tailRecFunSyms = tailRecFuns.map(_.dSym).toSet + new BlockTraverser: + override def applyFunDefn(fun: FunDefn): Unit = + if fun.tailRec && !tailRecFunSyms.contains(fun.dSym) then + raise(ErrorReport( + msg"This @tailrec function was not processed by the tail-call optimizer." -> fun.dSym.toLoc :: Nil)) + super.applyFunDefn(fun) + override def applyDefn(defn: Defn): Unit = defn match + case _: ClsLikeDefn => () + case _ => super.applyDefn(defn) + .applyBlock(result) if result is b then prog diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls b/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls index d9f0d6bfd7..ddac47cf5d 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls @@ -23,11 +23,6 @@ class A @tailrec fun f = g fun g = f -//│ FAILURE: Unexpected warning -//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.24: fun f = g -//│ ╙── ^ :w @tailcall @@ -43,7 +38,7 @@ fun test = @tailcall f //│ ╔══[WARNING] This annotation has no effect. //│ ╟── This annotation is not supported on reference terms. -//│ ║ l.43: @tailcall f +//│ ║ l.38: @tailcall f //│ ╙── ^ //│ f = 0 @@ -53,10 +48,7 @@ class A with fun f() = g() fun g() = f() //│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. -//│ ║ l.53: fun f() = g() -//│ ╙── ^ -//│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. -//│ ║ l.53: fun f() = g() +//│ ║ l.48: fun f() = g() //│ ╙── ^ :w @@ -71,10 +63,7 @@ module A with @tailrec fun f = 2 //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.72: fun f = 2 -//│ ╙── ^ -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.72: fun f = 2 +//│ ║ l.64: fun f = 2 //│ ╙── ^ :w @@ -98,5 +87,5 @@ fun test = @tailcall 1 + 2 //│ ╔══[WARNING] This annotation has no effect. //│ ╟── The @tailcall annotation has no effect on calls to built-in symbols. -//│ ║ l.98: @tailcall 1 + 2 +//│ ║ l.87: @tailcall 1 + 2 //│ ╙── ^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls index ffbc409ebc..6d59725bbf 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls @@ -9,15 +9,12 @@ fun f(x) = //│ ╔══[COMPILATION ERROR] This call is not in tail position. //│ ║ l.7: @tailcall f(x) //│ ╙── ^^^^ -//│ ╔══[COMPILATION ERROR] This call is not in tail position. -//│ ║ l.7: @tailcall f(x) -//│ ╙── ^^^^ :e fun f(x) = 2 fun g(x) = @tailcall f(x) //│ ╔══[COMPILATION ERROR] This call is not optimized as it does not directly recurse through its parent function. -//│ ║ l.18: fun g(x) = @tailcall f(x) +//│ ║ l.15: fun g(x) = @tailcall f(x) //│ ╙── ^^^^ :e @@ -25,16 +22,11 @@ fun g(x) = @tailcall f(x) g(x) fun g(x) = f(x); f(x) //│ ╔══[COMPILATION ERROR] This function is not tail recursive. -//│ ║ l.24: @tailrec fun f(x) = +//│ ║ l.21: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. -//│ ║ l.26: fun g(x) = f(x); f(x) +//│ ║ l.23: fun g(x) = f(x); f(x) //│ ╙── ^^^^ -//│ FAILURE: Unexpected warning -//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.24: @tailrec fun f(x) = -//│ ╙── ^ :e @tailrec fun f(x) = @@ -45,16 +37,10 @@ fun g(x) = fun h(x) = g(x) //│ ╔══[COMPILATION ERROR] This function is not tail recursive. -//│ ║ l.40: @tailrec fun f(x) = +//│ ║ l.32: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. -//│ ║ l.43: f(x) -//│ ╙── ^^^^ -//│ ╔══[COMPILATION ERROR] This function is not tail recursive. -//│ ║ l.40: @tailrec fun f(x) = -//│ ║ ^ -//│ ╟── It could self-recurse through this call, which is not a tail call. -//│ ║ l.43: f(x) +//│ ║ l.35: f(x) //│ ╙── ^^^^ :e @@ -66,16 +52,11 @@ fun g(x) = fun h(x) = f(x) //│ ╔══[COMPILATION ERROR] This function is not tail recursive. -//│ ║ l.61: @tailrec fun f(x) = +//│ ║ l.47: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. -//│ ║ l.64: h(x) +//│ ║ l.50: h(x) //│ ╙── ^^^^ -//│ FAILURE: Unexpected warning -//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.61: @tailrec fun f(x) = -//│ ╙── ^ :e module A with @@ -84,20 +65,14 @@ module A with @tailcall f(x - 1) @tailcall f(x - 1) //│ ╔══[COMPILATION ERROR] This call is not in tail position. -//│ ║ l.84: @tailcall f(x - 1) -//│ ╙── ^^^^^^^^ -//│ ╔══[COMPILATION ERROR] This call is not in tail position. -//│ ║ l.84: @tailcall f(x - 1) +//│ ║ l.65: @tailcall f(x - 1) //│ ╙── ^^^^^^^^ :w @tailrec fun f = 2 //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.95: fun f = 2 -//│ ╙── ^ -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.95: fun f = 2 +//│ ║ l.73: fun f = 2 //│ ╙── ^ :w @@ -105,11 +80,8 @@ module A with @tailrec fun f() = 2 //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.106: fun f() = 2 -//│ ╙── ^ -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.106: fun f() = 2 -//│ ╙── ^ +//│ ║ l.81: fun f() = 2 +//│ ╙── ^ :fixme // TODO: support @tailrec @@ -117,11 +89,8 @@ fun foo() = Foo.bar() module Foo with fun bar() = foo() //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.116: fun foo() = Foo.bar() -//│ ╙── ^^^ -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.116: fun foo() = Foo.bar() -//│ ╙── ^^^ +//│ ║ l.88: fun foo() = Foo.bar() +//│ ╙── ^^^ :e fun f(x) = @@ -129,11 +98,8 @@ fun f(x) = fun g() = g() g() //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.129: fun g() = g() -//│ ╙── ^ -//│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.129: fun g() = g() -//│ ╙── ^ +//│ ║ l.98: fun g() = g() +//│ ╙── ^ :e fun scan(idx, acc) = @@ -141,10 +107,7 @@ fun scan(idx, acc) = fun go(idx, tok) = scan(idx, tok) go(idx, acc) //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.141: fun go(idx, tok) = scan(idx, tok) -//│ ╙── ^^ -//│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.141: fun go(idx, tok) = scan(idx, tok) +//│ ║ l.107: fun go(idx, tok) = scan(idx, tok) //│ ╙── ^^ :e @@ -155,16 +118,10 @@ fun scan(idx, acc) = fun go(idx, tok) = scan(idx, tok) go(idx, acc) //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.153: fun scan(idx, acc) = +//│ ║ l.116: fun scan(idx, acc) = //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.155: fun go(idx, tok) = scan(idx, tok) -//│ ╙── ^^ -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.153: fun scan(idx, acc) = -//│ ╙── ^^^^ -//│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.155: fun go(idx, tok) = scan(idx, tok) +//│ ║ l.118: fun go(idx, tok) = scan(idx, tok) //│ ╙── ^^ :e @@ -172,30 +129,21 @@ fun scan(idx, acc) = @config(tailRecOpt: false) fun foo() = foo() //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.173: fun foo() = foo() -//│ ╙── ^^^ -//│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. -//│ ║ l.173: fun foo() = foo() +//│ ║ l.130: fun foo() = foo() //│ ╙── ^^^ :e fun f = @tailcall id(2) //│ ╔══[COMPILATION ERROR] Only functions in this compilation unit may be marked @tailcall. -//│ ║ l.183: @tailcall id(2) -//│ ╙── ^^^^^ -//│ ╔══[COMPILATION ERROR] Only functions in this compilation unit may be marked @tailcall. -//│ ║ l.183: @tailcall id(2) +//│ ║ l.137: @tailcall id(2) //│ ╙── ^^^^^ :e fun f(x)(y) = @tailcall f(x) //│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. -//│ ║ l.193: @tailcall f(x) -//│ ╙── ^^^^ -//│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. -//│ ║ l.193: @tailcall f(x) +//│ ║ l.144: @tailcall f(x) //│ ╙── ^^^^ :e @@ -204,14 +152,8 @@ fun f(x)() = fun g(x)(y) = @tailcall f(x) //│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. -//│ ║ l.205: @tailcall f(x) +//│ ║ l.153: @tailcall f(x) //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This call is not optimized as it does not directly recurse through its parent function. -//│ ║ l.203: @tailcall g(x)(0) +//│ ║ l.151: @tailcall g(x)(0) //│ ╙── ^^^^^^^ -//│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. -//│ ║ l.205: @tailcall f(x) -//│ ╙── ^^^^ -//│ ╔══[COMPILATION ERROR] Only fully applied calls may be marked @tailcall. -//│ ║ l.205: @tailcall f(x) -//│ ╙── ^^^^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls index f4e7a7997b..e1d42b76b8 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls @@ -18,11 +18,6 @@ fun loop(x)(y) = if x == 0 then y else loop(x - 1)(y + x) loop(1000)(0) -//│ FAILURE: Unexpected warning -//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.17: fun loop(x)(y) = -//│ ╙── ^^^^ //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let loop$tailrec; //│ @private @@ -155,6 +150,72 @@ even(9)(0) //│ = [false, 9] +// Minimal repro: the second TailRecOpt pass can reprocess the loop generated by +// the first pass together with the curried wrapper, producing a larger loop. +:soir +let sink = mut [] +fun handleEffects(x) = handleEffect(x) +fun handleEffect(x) = + sink.push(resume(x)) + handleEffects(x) +fun resume(x)(y) = handleEffects(x) +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ let handleEffects_handleEffect_resume, resume_handleEffects_handleEffect_resume; +//│ define resume_handleEffects_handleEffect_resume as fun resume_handleEffects_handleEffect_resume⁰(id, param0, param1, param2) { +//│ loop loopLabel: +//│ match id +//│ 0 => +//│ let param1_tmp, param0_tmp; +//│ set param1_tmp = param1; +//│ set param0_tmp = param0; +//│ set param0 = 2; +//│ set param1 = param0_tmp; +//│ set param2 = param1_tmp; +//│ set id = 1; +//│ continue loopLabel +//│ 1 => +//│ loop loopLabel1: +//│ match param0 +//│ 0 => +//│ set param0 = 1; +//│ continue loopLabel1 +//│ 1 => +//│ let tmp; +//│ set tmp = resume⁰(param1); +//│ do sink⁰.push﹖(tmp); +//│ set param0 = 0; +//│ continue loopLabel1 +//│ 2 => +//│ set param0 = 0; +//│ continue loopLabel1 +//│ else +//│ end +//│ end +//│ end +//│ else +//│ end +//│ end +//│ end +//│ }; +//│ define handleEffects_handleEffect_resume as fun handleEffects_handleEffect_resume⁰(id, param0, param1) { +//│ return resume_handleEffects_handleEffect_resume⁰(1, id, param0, param1) +//│ }; +//│ define handleEffects⁰ as fun handleEffects¹(x) { +//│ return handleEffects_handleEffect_resume⁰(0, x, undefined) +//│ }; +//│ define handleEffect⁰ as fun handleEffect¹(x) { +//│ return handleEffects_handleEffect_resume⁰(1, x, undefined) +//│ }; +//│ @inline +//│ define resume¹ as fun resume⁰(x)(y) { +//│ return resume_handleEffects_handleEffect_resume⁰(0, x, y, undefined) +//│ }; +//│ set sink⁰ = mut []; +//│ end +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ sink = [] + + // Spreads // This is the same as a test in `TailRecOpt.mls` but with some parameters separated and changed. @@ -303,11 +364,6 @@ fun loop(x)(y) = if x == 0 then y else loop(x - 1)(y + x) fun f = loop(1000)(0) -//│ FAILURE: Unexpected warning -//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.302: fun loop(x)(y) = -//│ ╙── ^^^^ //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ @tailrec @inline //│ define loop² as fun loop³(x)(y) { diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls index 9da1cdc133..6a86654700 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls @@ -5,11 +5,6 @@ open annotations :sjs @tailrec fun f = f -//│ FAILURE: Unexpected warning -//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.7: fun f = f -//│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let f; f = function f() { loopLabel: while (true) { continue loopLabel; } }; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -19,28 +14,15 @@ module Foo with @tailrec fun bar = bar //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.20: fun bar = bar -//│ ╙── ^^^ -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.20: fun bar = bar +//│ ║ l.15: fun bar = bar //│ ╙── ^^^ @tailrec fun f() = f() -//│ FAILURE: Unexpected warning -//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.29: fun f() = f() -//│ ╙── ^ module Foo with @tailrec fun bar() = bar() -//│ FAILURE: Unexpected warning -//│ FAILURE LOCATION: optScc (TailRecOpt.scala:252) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.38: fun bar() = bar() -//│ ╙── ^^^ fun f(x) = f(x + 1) @@ -70,10 +52,7 @@ module A with module B with fun g(x) = A.f(x * 2) //│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.69: fun f(x) = B.g(x + 1) -//│ ╙── ^ -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.69: fun f(x) = B.g(x + 1) +//│ ║ l.51: fun f(x) = B.g(x + 1) //│ ╙── ^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls index cf12b5f77c..6e000968cf 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls @@ -210,9 +210,6 @@ fun f(x) = //│ ╔══[COMPILATION ERROR] This call is not in tail position. //│ ║ l.207: @tailcall f(x) //│ ╙── ^^^^ -//│ ╔══[COMPILATION ERROR] This call is not in tail position. -//│ ║ l.207: @tailcall f(x) -//│ ╙── ^^^^ :lift fun f(x) = @@ -295,16 +292,10 @@ module A with @tailcall g(x - 1) A.f(10000) //│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.294: fun g(x) = if x < 0 then 0 else @tailcall f(x) -//│ ╙── ^^^^ -//│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.295: @tailcall g(x - 1) -//│ ╙── ^^^^^^^^ -//│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.294: fun g(x) = if x < 0 then 0 else @tailcall f(x) +//│ ║ l.291: fun g(x) = if x < 0 then 0 else @tailcall f(x) //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.295: @tailcall g(x - 1) +//│ ║ l.292: @tailcall g(x - 1) //│ ╙── ^^^^^^^^ //│ ═══[RUNTIME ERROR] RangeError: Maximum call stack size exceeded @@ -331,21 +322,12 @@ class A with fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) (new A).f(10) //│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.330: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) -//│ ╙── ^^^^^^^^ -//│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. -//│ ║ l.330: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) -//│ ╙── ^ -//│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.331: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) -//│ ╙── ^^^^^^^^ -//│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.330: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ║ l.321: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) //│ ╙── ^^^^^^^^ //│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. -//│ ║ l.330: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ║ l.321: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.331: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) +//│ ║ l.322: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) //│ ╙── ^^^^^^^^ //│ = 0 From 670a633de9523c067076969591716393c1bfeb67 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 10:40:56 +0800 Subject: [PATCH 04/15] Ignore under-applied calls in TailRecOpt graph --- .../main/scala/hkmc2/codegen/TailRecOpt.scala | 27 ++- .../src/test/mlscript-compile/Runtime.mjs | 198 +++++++----------- .../deforest/eta-expansion/recursive.mls | 43 ++-- .../test/mlscript/tailrec/MultiArgLists.mls | 55 ++--- 4 files changed, 129 insertions(+), 194 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index ceb6386d46..2df2ff7c56 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -112,13 +112,22 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): case _ => N else N + def executesCallee(c: Call, f: FunDefn): Bool = + c.argss.size >= f.params.size + override def applyBlock(b: Block): Unit = b match case TailCallShape(r, c) => getFun(r) match case Some(value) => - if c.argss.size != value.params.size then + // Only exactly saturated calls can be rewritten as direct loop jumps. + // Over-applied calls do execute the callee, but then apply the + // returned value to extra argument lists, so they are normal edges. + if c.argss.size === value.params.size then + edges ::= CallEdge.TailCall(f.dSym, r)(c) + else if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only fully applied calls may be marked @tailcall." -> c.toLoc :: Nil)) - else edges ::= CallEdge.TailCall(f.dSym, r)(c) + if executesCallee(c, value) then + edges ::= CallEdge.NormalCall(f.dSym, r)(c) case None => if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only functions in this compilation unit may be marked @tailcall." -> c.toLoc :: Nil)) @@ -132,7 +141,13 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"This call is not in tail position." -> c.toLoc :: Nil)) c match - case CallToFun(r) => edges ::= CallEdge.NormalCall(f.dSym, r)(c) + case CallToFun(r) => getFun(r) match + // Under-applied curried calls only build closures for later argument + // lists; they do not execute the callee body and therefore do not + // form recursive call-graph edges. + case Some(value) if executesCallee(c, value) => + edges ::= CallEdge.NormalCall(f.dSym, r)(c) + case _ => case _ => case _ => super.applyResult(r) @@ -570,10 +585,10 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): new BlockTraverserShallow(): for f <- c.methods do applyBlock(f.body) - if checkAnnotations && f.tailRec then + if f.tailRec then raise(ErrorReport(msg"Class methods may not yet be marked @tailrec." -> f.dSym.toLoc :: Nil)) override def applyResult(r: Result): Unit = r match - case c: Call if checkAnnotations && c.metadata.explicitTailCall => + case c: Call if c.metadata.explicitTailCall => raise(ErrorReport(msg"Calls from class methods cannot yet be marked @tailcall." -> c.toLoc :: Nil)) case _ => super.applyResult(r) @@ -585,7 +600,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): // Class methods cannot yet be optimized as they cannot yet be marked final. if c.k is syntax.Cls then - reportClassesTailrec(c) + if checkAnnotations then reportClassesTailrec(c) val companion = c.companion.mapConserve: comp => val cMtds = optFunctionsFlat(comp.methods, S(comp.isym)) if cMtds is comp.methods diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs index 7acfe4eca2..3101c36e51 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs @@ -610,127 +610,6 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { static [definitionMetadata] = ["class", "Int31", [null]]; }); } - static handleEffect_resume_handleEffects_handleEffect_resume(id, param0, param1, param2) { - loopLabel: while (true) { - switch (id) { - case 0: - { - let param0_tmp; - param0_tmp = param0; - param0 = 1; - param1 = param0_tmp; - param2 = undefined; - id = 2; - continue loopLabel; - } - case 1: - { - let param1_tmp, param0_tmp; - param1_tmp = param1; - param0_tmp = param0; - param0 = 2; - param1 = param0_tmp; - param2 = param1_tmp; - id = 2; - continue loopLabel; - } - case 2: - loopLabel1: while (true) { - switch (param0) { - case 0: - lbl: while (true) { - let nxt, scrut; - if (param1 instanceof Runtime.EffectSig.class) { - nxt = Runtime.handleEffect(param1); - scrut = param1 === nxt; - if (scrut === true) { - Runtime.curEffect = param1; - return null - } - param1 = nxt; - continue lbl; - } - return param1; - } - case 1: - { - let prevHandlerFrame, scrut, handlerFrame, saved, old, scrut1, scrut2, scrut3, tmp, tmp1, tmp2, tmp3; - prevHandlerFrame = param1.contTrace; - lbl1: while (true) { - let scrut4, scrut5; - scrut4 = prevHandlerFrame.nextHandler !== null; - if (scrut4 === true) { - scrut5 = prevHandlerFrame.nextHandler.handler !== param1.handler; - if (scrut5 === true) { - prevHandlerFrame = prevHandlerFrame.nextHandler; - continue lbl1 - } - } - break; - } - scrut = prevHandlerFrame.nextHandler === null; - if (scrut === true) { - return param1 - } - handlerFrame = prevHandlerFrame.nextHandler; - saved = new Runtime.ContTrace.class(handlerFrame.next, param1.contTrace.last, handlerFrame.nextHandler, param1.contTrace.lastHandler, false); - param1.contTrace.last = handlerFrame; - param1.contTrace.lastHandler = handlerFrame; - handlerFrame.next = null; - handlerFrame.nextHandler = null; - Runtime.curEffect = null; - old = Runtime.stackDepth; - try { - tmp1 = Runtime.stackDepth + 2; - Runtime.stackDepth = tmp1; - tmp2 = Runtime.resume(param1.contTrace); - tmp3 = runtime.safeCall(param1.handlerFun(tmp2)); - tmp = tmp3; - } finally { - Runtime.stackDepth = old; - } - scrut1 = Runtime.curEffect !== null; - if (scrut1 === true) { - param1 = Runtime.curEffect; - scrut2 = saved.next !== null; - if (scrut2 === true) { - param1.contTrace.last.next = saved.next; - param1.contTrace.last = saved.last; - } - scrut3 = saved.nextHandler !== null; - if (scrut3 === true) { - param1.contTrace.lastHandler.nextHandler = saved.nextHandler; - param1.contTrace.lastHandler = saved.lastHandler; - return param1 - } - return param1; - } - return Runtime.resumeContTrace(saved, tmp); - } - case 2: - { - let scrut, tmp; - scrut = param1.resumed; - if (scrut === true) { - throw runtime.safeCall(globalThis.Error("Multiple resumption")) - } - param1.resumed = true; - tmp = Runtime.resumeContTrace(param1, param2); - param1 = tmp; - param0 = 0; - continue loopLabel1; - } - } - break; - } - break; - } - break; - } - } - static handleEffects_handleEffect_resume(id, param0, param1) { - return Runtime.handleEffect_resume_handleEffects_handleEffect_resume(2, id, param0, param1) - } static get unreachable() { throw runtime.safeCall(globalThis.Error("unreachable")); } @@ -1108,14 +987,85 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { return Runtime.handleBlockImpl(Runtime.curEffect, handler); } static handleEffects(cur) { - return Runtime.handleEffects_handleEffect_resume(0, cur, undefined) + lbl: while (true) { + let nxt, scrut; + if (cur instanceof Runtime.EffectSig.class) { + nxt = Runtime.handleEffect(cur); + scrut = cur === nxt; + if (scrut === true) { + Runtime.curEffect = cur; + return null + } + cur = nxt; + continue lbl; + } + return cur; + } } static handleEffect(cur) { - return Runtime.handleEffect_resume_handleEffects_handleEffect_resume(0, cur, undefined, undefined) + let prevHandlerFrame, scrut, handlerFrame, saved, old, scrut1, scrut2, scrut3, tmp, tmp1, tmp2, tmp3; + prevHandlerFrame = cur.contTrace; + lbl: while (true) { + let scrut4, scrut5; + scrut4 = prevHandlerFrame.nextHandler !== null; + if (scrut4 === true) { + scrut5 = prevHandlerFrame.nextHandler.handler !== cur.handler; + if (scrut5 === true) { + prevHandlerFrame = prevHandlerFrame.nextHandler; + continue lbl + } + } + break; + } + scrut = prevHandlerFrame.nextHandler === null; + if (scrut === true) { + return cur + } + handlerFrame = prevHandlerFrame.nextHandler; + saved = new Runtime.ContTrace.class(handlerFrame.next, cur.contTrace.last, handlerFrame.nextHandler, cur.contTrace.lastHandler, false); + cur.contTrace.last = handlerFrame; + cur.contTrace.lastHandler = handlerFrame; + handlerFrame.next = null; + handlerFrame.nextHandler = null; + Runtime.curEffect = null; + old = Runtime.stackDepth; + try { + tmp1 = Runtime.stackDepth + 2; + Runtime.stackDepth = tmp1; + tmp2 = Runtime.resume(cur.contTrace); + tmp3 = runtime.safeCall(cur.handlerFun(tmp2)); + tmp = tmp3; + } finally { + Runtime.stackDepth = old; + } + scrut1 = Runtime.curEffect !== null; + if (scrut1 === true) { + cur = Runtime.curEffect; + scrut2 = saved.next !== null; + if (scrut2 === true) { + cur.contTrace.last.next = saved.next; + cur.contTrace.last = saved.last; + } + scrut3 = saved.nextHandler !== null; + if (scrut3 === true) { + cur.contTrace.lastHandler.nextHandler = saved.nextHandler; + cur.contTrace.lastHandler = saved.lastHandler; + return cur + } + return cur; + } + return Runtime.resumeContTrace(saved, tmp); } static resume(contTrace) { return (value) => { - return Runtime.handleEffect_resume_handleEffects_handleEffect_resume(1, contTrace, value, undefined) + let scrut, tmp; + scrut = contTrace.resumed; + if (scrut === true) { + throw runtime.safeCall(globalThis.Error("Multiple resumption")) + } + contTrace.resumed = true; + tmp = Runtime.resumeContTrace(contTrace, value); + return Runtime.handleEffects(tmp); } } static resumeContTrace(contTrace, value) { diff --git a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls index 862635409c..01ce411800 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls @@ -112,39 +112,26 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ eta-expansion > flatten_31$flatten: [1, 6] //│ eta-expansion > <<< eta-expansion targets shapes <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, flatten_31$flatten, flatten_31_append_19$append$worker, flatten_31$flatten$worker, flatten_31$flatten_flatten_31$flatten$worker; -//│ define flatten_31$flatten_flatten_31$flatten$worker as fun flatten_31$flatten_flatten_31$flatten$worker⁰(id, param0) { -//│ loop loopLabel: -//│ match id -//│ 0 => -//│ set id = 1; -//│ continue loopLabel -//│ 1 => -//│ let arg$Cons$0$, arg$Cons$1$; -//│ match param0 -//│ Nil⁰ => -//│ return 0 -//│ Cons¹ => -//│ let ys; -//│ set arg$Cons$0$ = param0.h⁰; -//│ set arg$Cons$1$ = param0.t⁰; -//│ set ys = flatten_31$flatten⁰(arg$Cons$1$); -//│ return flatten_31_append_19$append$worker⁰(arg$Cons$0$, ys) -//│ else -//│ throw new globalThis⁰.Error⁰("match error") -//│ end -//│ else -//│ end -//│ end -//│ end -//│ }; +//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, flatten_31$flatten, flatten_31_append_19$append$worker, flatten_31$flatten$worker; //│ @inline @private //│ define flatten_31$flatten as fun flatten_31$flatten⁰(xss)() { -//│ return flatten_31$flatten_flatten_31$flatten$worker⁰(0, xss) +//│ return flatten_31$flatten$worker⁰(xss) //│ }; //│ @private //│ define flatten_31$flatten$worker as fun flatten_31$flatten$worker⁰(xss) { -//│ return flatten_31$flatten_flatten_31$flatten$worker⁰(1, xss) +//│ let arg$Cons$0$, arg$Cons$1$; +//│ match xss +//│ Nil⁰ => +//│ return 0 +//│ Cons¹ => +//│ let ys; +//│ set arg$Cons$0$ = xss.h⁰; +//│ set arg$Cons$1$ = xss.t⁰; +//│ set ys = flatten_31$flatten⁰(arg$Cons$1$); +//│ return flatten_31_append_19$append$worker⁰(arg$Cons$0$, ys) +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end //│ }; //│ @private //│ define flatten_31_append_19$append$worker as fun flatten_31_append_19$append$worker⁰(xs, ys) { diff --git a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls index e1d42b76b8..df078514c0 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls @@ -150,8 +150,15 @@ even(9)(0) //│ = [false, 9] -// Minimal repro: the second TailRecOpt pass can reprocess the loop generated by -// the first pass together with the curried wrapper, producing a larger loop. +// A partial application of a curried recursive function must not count as an +// executable recursive call edge. In the generated code for this block, the +// first TailRecOpt pass creates a shared loop plus a curried wrapper for +// `resume`. The body of the shared loop keeps the partial application +// `resume(x)`, which only constructs the closure waiting for `y`; it does not +// execute `resume`'s body. If TailRecOpt treats that partial application as a +// normal call edge, a later TailRecOpt pass incorrectly sees a cycle between the +// shared loop and the wrapper and wraps the already-optimized loop in a second, +// larger loop. :soir let sink = mut [] fun handleEffects(x) = handleEffect(x) @@ -160,56 +167,32 @@ fun handleEffect(x) = handleEffects(x) fun resume(x)(y) = handleEffects(x) //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let handleEffects_handleEffect_resume, resume_handleEffects_handleEffect_resume; -//│ define resume_handleEffects_handleEffect_resume as fun resume_handleEffects_handleEffect_resume⁰(id, param0, param1, param2) { +//│ let handleEffects_handleEffect; +//│ define handleEffects_handleEffect as fun handleEffects_handleEffect⁰(id, param0) { //│ loop loopLabel: //│ match id //│ 0 => -//│ let param1_tmp, param0_tmp; -//│ set param1_tmp = param1; -//│ set param0_tmp = param0; -//│ set param0 = 2; -//│ set param1 = param0_tmp; -//│ set param2 = param1_tmp; //│ set id = 1; //│ continue loopLabel //│ 1 => -//│ loop loopLabel1: -//│ match param0 -//│ 0 => -//│ set param0 = 1; -//│ continue loopLabel1 -//│ 1 => -//│ let tmp; -//│ set tmp = resume⁰(param1); -//│ do sink⁰.push﹖(tmp); -//│ set param0 = 0; -//│ continue loopLabel1 -//│ 2 => -//│ set param0 = 0; -//│ continue loopLabel1 -//│ else -//│ end -//│ end -//│ end +//│ let tmp; +//│ set tmp = resume⁰(param0); +//│ do sink⁰.push﹖(tmp); +//│ set id = 0; +//│ continue loopLabel //│ else //│ end //│ end //│ end //│ }; -//│ define handleEffects_handleEffect_resume as fun handleEffects_handleEffect_resume⁰(id, param0, param1) { -//│ return resume_handleEffects_handleEffect_resume⁰(1, id, param0, param1) -//│ }; //│ define handleEffects⁰ as fun handleEffects¹(x) { -//│ return handleEffects_handleEffect_resume⁰(0, x, undefined) +//│ return handleEffects_handleEffect⁰(0, x) //│ }; //│ define handleEffect⁰ as fun handleEffect¹(x) { -//│ return handleEffects_handleEffect_resume⁰(1, x, undefined) +//│ return handleEffects_handleEffect⁰(1, x) //│ }; //│ @inline -//│ define resume¹ as fun resume⁰(x)(y) { -//│ return resume_handleEffects_handleEffect_resume⁰(0, x, y, undefined) -//│ }; +//│ define resume¹ as fun resume⁰(x)(y) { return handleEffects¹(x) }; //│ set sink⁰ = mut []; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— From 8508745ce30436590d5fff278f219974c6093d47 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 11:09:31 +0800 Subject: [PATCH 05/15] Handle nullary callees in TailRecOpt arity checks --- .../main/scala/hkmc2/codegen/TailRecOpt.scala | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index 2df2ff7c56..ff600d737a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -72,6 +72,23 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): type AccessMap = Map[ScopedInfo, AccessInfo] + private def compareCallArgListCount(c: Call, f: FunDefn): Int = + val cmp = c.argss.sizeCompare(f.params.size) + // A zero-argument-list definition may still be invoked by a `Call` node: + // the callee body is evaluated as a nullary thunk, and the call's argument + // lists are then applied to the returned value. For non-nullary callees, + // passing more argument lists than the callee can receive should not happen; + // leave this assertion here so that invariant can be enabled once existing + // nullary call sites have been audited. + // softAssert( + // cmp <= 0 || f.params.isEmpty, + // s"Call node passes ${c.argss.size} argument lists to ${f.dSym.showDbg}, which can receive ${f.params.size}.", + // ) + cmp + + private def appliesResultOfNullaryCallee(f: FunDefn, cmp: Int): Bool = + cmp > 0 && f.params.isEmpty + object CallToFun: def unapply(c: Call): Opt[TermSymbol] = c match case Call(fun = Value.MemberRef(_, r: TermSymbol)) => S(r) @@ -112,22 +129,22 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): case _ => N else N - def executesCallee(c: Call, f: FunDefn): Bool = - c.argss.size >= f.params.size - override def applyBlock(b: Block): Unit = b match case TailCallShape(r, c) => getFun(r) match case Some(value) => // Only exactly saturated calls can be rewritten as direct loop jumps. - // Over-applied calls do execute the callee, but then apply the - // returned value to extra argument lists, so they are normal edges. - if c.argss.size === value.params.size then - edges ::= CallEdge.TailCall(f.dSym, r)(c) - else - if checkAnnotations && c.metadata.explicitTailCall then - raise(ErrorReport(msg"Only fully applied calls may be marked @tailcall." -> c.toLoc :: Nil)) - if executesCallee(c, value) then + // Under-applied calls only build closures for later argument lists, + // while calls to zero-argument-list definitions execute the callee + // and then apply the returned value, so they are non-tail edges. + val cmp = compareCallArgListCount(c, value) + cmp match + case 0 => edges ::= CallEdge.TailCall(f.dSym, r)(c) + case n if n < 0 => + if checkAnnotations && c.metadata.explicitTailCall then + raise(ErrorReport(msg"Only fully applied calls may be marked @tailcall." -> c.toLoc :: Nil)) + case _ if appliesResultOfNullaryCallee(value, cmp) => edges ::= CallEdge.NormalCall(f.dSym, r)(c) + case _ => case None => if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only functions in this compilation unit may be marked @tailcall." -> c.toLoc :: Nil)) @@ -145,8 +162,13 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): // Under-applied curried calls only build closures for later argument // lists; they do not execute the callee body and therefore do not // form recursive call-graph edges. - case Some(value) if executesCallee(c, value) => - edges ::= CallEdge.NormalCall(f.dSym, r)(c) + case Some(value) => + val cmp = compareCallArgListCount(c, value) + cmp match + case 0 => edges ::= CallEdge.NormalCall(f.dSym, r)(c) + case _ if appliesResultOfNullaryCallee(value, cmp) => + edges ::= CallEdge.NormalCall(f.dSym, r)(c) + case _ => case _ => case _ => case _ => super.applyResult(r) @@ -365,11 +387,11 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): case None => super.applyBlock(b) case Some(id) => val callee = dSymToDefn(calleeSym) - val calleeParamsMap = paramSymsMap(callee.dSym) // We require the call to be fully applied. - if c.argss.size != callee.params.size then + if compareCallArgListCount(c, callee) != 0 then super.applyBlock(b) else + val calleeParamsMap = paramSymsMap(callee.dSym) // The code used to continute the loop. val cont = if funsLen === 1 then Continue(loopSym) From ec24a379251ff241eb53c43f6eae4e09cca4eec0 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 11:16:09 +0800 Subject: [PATCH 06/15] Clarify TailRecOpt call saturation checks --- .../main/scala/hkmc2/codegen/TailRecOpt.scala | 53 +++++++++---------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index ff600d737a..1bee73f2dd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -72,22 +72,26 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): type AccessMap = Map[ScopedInfo, AccessInfo] - private def compareCallArgListCount(c: Call, f: FunDefn): Int = - val cmp = c.argss.sizeCompare(f.params.size) + private def checkArgListCount(c: Call, f: FunDefn, cmp: Int): Unit = // A zero-argument-list definition may still be invoked by a `Call` node: // the callee body is evaluated as a nullary thunk, and the call's argument // lists are then applied to the returned value. For non-nullary callees, - // passing more argument lists than the callee can receive should not happen; - // leave this assertion here so that invariant can be enabled once existing - // nullary call sites have been audited. - // softAssert( - // cmp <= 0 || f.params.isEmpty, - // s"Call node passes ${c.argss.size} argument lists to ${f.dSym.showDbg}, which can receive ${f.params.size}.", - // ) - cmp + // passing more argument lists than the callee can receive violates the + // expected IR shape and is reported here. + softAssert( + cmp <= 0 || f.params.isEmpty, + s"Call node passes ${c.argss.size} argument lists to ${f.dSym.showDbg}, which can receive ${f.params.size}.", + ) + + private def isExactlySaturatedCall(c: Call, f: FunDefn): Bool = + val cmp = c.argss.sizeCompare(f.params.size) + checkArgListCount(c, f, cmp) + cmp === 0 - private def appliesResultOfNullaryCallee(f: FunDefn, cmp: Int): Bool = - cmp > 0 && f.params.isEmpty + private def executesCallee(c: Call, f: FunDefn): Bool = + val cmp = c.argss.sizeCompare(f.params.size) + checkArgListCount(c, f, cmp) + cmp >= 0 object CallToFun: def unapply(c: Call): Opt[TermSymbol] = c match @@ -136,15 +140,13 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): // Under-applied calls only build closures for later argument lists, // while calls to zero-argument-list definitions execute the callee // and then apply the returned value, so they are non-tail edges. - val cmp = compareCallArgListCount(c, value) - cmp match - case 0 => edges ::= CallEdge.TailCall(f.dSym, r)(c) - case n if n < 0 => - if checkAnnotations && c.metadata.explicitTailCall then - raise(ErrorReport(msg"Only fully applied calls may be marked @tailcall." -> c.toLoc :: Nil)) - case _ if appliesResultOfNullaryCallee(value, cmp) => + if isExactlySaturatedCall(c, value) then + edges ::= CallEdge.TailCall(f.dSym, r)(c) + else + if checkAnnotations && c.metadata.explicitTailCall then + raise(ErrorReport(msg"Only fully applied calls may be marked @tailcall." -> c.toLoc :: Nil)) + if executesCallee(c, value) then edges ::= CallEdge.NormalCall(f.dSym, r)(c) - case _ => case None => if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only functions in this compilation unit may be marked @tailcall." -> c.toLoc :: Nil)) @@ -162,13 +164,8 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): // Under-applied curried calls only build closures for later argument // lists; they do not execute the callee body and therefore do not // form recursive call-graph edges. - case Some(value) => - val cmp = compareCallArgListCount(c, value) - cmp match - case 0 => edges ::= CallEdge.NormalCall(f.dSym, r)(c) - case _ if appliesResultOfNullaryCallee(value, cmp) => - edges ::= CallEdge.NormalCall(f.dSym, r)(c) - case _ => + case Some(value) if executesCallee(c, value) => + edges ::= CallEdge.NormalCall(f.dSym, r)(c) case _ => case _ => case _ => super.applyResult(r) @@ -388,7 +385,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): case Some(id) => val callee = dSymToDefn(calleeSym) // We require the call to be fully applied. - if compareCallArgListCount(c, callee) != 0 then + if !isExactlySaturatedCall(c, callee) then super.applyBlock(b) else val calleeParamsMap = paramSymsMap(callee.dSym) From eca0eda90309b9397f40e195cac839b9cc22a88e Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Sat, 4 Jul 2026 11:30:21 +0800 Subject: [PATCH 07/15] WIP --- .../main/scala/hkmc2/codegen/TailRecOpt.scala | 3 +- .../src/test/mlscript/tailrec/Errors.mls | 44 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index 1bee73f2dd..f49d6213ab 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -145,6 +145,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): else if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only fully applied calls may be marked @tailcall." -> c.toLoc :: Nil)) + // * if executesCallee(c, value) then edges ::= CallEdge.NormalCall(f.dSym, r)(c) case None => @@ -283,7 +284,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): if nonTailCallsLs.sizeCompare(calls) === 0 then for f <- funs if checkAnnotations && f.tailRec do - raise(WarningReport(msg"This function does not directly self-recurse, but is marked @tailrec." -> f.dSym.toLoc :: Nil)) + raise(WarningReport(msg"This function is marked @tailrec but has no apparent tail calls." -> f.dSym.toLoc :: Nil)) return (N, funs) if !nonTailCalls.isEmpty then diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls index 6d59725bbf..1b2f97487c 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls @@ -71,7 +71,7 @@ module A with :w @tailrec fun f = 2 -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. //│ ║ l.73: fun f = 2 //│ ╙── ^ @@ -79,7 +79,7 @@ fun f = 2 module A with @tailrec fun f() = 2 -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. //│ ║ l.81: fun f() = 2 //│ ╙── ^ @@ -88,7 +88,7 @@ module A with fun foo() = Foo.bar() module Foo with fun bar() = foo() -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. //│ ║ l.88: fun foo() = Foo.bar() //│ ╙── ^^^ @@ -117,7 +117,7 @@ fun scan(idx, acc) = @tailrec fun go(idx, tok) = scan(idx, tok) go(idx, acc) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. //│ ║ l.116: fun scan(idx, acc) = //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. @@ -157,3 +157,39 @@ fun g(x)(y) = //│ ╔══[COMPILATION ERROR] This call is not optimized as it does not directly recurse through its parent function. //│ ║ l.151: @tailcall g(x)(0) //│ ╙── ^^^^^^^ + + +:w +module M with + @tailrec + fun f = print("ok"); f +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ║ l.165: fun f = print("ok"); f +//│ ╙── ^ + +:w +module M with + @tailrec + fun f = case 0 then f(1) +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ║ l.173: fun f = case 0 then f(1) +//│ ╙── ^ + + +:w +@tailrec +fun foo(x) = + foo(1) + 1 +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ║ l.181: fun foo(x) = +//│ ╙── ^^^ + +:w +@tailrec +fun foo(x) = + 1 |> foo +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ║ l.189: fun foo(x) = +//│ ╙── ^^^ + + From b99eb95f08f8f22885d4ceb2fe11a3e48c163e70 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 14:05:11 +0800 Subject: [PATCH 08/15] Clean up TailRecOpt saturation handling --- .../main/scala/hkmc2/codegen/TailRecOpt.scala | 36 +++++++------------ .../src/test/mlscript/tailrec/Annots.mls | 2 +- .../src/test/mlscript/tailrec/Errors.mls | 12 ++++--- .../src/test/mlscript/tailrec/Simple.mls | 23 +++++------- 4 files changed, 30 insertions(+), 43 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index f49d6213ab..1635726180 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -72,27 +72,20 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): type AccessMap = Map[ScopedInfo, AccessInfo] - private def checkArgListCount(c: Call, f: FunDefn, cmp: Int): Unit = + private def isExactlySaturatedCall(c: Call, f: FunDefn): Bool = + val cmp = c.argss.sizeCompare(f.params.size) // A zero-argument-list definition may still be invoked by a `Call` node: // the callee body is evaluated as a nullary thunk, and the call's argument // lists are then applied to the returned value. For non-nullary callees, // passing more argument lists than the callee can receive violates the - // expected IR shape and is reported here. - softAssert( - cmp <= 0 || f.params.isEmpty, - s"Call node passes ${c.argss.size} argument lists to ${f.dSym.showDbg}, which can receive ${f.params.size}.", - ) - - private def isExactlySaturatedCall(c: Call, f: FunDefn): Bool = - val cmp = c.argss.sizeCompare(f.params.size) - checkArgListCount(c, f, cmp) + // expected IR shape. Once the known producers of such calls have been fixed, + // we should restore a softAssert here to report that invariant violation. + // softAssert( + // cmp <= 0 || f.params.isEmpty, + // s"Call node passes ${c.argss.size} argument lists to ${f.dSym.showDbg}, which can receive ${f.params.size}.", + // ) cmp === 0 - private def executesCallee(c: Call, f: FunDefn): Bool = - val cmp = c.argss.sizeCompare(f.params.size) - checkArgListCount(c, f, cmp) - cmp >= 0 - object CallToFun: def unapply(c: Call): Opt[TermSymbol] = c match case Call(fun = Value.MemberRef(_, r: TermSymbol)) => S(r) @@ -137,17 +130,13 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): case TailCallShape(r, c) => getFun(r) match case Some(value) => // Only exactly saturated calls can be rewritten as direct loop jumps. - // Under-applied calls only build closures for later argument lists, - // while calls to zero-argument-list definitions execute the callee - // and then apply the returned value, so they are non-tail edges. + // Under-applied calls only build closures for later argument lists; + // over-applied calls cannot be rewritten as direct jumps either. if isExactlySaturatedCall(c, value) then edges ::= CallEdge.TailCall(f.dSym, r)(c) else if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only fully applied calls may be marked @tailcall." -> c.toLoc :: Nil)) - // * - if executesCallee(c, value) then - edges ::= CallEdge.NormalCall(f.dSym, r)(c) case None => if checkAnnotations && c.metadata.explicitTailCall then raise(ErrorReport(msg"Only functions in this compilation unit may be marked @tailcall." -> c.toLoc :: Nil)) @@ -165,7 +154,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): // Under-applied curried calls only build closures for later argument // lists; they do not execute the callee body and therefore do not // form recursive call-graph edges. - case Some(value) if executesCallee(c, value) => + case Some(value) if isExactlySaturatedCall(c, value) => edges ::= CallEdge.NormalCall(f.dSym, r)(c) case _ => case _ => @@ -282,7 +271,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): case c: CallEdge.NormalCall => c.f2 -> c.call val nonTailCalls = nonTailCallsLs.toMap - if nonTailCallsLs.sizeCompare(calls) === 0 then + if calls.isEmpty then for f <- funs if checkAnnotations && f.tailRec do raise(WarningReport(msg"This function is marked @tailrec but has no apparent tail calls." -> f.dSym.toLoc :: Nil)) return (N, funs) @@ -298,6 +287,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): :: msg"It could self-recurse through this call, which is not a tail call." -> reportLoc :: Nil )) + return (N, funs) val maxParamLen = maxInt(funs, paramsLen) val paramSyms = diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls b/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls index ddac47cf5d..9f28be139d 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls @@ -62,7 +62,7 @@ class A module A with @tailrec fun f = 2 -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. //│ ║ l.64: fun f = 2 //│ ╙── ^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls index 1b2f97487c..ca48d7befa 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls @@ -176,20 +176,22 @@ module M with //│ ╙── ^ -:w +:e @tailrec fun foo(x) = foo(1) + 1 -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[COMPILATION ERROR] This function is not tail recursive. //│ ║ l.181: fun foo(x) = -//│ ╙── ^^^ +//│ ║ ^^^ +//│ ╟── It could self-recurse through this call, which is not a tail call. +//│ ║ l.182: foo(1) + 1 +//│ ╙── ^^^^^^ :w @tailrec fun foo(x) = 1 |> foo //│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. -//│ ║ l.189: fun foo(x) = +//│ ║ l.192: fun foo(x) = //│ ╙── ^^^ - diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls index 6a86654700..8f48b2ac70 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls @@ -13,7 +13,7 @@ fun f = f module Foo with @tailrec fun bar = bar -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. //│ ║ l.15: fun bar = bar //│ ╙── ^^^ @@ -26,19 +26,15 @@ module Foo with fun f(x) = f(x + 1) +// The inner self-call in this source expression is evaluated before the final +// call and becomes a normal recursive edge in ANF. TailRecOpt must therefore +// leave the function unrewritten: optimizing only the final call would still +// keep one stack-consuming recursive call and would silently change the shape +// of the generated code without making the function tail-recursive. :sjs fun f(x) = f(f(x) + 1) //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let f3; -//│ f3 = function f(x) { -//│ loopLabel: while (true) { -//│ let tmp, tmp1; -//│ tmp = f3(x); -//│ tmp1 = tmp + 1; -//│ x = tmp1; -//│ continue loopLabel; -//│ } -//│ }; +//│ let f3; f3 = function f(x) { let tmp, tmp1; tmp = f3(x); tmp1 = tmp + 1; return f3(tmp1) }; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— module A with @@ -51,8 +47,7 @@ module A with fun f(x) = B.g(x + 1) module B with fun g(x) = A.f(x * 2) -//│ ╔══[WARNING] This function does not directly self-recurse, but is marked @tailrec. -//│ ║ l.51: fun f(x) = B.g(x + 1) +//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ║ l.47: fun f(x) = B.g(x + 1) //│ ╙── ^ - From 3419533c62bd767bc649e1b2a110ba99240c5cf6 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 20:21:48 +0800 Subject: [PATCH 09/15] Avoid applying runtime unreachable --- .../src/main/scala/hkmc2/codegen/TailRecOpt.scala | 14 +++++++------- hkmc2/shared/src/test/mlscript-compile/Runtime.mjs | 4 ++-- hkmc2/shared/src/test/mlscript-compile/Runtime.mls | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index 1635726180..e60dd14a94 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -77,13 +77,13 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): // A zero-argument-list definition may still be invoked by a `Call` node: // the callee body is evaluated as a nullary thunk, and the call's argument // lists are then applied to the returned value. For non-nullary callees, - // passing more argument lists than the callee can receive violates the - // expected IR shape. Once the known producers of such calls have been fixed, - // we should restore a softAssert here to report that invariant violation. - // softAssert( - // cmp <= 0 || f.params.isEmpty, - // s"Call node passes ${c.argss.size} argument lists to ${f.dSym.showDbg}, which can receive ${f.params.size}.", - // ) + // passing more argument lists than the callee can receive violates the IR + // invariant that a `Call` node does not apply the result of a non-nullary + // callee. + softAssert( + cmp <= 0 || f.params.isEmpty, + s"Call node passes ${c.argss.size} argument lists to ${f.dSym.showDbg}, which can receive ${f.params.size}.", + ) cmp === 0 object CallToFun: diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs index 3101c36e51..ac69426c1b 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs @@ -1179,9 +1179,9 @@ lambda$ = (undefined, function (Runtime2, EffectHandle1, value) { if (rhs instanceof Runtime.Int31.class) { return lhs + rhs } - return runtime.safeCall(Runtime.unreachable()); + return Runtime.unreachable; } - return runtime.safeCall(Runtime.unreachable()); + return Runtime.unreachable; } toString() { return runtime.render(this); } static [definitionMetadata] = ["class", "Runtime"]; diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls index 7ca96f1e33..e9f0dfdbd8 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls @@ -509,4 +509,4 @@ class Int31(v) with fun plus_impl(lhs, rhs) = if lhs is Int31 and rhs is Int31 then lhs + rhs - else unreachable() + else unreachable From a7dcfc1c1606882fad51d6d72a9717962fb85287 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Sat, 4 Jul 2026 21:14:19 +0800 Subject: [PATCH 10/15] Address Copilot review --- .../src/test/mlscript/tailrec/TailRecOpt.mls | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls index 6e000968cf..edf8964562 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls @@ -220,6 +220,7 @@ fun f(x) = // Ensure y is set using x_tmp and x is assigned to x_tmp :soir fun f(x, y) = + print(y) if x == 0 then 0 else f(x - 1, x) @@ -227,13 +228,17 @@ fun f(x, y) = //│ define f⁴ as fun f⁵(x, y) { //│ loop loopLabel: //│ let scrut, tmp; +//│ do Predef⁰.print⁰(y); //│ set scrut = Predef⁰.equals⁰(x, 0); //│ match scrut //│ true => //│ return 0 //│ else +//│ let x_tmp; //│ set tmp = -⁰(x, 1); +//│ set x_tmp = x; //│ set x = tmp; +//│ set y = x_tmp; //│ continue loopLabel //│ end //│ unreachable /* Rest of abortive labelled block */ @@ -241,6 +246,12 @@ fun f(x, y) = //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +f(2, 4) +//│ > 4 +//│ > 2 +//│ > 1 +//│ = 0 + // Make sure variables accessed by nested functions or classes are re-defined in a new scope in the while loop :expect 2 @@ -292,10 +303,10 @@ module A with @tailcall g(x - 1) A.f(10000) //│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.291: fun g(x) = if x < 0 then 0 else @tailcall f(x) +//│ ║ l.302: fun g(x) = if x < 0 then 0 else @tailcall f(x) //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This tail call exits the current scope and is not optimized. -//│ ║ l.292: @tailcall g(x - 1) +//│ ║ l.303: @tailcall g(x - 1) //│ ╙── ^^^^^^^^ //│ ═══[RUNTIME ERROR] RangeError: Maximum call stack size exceeded @@ -322,12 +333,12 @@ class A with fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) (new A).f(10) //│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.321: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ║ l.332: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) //│ ╙── ^^^^^^^^ //│ ╔══[COMPILATION ERROR] Class methods may not yet be marked @tailrec. -//│ ║ l.321: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) +//│ ║ l.332: @tailrec fun f(x) = if x == 0 then 0 else @tailcall g(x - 1) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Calls from class methods cannot yet be marked @tailcall. -//│ ║ l.322: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) +//│ ║ l.333: fun g(x) = if x == 0 then 1 else @tailcall f(x - 1) //│ ╙── ^^^^^^^^ //│ = 0 From c35099877d6087e1ba77d10eefa7c5bc45a71786 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Sun, 5 Jul 2026 19:44:46 +0800 Subject: [PATCH 11/15] Update comment --- hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index e60dd14a94..e9f0013a53 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -78,8 +78,8 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): // the callee body is evaluated as a nullary thunk, and the call's argument // lists are then applied to the returned value. For non-nullary callees, // passing more argument lists than the callee can receive violates the IR - // invariant that a `Call` node does not apply the result of a non-nullary - // callee. + // invariant that a `Call` node does not pass more arguments than a non-nullary + // callee can receive. softAssert( cmp <= 0 || f.params.isEmpty, s"Call node passes ${c.argss.size} argument lists to ${f.dSym.showDbg}, which can receive ${f.params.size}.", From dc717ba7d5552125c501b2dbfcb3bdddb3d139ce Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Sun, 5 Jul 2026 20:41:44 +0800 Subject: [PATCH 12/15] Add a second simplification pass --- .../hkmc2/codegen/CompilationPipeline.scala | 8 ++- .../mlscript/dead-param-elim/refresher.mls | 47 ++++++------- .../src/test/mlscript/deforest/cyclic.mls | 34 +++++----- .../deforest/eta-expansion/recursive.mls | 49 ++++++------- .../src/test/mlscript/opt/ConstantFolding.mls | 2 + .../test/mlscript/opt/InfiniteInlining.mls | 68 ++++++++++++------- 6 files changed, 116 insertions(+), 92 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala index 47d8119922..e95b7b8b96 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/CompilationPipeline.scala @@ -62,7 +62,10 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): runPass("TailRecOpt")(TailRecOpt(true).transform) runPass("WorkerWrapper")(WorkerWrapper(symbolsToPreserve, otl, printer)) - runPass("BlockSimplifier")(BlockSimplifier(symbolsToPreserve, otl, printer).apply) + + // * First simplification pass + runPass("BlockSimplifier 1")(BlockSimplifier(symbolsToPreserve, otl, printer).apply) + runPass("DeadParamElim")(otl.givenIn(DeadParamElim.apply)) // * More tailrec opportunities might be revealed after WorkerWrapper + BlockSimplifier, @@ -70,4 +73,7 @@ class CompilationPipeline(using Config, Raise, State, Ctx, SymbolPrinter): // * into proper tail positions. runPass("TailRecOpt")(TailRecOpt(false).transform) + // * Final simplification pass + runPass("BlockSimplifier 2")(BlockSimplifier(symbolsToPreserve, otl, printer).apply) + result diff --git a/hkmc2/shared/src/test/mlscript/dead-param-elim/refresher.mls b/hkmc2/shared/src/test/mlscript/dead-param-elim/refresher.mls index a8b7e096b6..f914e8caa0 100644 --- a/hkmc2/shared/src/test/mlscript/dead-param-elim/refresher.mls +++ b/hkmc2/shared/src/test/mlscript/dead-param-elim/refresher.mls @@ -69,42 +69,39 @@ wrapper(42, 0) //│ dead-param-elim > prodfun wrapper#0 @ wrapper@3 -> eliminable: {1} //│ dead-param-elim > <<< dead-param-elim results <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let wrapper_3$wrapper; -//│ define wrapper_3$wrapper as fun wrapper_3$wrapper⁰(val1) { -//│ let A, tmp, tmp1; -//│ define A as class A⁰ { +//│ let val1, A, tmp, tmp1; +//│ define wrapper⁰ as fun wrapper¹(val11, dead) { +//│ let A1, tmp2, tmp3; +//│ define A1 as class A⁰ { //│ method makeB⁰ = fun makeB¹() { //│ let B; //│ define B as class B⁰ { //│ method getVal⁰ = fun getVal¹() { -//│ return val1 +//│ return val11 //│ } //│ }; //│ return new B⁰() //│ } //│ }; -//│ set tmp = new A⁰(); -//│ set tmp1 = tmp.makeB¹(); -//│ return tmp1.getVal﹖() +//│ set tmp2 = new A⁰(); +//│ set tmp3 = tmp2.makeB¹(); +//│ return tmp3.getVal﹖() //│ }; -//│ define wrapper⁰ as fun wrapper¹(val1, dead) { -//│ let A, tmp, tmp1; -//│ define A as class A¹ { -//│ method makeB² = fun makeB³() { -//│ let B; -//│ define B as class B¹ { -//│ method getVal² = fun getVal³() { -//│ return val1 -//│ } -//│ }; -//│ return new B¹() -//│ } -//│ }; -//│ set tmp = new A¹(); -//│ set tmp1 = tmp.makeB³(); -//│ return tmp1.getVal﹖() +//│ set val1 = 42; +//│ define A as class A¹ { +//│ method makeB² = fun makeB³() { +//│ let B; +//│ define B as class B¹ { +//│ method getVal² = fun getVal³() { +//│ return val1 +//│ } +//│ }; +//│ return new B¹() +//│ } //│ }; -//│ return wrapper_3$wrapper⁰(42) +//│ set tmp = new A¹(); +//│ set tmp1 = tmp.makeB³(); +//│ return tmp1.getVal﹖() //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 42 diff --git a/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls b/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls index 1b6f31a044..dc9c9d6781 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls @@ -84,23 +84,7 @@ modSome(Some(17), 3) //│ deforest > fields: x⁰.x¹ //│ deforest > <<< fusing <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let modSome_12$x_Some$worker; -//│ @affine(1) @private -//│ define modSome_12$x_Some$worker as fun modSome_12$x_Some$worker⁰(Some_x, fv_m, fv_scrut, fv_tmp) { -//│ loop loopLabel: -//│ set fv_scrut = >=⁰(Some_x, fv_m); -//│ match fv_scrut -//│ true => -//│ set fv_tmp = -⁰(Some_x, fv_m); -//│ set Some_x = fv_tmp; -//│ set fv_scrut = undefined; -//│ set fv_tmp = undefined; -//│ continue loopLabel -//│ else -//│ return Some⁰(Some_x) -//│ end -//│ end -//│ }; +//│ let Some_x, fv_scrut, fv_tmp, inlinedVal; //│ define modSome⁰ as fun modSome¹(x, m) { //│ loop loopLabel: //│ let scrut, arg$Some$0$, tmp, tmp1; @@ -124,7 +108,21 @@ modSome(Some(17), 3) //│ end //│ unreachable /* Rest of abortive labelled block */ //│ }; -//│ return modSome_12$x_Some$worker⁰(17, 3, undefined, undefined) +//│ set Some_x = 17; +//│ block inlinedLbl: +//│ loop loopLabel: +//│ set fv_scrut = >=⁰(Some_x, 3); +//│ match fv_scrut +//│ true => +//│ set fv_tmp = -⁰(Some_x, 3); +//│ set Some_x = fv_tmp; +//│ continue loopLabel +//│ else +//│ set inlinedVal = Some⁰(Some_x); +//│ break inlinedLbl +//│ end +//│ unreachable /* Rest of abortive labelled block */ +//│ return inlinedVal //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = Some(2) diff --git a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls index 01ce411800..04384fc97a 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls @@ -33,35 +33,17 @@ sum(enum(5), 0) //│ eta-expansion > enum_13$enum: [1, 7] //│ eta-expansion > <<< eta-expansion targets shapes <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let enum_13$enum$worker; -//│ @private -//│ define enum_13$enum$worker as fun enum_13$enum$worker⁰(x, eta$0$1) { -//│ loop loopLabel: -//│ let scrut, tmp; -//│ set scrut = <⁰(x, 0); -//│ match scrut -//│ true => -//│ return eta$0$1 -//│ else -//│ let fv_tmp; -//│ set tmp = -⁰(x, 1); -//│ set fv_tmp = +⁰(x, eta$0$1); -//│ set x = tmp; -//│ set eta$0$1 = fv_tmp; -//│ continue loopLabel -//│ end -//│ end -//│ }; -//│ define enum⁰ as fun enum¹(x) { +//│ let x, eta$0$1, inlinedVal; +//│ define enum⁰ as fun enum¹(x1) { //│ let scrut, tmp, tmp1; -//│ set scrut = <⁰(x, 0); +//│ set scrut = <⁰(x1, 0); //│ match scrut //│ true => //│ return Nil⁰ //│ else -//│ set tmp = -⁰(x, 1); +//│ set tmp = -⁰(x1, 1); //│ set tmp1 = enum¹(tmp); -//│ return Cons⁰(x, tmp1) +//│ return Cons⁰(x1, tmp1) //│ end //│ }; //│ define sum⁰ as fun sum¹(ls, acc) { @@ -82,7 +64,26 @@ sum(enum(5), 0) //│ end //│ unreachable /* Rest of abortive labelled block */ //│ }; -//│ return enum_13$enum$worker⁰(5, 0) +//│ set x = 5; +//│ set eta$0$1 = 0; +//│ block inlinedLbl: +//│ loop loopLabel: +//│ let scrut, tmp; +//│ set scrut = <⁰(x, 0); +//│ match scrut +//│ true => +//│ set inlinedVal = eta$0$1; +//│ break inlinedLbl +//│ else +//│ let fv_tmp; +//│ set tmp = -⁰(x, 1); +//│ set fv_tmp = +⁰(x, eta$0$1); +//│ set x = tmp; +//│ set eta$0$1 = fv_tmp; +//│ continue loopLabel +//│ end +//│ unreachable /* Rest of abortive labelled block */ +//│ return inlinedVal //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 15 diff --git a/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls b/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls index 9719f9eac5..497ac0b432 100644 --- a/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls +++ b/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls @@ -54,6 +54,8 @@ in //│ Captured variables: HashSet() //│ ⬤ Simplif. iter. 3 //│ Captured variables: HashSet() +//│ ⬤ Simplif. iter. 1 +//│ Captured variables: HashSet() //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ return Predef⁰.print⁰(6) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/opt/InfiniteInlining.mls b/hkmc2/shared/src/test/mlscript/opt/InfiniteInlining.mls index bb3bd07f21..89e41c8d8d 100644 --- a/hkmc2/shared/src/test/mlscript/opt/InfiniteInlining.mls +++ b/hkmc2/shared/src/test/mlscript/opt/InfiniteInlining.mls @@ -10,47 +10,67 @@ funny //│ let lambda; //│ @private //│ define lambda as fun lambda²(caseScrut) { -//│ let tmp, inlinedVal, tmp1, inlinedVal1, tmp2, inlinedVal2, tmp3, inlinedVal3, tmp4, inlinedVal4, tmp5, tmp6, caseScrut1, inlinedVal5, tmp7, tmp8, lambda1, caseScrut2, inlinedVal6; +//│ let tmp, inlinedVal, tmp1, inlinedVal1, tmp2, inlinedVal2, tmp3, inlinedVal3, tmp4, inlinedVal4, tmp5, inlinedVal5, tmp6, inlinedVal6, tmp7, inlinedVal7, tmp8, inlinedVal8, tmp9, inlinedVal9, tmp10, inlinedVal10, tmp11, inlinedVal11, tmp12, inlinedVal12, tmp13, inlinedVal13, tmp14, inlinedVal14, tmp15, tmp16, caseScrut1, inlinedVal15, tmp17, tmp18, lambda1, caseScrut2, inlinedVal16; //│ set tmp = -⁰(caseScrut, 1); //│ set tmp1 = -⁰(tmp, 1); //│ set tmp2 = -⁰(tmp1, 1); //│ set tmp3 = -⁰(tmp2, 1); //│ set tmp4 = -⁰(tmp3, 1); //│ set tmp5 = -⁰(tmp4, 1); -//│ set caseScrut1 = tmp5; -//│ set tmp7 = -⁰(tmp5, 1); -//│ set caseScrut2 = tmp7; +//│ set tmp6 = -⁰(tmp5, 1); +//│ set tmp7 = -⁰(tmp6, 1); +//│ set tmp8 = -⁰(tmp7, 1); +//│ set tmp9 = -⁰(tmp8, 1); +//│ set tmp10 = -⁰(tmp9, 1); +//│ set tmp11 = -⁰(tmp10, 1); +//│ set tmp12 = -⁰(tmp11, 1); +//│ set tmp13 = -⁰(tmp12, 1); +//│ set tmp14 = -⁰(tmp13, 1); +//│ set tmp15 = -⁰(tmp14, 1); +//│ set caseScrut1 = tmp15; +//│ set tmp17 = -⁰(tmp15, 1); +//│ set caseScrut2 = tmp17; //│ block inlinedLbl: -//│ let tmp9, tmp10, tmp11, inlinedVal7, lambda2; +//│ let tmp19, tmp20, tmp21, inlinedVal17, lambda2; //│ @private //│ define lambda2 as fun lambda¹(caseScrut3) { -//│ let tmp12, tmp13, tmp14, inlinedVal8; +//│ let tmp22, tmp23, tmp24, inlinedVal18; //│ block inlinedLbl1: //│ let lambda3; //│ @private //│ define lambda3 as fun lambda⁰(caseScrut4) { -//│ let tmp15, tmp16, tmp17; -//│ set tmp15 = funny⁰(); -//│ set tmp16 = -⁰(caseScrut4, 1); -//│ set tmp17 = tmp15(tmp16); -//│ return +⁰(tmp17, 1) +//│ let tmp25, tmp26, tmp27; +//│ set tmp25 = funny⁰(); +//│ set tmp26 = -⁰(caseScrut4, 1); +//│ set tmp27 = tmp25(tmp26); +//│ return +⁰(tmp27, 1) //│ }; -//│ set inlinedVal8 = lambda⁰; +//│ set inlinedVal18 = lambda⁰; //│ break inlinedLbl1 -//│ set tmp12 = inlinedVal8; -//│ set tmp13 = -⁰(caseScrut3, 1); -//│ set tmp14 = tmp12(tmp13); -//│ return +⁰(tmp14, 1) +//│ set tmp22 = inlinedVal18; +//│ set tmp23 = -⁰(caseScrut3, 1); +//│ set tmp24 = tmp22(tmp23); +//│ return +⁰(tmp24, 1) //│ }; -//│ set inlinedVal7 = lambda¹; -//│ set tmp9 = lambda¹; -//│ set tmp10 = -⁰(caseScrut2, 1); -//│ set tmp11 = lambda¹(tmp10); -//│ set inlinedVal6 = +⁰(tmp11, 1); +//│ set inlinedVal17 = lambda¹; +//│ set tmp19 = lambda¹; +//│ set tmp20 = -⁰(caseScrut2, 1); +//│ set tmp21 = lambda¹(tmp20); +//│ set inlinedVal16 = +⁰(tmp21, 1); //│ break inlinedLbl -//│ set tmp8 = inlinedVal6; -//│ set inlinedVal5 = +⁰(tmp8, 1); -//│ set tmp6 = inlinedVal5; +//│ set tmp18 = inlinedVal16; +//│ set inlinedVal15 = +⁰(tmp18, 1); +//│ set tmp16 = inlinedVal15; +//│ set inlinedVal14 = +⁰(inlinedVal15, 1); +//│ set inlinedVal13 = +⁰(inlinedVal14, 1); +//│ set inlinedVal12 = +⁰(inlinedVal13, 1); +//│ set inlinedVal11 = +⁰(inlinedVal12, 1); +//│ set inlinedVal10 = +⁰(inlinedVal11, 1); +//│ set inlinedVal9 = +⁰(inlinedVal10, 1); +//│ set inlinedVal8 = +⁰(inlinedVal9, 1); +//│ set inlinedVal7 = +⁰(inlinedVal8, 1); +//│ set inlinedVal6 = +⁰(inlinedVal7, 1); +//│ set inlinedVal5 = +⁰(inlinedVal6, 1); //│ set inlinedVal4 = +⁰(inlinedVal5, 1); //│ set inlinedVal3 = +⁰(inlinedVal4, 1); //│ set inlinedVal2 = +⁰(inlinedVal3, 1); From f9df9eda81d4897175a5b88ff8e1e41a78725084 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Sun, 5 Jul 2026 22:00:26 +0800 Subject: [PATCH 13/15] Address PR review --- .../main/scala/hkmc2/codegen/TailRecOpt.scala | 4 ++-- .../src/test/mlscript/tailrec/Annots.mls | 2 +- .../src/test/mlscript/tailrec/Errors.mls | 22 +++++++++---------- .../src/test/mlscript/tailrec/Simple.mls | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index e9f0013a53..0275c49698 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -273,7 +273,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): if calls.isEmpty then for f <- funs if checkAnnotations && f.tailRec do - raise(WarningReport(msg"This function is marked @tailrec but has no apparent tail calls." -> f.dSym.toLoc :: Nil)) + raise(WarningReport(msg"This function is marked @tailrec but has no direct self-recursion." -> f.dSym.toLoc :: Nil)) return (N, funs) if !nonTailCalls.isEmpty then @@ -283,7 +283,7 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): case Some(value) => value.toLoc case None => nonTailCalls.head._2.toLoc raise(ErrorReport( - msg"This function is not tail recursive." -> f.dSym.toLoc + msg"This function is marked @tailrec but is not tail recursive." -> f.dSym.toLoc :: msg"It could self-recurse through this call, which is not a tail call." -> reportLoc :: Nil )) diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls b/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls index 9f28be139d..30e09e83cf 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Annots.mls @@ -62,7 +62,7 @@ class A module A with @tailrec fun f = 2 -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.64: fun f = 2 //│ ╙── ^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls index ca48d7befa..ba9d364a61 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Errors.mls @@ -21,7 +21,7 @@ fun g(x) = @tailcall f(x) @tailrec fun f(x) = g(x) fun g(x) = f(x); f(x) -//│ ╔══[COMPILATION ERROR] This function is not tail recursive. +//│ ╔══[COMPILATION ERROR] This function is marked @tailrec but is not tail recursive. //│ ║ l.21: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. @@ -36,7 +36,7 @@ fun g(x) = h(x) fun h(x) = g(x) -//│ ╔══[COMPILATION ERROR] This function is not tail recursive. +//│ ╔══[COMPILATION ERROR] This function is marked @tailrec but is not tail recursive. //│ ║ l.32: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. @@ -51,7 +51,7 @@ fun g(x) = f(x) fun h(x) = f(x) -//│ ╔══[COMPILATION ERROR] This function is not tail recursive. +//│ ╔══[COMPILATION ERROR] This function is marked @tailrec but is not tail recursive. //│ ║ l.47: @tailrec fun f(x) = //│ ║ ^ //│ ╟── It could self-recurse through this call, which is not a tail call. @@ -71,7 +71,7 @@ module A with :w @tailrec fun f = 2 -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.73: fun f = 2 //│ ╙── ^ @@ -79,7 +79,7 @@ fun f = 2 module A with @tailrec fun f() = 2 -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.81: fun f() = 2 //│ ╙── ^ @@ -88,7 +88,7 @@ module A with fun foo() = Foo.bar() module Foo with fun bar() = foo() -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.88: fun foo() = Foo.bar() //│ ╙── ^^^ @@ -117,7 +117,7 @@ fun scan(idx, acc) = @tailrec fun go(idx, tok) = scan(idx, tok) go(idx, acc) -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.116: fun scan(idx, acc) = //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] This @tailrec function was not processed by the tail-call optimizer. @@ -163,7 +163,7 @@ fun g(x)(y) = module M with @tailrec fun f = print("ok"); f -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.165: fun f = print("ok"); f //│ ╙── ^ @@ -171,7 +171,7 @@ module M with module M with @tailrec fun f = case 0 then f(1) -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.173: fun f = case 0 then f(1) //│ ╙── ^ @@ -180,7 +180,7 @@ module M with @tailrec fun foo(x) = foo(1) + 1 -//│ ╔══[COMPILATION ERROR] This function is not tail recursive. +//│ ╔══[COMPILATION ERROR] This function is marked @tailrec but is not tail recursive. //│ ║ l.181: fun foo(x) = //│ ║ ^^^ //│ ╟── It could self-recurse through this call, which is not a tail call. @@ -191,7 +191,7 @@ fun foo(x) = @tailrec fun foo(x) = 1 |> foo -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.192: fun foo(x) = //│ ╙── ^^^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls index 8f48b2ac70..371a1dac5e 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls @@ -13,7 +13,7 @@ fun f = f module Foo with @tailrec fun bar = bar -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.15: fun bar = bar //│ ╙── ^^^ @@ -47,7 +47,7 @@ module A with fun f(x) = B.g(x + 1) module B with fun g(x) = A.f(x * 2) -//│ ╔══[WARNING] This function is marked @tailrec but has no apparent tail calls. +//│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. //│ ║ l.47: fun f(x) = B.g(x + 1) //│ ╙── ^ From db556d4e91af1fee3e20dffc23c08f80cdb2501f Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Sun, 5 Jul 2026 22:06:05 +0800 Subject: [PATCH 14/15] Tweak --- hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index 0275c49698..2a8426b863 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -272,12 +272,12 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): val nonTailCalls = nonTailCallsLs.toMap if calls.isEmpty then - for f <- funs if checkAnnotations && f.tailRec do + if checkAnnotations then for f <- funs if f.tailRec do raise(WarningReport(msg"This function is marked @tailrec but has no direct self-recursion." -> f.dSym.toLoc :: Nil)) return (N, funs) if !nonTailCalls.isEmpty then - for f <- funs if checkAnnotations && f.tailRec do + if checkAnnotations then for f <- funs if f.tailRec do val reportLoc = nonTailCalls.get(f.dSym) match // always display a call to f, if possible case Some(value) => value.toLoc From ba410f0ce7d7df8055abb587c9323fe0227506a7 Mon Sep 17 00:00:00 2001 From: Lionel Parreaux Date: Sun, 5 Jul 2026 22:11:38 +0800 Subject: [PATCH 15/15] Revert sneaky Codex change --- .../main/scala/hkmc2/codegen/TailRecOpt.scala | 1 - .../src/test/mlscript/tailrec/Simple.mls | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index 2a8426b863..6a3d717a36 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -287,7 +287,6 @@ class TailRecOpt(checkAnnotations: Bool)(using State, TL, Raise): :: msg"It could self-recurse through this call, which is not a tail call." -> reportLoc :: Nil )) - return (N, funs) val maxParamLen = maxInt(funs, paramsLen) val paramSyms = diff --git a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls index 371a1dac5e..9f44db8584 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/Simple.mls @@ -26,15 +26,20 @@ module Foo with fun f(x) = f(x + 1) -// The inner self-call in this source expression is evaluated before the final -// call and becomes a normal recursive edge in ANF. TailRecOpt must therefore -// leave the function unrewritten: optimizing only the final call would still -// keep one stack-consuming recursive call and would silently change the shape -// of the generated code without making the function tail-recursive. +// * We can still optimize functions that have only some of their calls in tail positions. :sjs fun f(x) = f(f(x) + 1) //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let f3; f3 = function f(x) { let tmp, tmp1; tmp = f3(x); tmp1 = tmp + 1; return f3(tmp1) }; +//│ let f3; +//│ f3 = function f(x) { +//│ loopLabel: while (true) { +//│ let tmp, tmp1; +//│ tmp = f3(x); +//│ tmp1 = tmp + 1; +//│ x = tmp1; +//│ continue loopLabel; +//│ } +//│ }; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— module A with @@ -48,6 +53,6 @@ module A with module B with fun g(x) = A.f(x * 2) //│ ╔══[WARNING] This function is marked @tailrec but has no direct self-recursion. -//│ ║ l.47: fun f(x) = B.g(x + 1) +//│ ║ l.52: fun f(x) = B.g(x + 1) //│ ╙── ^