From aeb6bf6a9fa289eb2a95313d487ab9af9a94707c Mon Sep 17 00:00:00 2001 From: Yura Lazarev Date: Fri, 24 Jul 2026 11:27:49 +0200 Subject: [PATCH 1/2] feat(optimizer): fold a trailing call through a scope call --- ...000_unisay_fold_call_through_scope_call.md | 28 +++ .../PureScript/Backend/Lua/Optimizer.hs | 176 ++++++++++++-- .../PureScript/Backend/Lua/Optimizer/Spec.hs | 222 ++++++++++++++++++ .../Golden.ArrayOfUnits.Test/golden.lua | 22 +- .../Golden.BugListGenericEq.Test/golden.lua | 48 ++-- test/ps/output/Golden.Issue37.Test/golden.lua | 12 +- .../Golden.LongApplyChain.Test/golden.lua | 30 ++- .../Golden.LongEitherBind.Test/golden.lua | 4 +- .../Golden.LongExceptBind.Test/golden.lua | 4 +- .../Golden.LongStackBind.Test/golden.lua | 4 +- .../Golden.LongWriterBind.Test/golden.lua | 30 ++- .../Golden.RecGroupOrder.Test/golden.lua | 4 +- .../Golden.StringCodePoints.Test/golden.lua | 6 +- .../Golden.TailRecM2Shadow.Test/golden.lua | 20 +- .../output/Golden.UncurryCtor.Test/golden.lua | 32 ++- .../Golden.UncurryEffect.Test/golden.lua | 12 +- 16 files changed, 514 insertions(+), 140 deletions(-) create mode 100644 changelog.d/20260724_140000_unisay_fold_call_through_scope_call.md diff --git a/changelog.d/20260724_140000_unisay_fold_call_through_scope_call.md b/changelog.d/20260724_140000_unisay_fold_call_through_scope_call.md new file mode 100644 index 00000000..0b2e9261 --- /dev/null +++ b/changelog.d/20260724_140000_unisay_fold_call_through_scope_call.md @@ -0,0 +1,28 @@ +### Added + +- A call applied to the result of an IIFE folds into its returns. The + "pick a thunk, then run it" codegen shape — a scope call immediately + applied, `(function() … end)()()` or `(function() … end)()(b)` — kept a + closure allocation and an extra call per invocation that the + tail-position collapse could not touch, because the applied result, not + the scope call itself, sat in tail position. The new + `foldCallThroughScopeCall` rule rewrites + `(function() …; return e end)()(args)` to + `(function() …; return e(args) end)()`, pushing the application into the + tail returns — through a branching tail (`if … then return f else return + g end`) as well, where the thunk selection lives. Evaluation order is + preserved (leading statements, then the returned expression, then the + arguments, in both forms), and `return e(args)` is a tail call, so even + the activation depth at the moment the result runs is unchanged. The + rule declines on early returns, fall-off paths (the original would call + `nil` and error), multi-valued returns, arguments whose free names a + body local would capture, varargs among the arguments, and non-atomic + arguments on a branching tail (they would be syntactically duplicated + per return site). The exposed plain scope call is then spliced away by + `collapseTailScopeCall`, which now re-applies itself to the merged + body — the fold builds tails at depths the bottom-up driver has already + passed. Thirteen goldens shrink: `Golden.UncurryEffect.Test`'s + `countdown` loses both tail closures, `Golden.TailRecM2Shadow.Test`'s + `untilE` predicate drops its per-iteration selection closure, and the + long-bind family runs its final effect directly. Eval goldens are + unchanged. diff --git a/lib/Language/PureScript/Backend/Lua/Optimizer.hs b/lib/Language/PureScript/Backend/Lua/Optimizer.hs index 1eea78be..a6c0f5a4 100644 --- a/lib/Language/PureScript/Backend/Lua/Optimizer.hs +++ b/lib/Language/PureScript/Backend/Lua/Optimizer.hs @@ -2,10 +2,11 @@ module Language.PureScript.Backend.Lua.Optimizer where import Control.Monad.Trans.Accum (Accum, add, execAccum) import Data.Map qualified as Map +import Data.Set qualified as Set import Language.PureScript.Backend.Lua.Fixture qualified as Fixture import Language.PureScript.Backend.Lua.Limits (LuaLimits, workingLocalCeiling) import Language.PureScript.Backend.Lua.Linker.Foreign (chunkScopeUsesVararg) -import Language.PureScript.Backend.Lua.Localize (localizeChunk) +import Language.PureScript.Backend.Lua.Localize (localizeChunk, namesInBlock) import Language.PureScript.Backend.Lua.Name qualified as Lua import Language.PureScript.Backend.Lua.Promote (promoteChunk) import Language.PureScript.Backend.Lua.Traversal @@ -69,6 +70,7 @@ rewriteRulesInOrder ∷ LuaLimits → [RewriteRule] rewriteRulesInOrder limits = [ reduceTableDefinitionAccessor , foldFieldProjectionThroughScopeCall limits + , foldCallThroughScopeCall , collapseTailScopeCall limits , foldNotEqual ] @@ -170,23 +172,152 @@ foldFieldProjectionThroughScopeCall limits original _ → Nothing _ → Nothing - containsReturn ∷ Annotated Comments StatementF → Bool - containsReturn (Ann statement) = case statement of - Return {} → True +{- | Whether a statement contains a 'Return' at the level of the enclosing +function body — one that exits the activation. A 'Return' inside a nested +'Function' (or 'LocalFunction') belongs to a different activation and does +not count, while a 'Return' inside a loop or 'Do' block at body level does. +-} +containsReturn ∷ Annotated Comments StatementF → Bool +containsReturn (Ann statement) = case statement of + Return {} → True + IfThenElse _predicate thenBlock elseBlock → + any containsReturn thenBlock || any containsReturn elseBlock + Do body → any containsReturn body + While _predicate body → any containsReturn body + Repeat body _predicate → any containsReturn body + ForNum _name _start _limit _step body → any containsReturn body + ForIn _names _exprs body → any containsReturn body + LocalFunction {} → False + Assign {} → False + Local {} → False + CallStatement {} → False + Break → False + +{- | Rewrites @(function() …; return e end)()(args)@ to +@(function() …; return e(args) end)()@: a call applied to the result of a +no-argument, immediately-invoked function is folded into its @return@s. +The shape is how the code generator runs a selected thunk — "pick an +effect, then run it" lowers to a scope call immediately applied — and +folding the application inward exposes a plain scope call that +'collapseTailScopeCall' can then splice away. The freshly built call is +folded once more by this same rule, in case the returned expression is +itself an applied scope call. + +Applying before versus after the call returns is observably the same: the +leading statements run first either way, the returned expression is +evaluated before the arguments in both forms, and @return e(args)@ is a +tail call, so even the activation depth at the moment the result runs is +unchanged. + +The tail chain the fold covers is a single-valued 'Return', or an +'IfThenElse'/'Do' whose every branch ends in one, recursively — the thunk +selection is exactly a branching tail. The rule declines when: + +* a leading statement (of the body or of any branch on the tail chain) + 'containsReturn': such an early return leaves the call on a path the + fold does not cover; + +* the tail chain has a fall-off path (a branch not ending in a 'Return', + an empty body): falling off yields @nil@, which the original code then + calls — an error the folded code would not reproduce; + +* the tail 'Return' is not single-valued: the call consumes the first + value only after Lua's adjustment, but the fold cannot drop the other + results without dropping their effects; + +* the arguments mention @...@ in their own scope: moved inside the + no-parameter callee, it would be rebound or fail to load; + +* a free name of the arguments collides with a local the callee's body + declares ('declaredNamesInActivation'): moved inside, the argument + would resolve the name to the callee's local instead of the enclosing + scope's binding; + +* the tail chain branches and any argument is not a name or a literal: + the arguments are duplicated into every return site — evaluated at most + once, since a single branch runs, but syntactically repeated — and + atoms keep that duplication trivial. +-} +foldCallThroughScopeCall ∷ RewriteRule +foldCallThroughScopeCall = \case + original@( FunctionCall + (Ann (FunctionCall (Ann (Function [] body)) [])) + args + ) + | not (chunkScopeUsesVararg argsBlock) + , Set.disjoint (declaredNamesInActivation body) (namesInBlock argsBlock) + , Just body' ← pushCallIntoTail body → + FunctionCall (Lua.ann (Function [] body')) [] + | otherwise → original + where + argsBlock = [Lua.ann (Return args)] + + atomicArgs ∷ Bool + atomicArgs = all (isAtom . Lua.unAnn) args + where + isAtom ∷ Exp → Bool + isAtom = \case + Nil → True + Boolean _ → True + Integer _ → True + Float _ → True + String _ → True + Var (Ann (VarName _)) → True + _ → False + + pushCallIntoTail + ∷ [Annotated Comments StatementF] + → Maybe [Annotated Comments StatementF] + pushCallIntoTail block = case reverse block of + lastStatement : reverseLeading + | not (any containsReturn reverseLeading) → + pushIntoStatement lastStatement <&> \pushed → + reverse reverseLeading <> [pushed] + _ → Nothing + + pushIntoStatement + ∷ Annotated Comments StatementF + → Maybe (Annotated Comments StatementF) + pushIntoStatement (c, statement) = + (c,) <$> case statement of + Return [returnedValue] → + Just . Return . pure . Lua.ann $ + foldCallThroughScopeCall (FunctionCall returnedValue args) + IfThenElse p thenBlock elseBlock + | atomicArgs → + IfThenElse p + <$> pushCallIntoTail thenBlock + <*> pushCallIntoTail elseBlock + Do doBody → Do <$> pushCallIntoTail doBody + _ → Nothing + e → e + +{- | Every name declared at the activation level of a block: block-level +declarations at any depth count, while nested function literals are +separate scopes whose declarations are invisible outside. The name-set +counterpart of 'activationLocalSlots'. +-} +declaredNamesInActivation ∷ [Annotated Comments StatementF] → Set Lua.Name +declaredNamesInActivation = foldMap (declared . Lua.unAnn) + where + declared ∷ StatementF Comments → Set Lua.Name + declared = \case + Local names _values → Set.fromList (toList names) + LocalFunction fname _params _body → Set.singleton fname + ForNum n _start _limit _step body → + Set.insert n (declaredNamesInActivation body) + ForIn names _exprs body → + Set.fromList (toList names) <> declaredNamesInActivation body IfThenElse _predicate thenBlock elseBlock → - any containsReturn thenBlock || any containsReturn elseBlock - Do body → any containsReturn body - While _predicate body → any containsReturn body - Repeat body _predicate → any containsReturn body - ForNum _name _start _limit _step body → any containsReturn body - ForIn _names _exprs body → any containsReturn body - -- A nested (local) function is a different activation: its returns do - -- not exit this call. - LocalFunction {} → False - Assign {} → False - Local {} → False - CallStatement {} → False - Break → False + declaredNamesInActivation thenBlock + <> declaredNamesInActivation elseBlock + Do body → declaredNamesInActivation body + While _predicate body → declaredNamesInActivation body + Repeat body _predicate → declaredNamesInActivation body + Assign {} → mempty + Return {} → mempty + CallStatement {} → mempty + Break → mempty {- | Rewrites @function(…) …; return (function() end)() end@ to @function(…) …; end@: a no-argument, immediately-invoked function @@ -226,6 +357,11 @@ Conditions checked: 'localizeChunk' budgets its cache locals against what is actually declared after the splice, and only gains upvalue headroom from the disappearing proto. + +The rule re-applies itself to the merged function: the new tail may again +be a scope-call return — 'foldCallThroughScopeCall' builds such tails at +depths the bottom-up driver has already passed — and every round +re-checks the conditions above. -} collapseTailScopeCall ∷ LuaLimits → RewriteRule collapseTailScopeCall limits = \case @@ -235,7 +371,11 @@ collapseTailScopeCall limits = \case , let merged = leading <> spliced , length params + activationLocalSlots merged <= workingLocalCeiling limits → - Function params merged + -- The merged tail may itself be a scope-call return — one + -- 'foldCallThroughScopeCall' built after the bottom-up driver had + -- already passed that depth — so keep collapsing while the budget + -- admits it; every round consumes one nesting level. + collapseTailScopeCall limits (Function params merged) e → e where -- Splits a function body whose last statement is a single-valued diff --git a/test/Language/PureScript/Backend/Lua/Optimizer/Spec.hs b/test/Language/PureScript/Backend/Lua/Optimizer/Spec.hs index 49f75842..014809a9 100644 --- a/test/Language/PureScript/Backend/Lua/Optimizer/Spec.hs +++ b/test/Language/PureScript/Backend/Lua/Optimizer/Spec.hs @@ -6,8 +6,10 @@ import Language.PureScript.Backend.Lua.Limits (LuaLimits (..), lua51Limits) import Language.PureScript.Backend.Lua.Name (name) import Language.PureScript.Backend.Lua.Optimizer ( collapseTailScopeCall + , foldCallThroughScopeCall , foldFieldProjectionThroughScopeCall , foldNotEqual + , optimizeStatement , reduceTableDefinitionAccessor , rewriteExpWithRule ) @@ -186,6 +188,202 @@ spec = describe "Lua AST Optimizer" do assertEqual (toString $ pShow original) expected $ rewriteExpWithRule (foldFieldProjectionThroughScopeCall lua51Limits) original + describe "foldCallThroughScopeCall" do + it "folds a trailing call into a plain-return scope call" do + let original ∷ Lua.Exp = + Lua.functionCall + ( Lua.scope + [ Lua.local1 [name|v|] (Lua.varName [name|g|]) + , Lua.return (Lua.varName [name|f|]) + ] + ) + [Lua.varName [name|b|]] + expected ∷ Lua.Exp = + Lua.scope + [ Lua.local1 [name|v|] (Lua.varName [name|g|]) + , Lua.return + (Lua.functionCall (Lua.varName [name|f|]) [Lua.varName [name|b|]]) + ] + assertEqual (toString $ pShow original) expected $ + rewriteExpWithRule foldCallThroughScopeCall original + + it "folds a zero-argument run into both branches of a tail if" do + -- The thunk-selection shape: `(function() if c then return f else + -- return g end end)()()` picks an effect and immediately runs it. + let original ∷ Lua.Exp = + Lua.functionCall + ( Lua.scope + [ Lua.ifThenElse + (Lua.varName [name|c|]) + [Lua.return (Lua.varName [name|f|])] + [Lua.return (Lua.varName [name|g|])] + ] + ) + [] + expected ∷ Lua.Exp = + Lua.scope + [ Lua.ifThenElse + (Lua.varName [name|c|]) + [Lua.return (Lua.functionCall (Lua.varName [name|f|]) [])] + [Lua.return (Lua.functionCall (Lua.varName [name|g|]) [])] + ] + assertEqual (toString $ pShow original) expected $ + rewriteExpWithRule foldCallThroughScopeCall original + + it "re-folds when the returned expression is itself a scope call" do + let original ∷ Lua.Exp = + Lua.functionCall + (Lua.scope [Lua.return (Lua.scope [Lua.return (Lua.varName [name|f|])])]) + [Lua.varName [name|b|]] + expected ∷ Lua.Exp = + Lua.scope + [ Lua.return + ( Lua.scope + [ Lua.return + ( Lua.functionCall + (Lua.varName [name|f|]) + [Lua.varName [name|b|]] + ) + ] + ) + ] + assertEqual (toString $ pShow original) expected $ + rewriteExpWithRule foldCallThroughScopeCall original + + it "declines when a leading statement contains an early return" do + let original ∷ Lua.Exp = + Lua.functionCall + ( Lua.scope + [ Lua.ifThenElse + (Lua.varName [name|c|]) + [Lua.return (Lua.varName [name|f|])] + [] + , Lua.return (Lua.varName [name|g|]) + ] + ) + [] + assertEqual (toString $ pShow original) original $ + rewriteExpWithRule foldCallThroughScopeCall original + + it "declines on a fall-off path (tail if without an else)" do + -- Falling off the end yields nil, which the original code then + -- calls (an error); a partial fold would return nil instead. + let original ∷ Lua.Exp = + Lua.functionCall + ( Lua.scope + [ Lua.ifThenElse + (Lua.varName [name|c|]) + [Lua.return (Lua.varName [name|f|])] + [] + ] + ) + [] + assertEqual (toString $ pShow original) original $ + rewriteExpWithRule foldCallThroughScopeCall original + + it "declines on a multi-valued tail return" do + let original ∷ Lua.Exp = + Lua.functionCall + ( Lua.scope + [Lua.returnN (Lua.varName [name|f|] :| [Lua.varName [name|g|]])] + ) + [] + assertEqual (toString $ pShow original) original $ + rewriteExpWithRule foldCallThroughScopeCall original + + it "declines when an argument name collides with a body local" do + -- Moved inside, `x` would resolve to the callee's local, not the + -- enclosing scope's binding. + let original ∷ Lua.Exp = + Lua.functionCall + ( Lua.scope + [ Lua.local1 [name|x|] (Lua.Integer 1) + , Lua.return (Lua.varName [name|f|]) + ] + ) + [Lua.varName [name|x|]] + assertEqual (toString $ pShow original) original $ + rewriteExpWithRule foldCallThroughScopeCall original + + it "declines non-atomic arguments on a branching tail" do + -- Branch pushing duplicates the arguments into every return site; + -- only names and literals keep that duplication trivial. + let scopeCall ∷ Lua.Exp = + Lua.scope + [ Lua.ifThenElse + (Lua.varName [name|c|]) + [Lua.return (Lua.varName [name|f|])] + [Lua.return (Lua.varName [name|g|])] + ] + nonAtomic ∷ Lua.Exp = + Lua.functionCall + scopeCall + [Lua.functionCall (Lua.varName [name|h|]) []] + atomic ∷ Lua.Exp = + Lua.functionCall scopeCall [Lua.varName [name|b|]] + pushed ∷ Lua.Exp = + Lua.scope + [ Lua.ifThenElse + (Lua.varName [name|c|]) + [ Lua.return + ( Lua.functionCall + (Lua.varName [name|f|]) + [Lua.varName [name|b|]] + ) + ] + [ Lua.return + ( Lua.functionCall + (Lua.varName [name|g|]) + [Lua.varName [name|b|]] + ) + ] + ] + assertEqual (toString $ pShow nonAtomic) nonAtomic $ + rewriteExpWithRule foldCallThroughScopeCall nonAtomic + assertEqual (toString $ pShow atomic) pushed $ + rewriteExpWithRule foldCallThroughScopeCall atomic + + it "declines varargs among the arguments" do + let original ∷ Lua.Exp = + Lua.functionCall + (Lua.scope [Lua.return (Lua.varName [name|f|])]) + [Lua.Vararg] + assertEqual (toString $ pShow original) original $ + rewriteExpWithRule foldCallThroughScopeCall original + + it "composes with collapseTailScopeCall into a zero-closure tail" do + -- The end-to-end #230-family result: `return (function() if c then + -- return f else return g end end)()()` in a function tail loses + -- both the closure and the extra calls. + let worker ∷ [Lua.Statement] → Lua.Statement + worker body = + Lua.local1 + [name|w|] + (Lua.functionDef [Lua.ParamNamed [name|n|]] body) + original = + worker + [ Lua.return + ( Lua.functionCall + ( Lua.scope + [ Lua.ifThenElse + (Lua.varName [name|c|]) + [Lua.return (Lua.varName [name|f|])] + [Lua.return (Lua.varName [name|g|])] + ] + ) + [] + ) + ] + expected = + worker + [ Lua.ifThenElse + (Lua.varName [name|c|]) + [Lua.return (Lua.functionCall (Lua.varName [name|f|]) [])] + [Lua.return (Lua.functionCall (Lua.varName [name|g|]) [])] + ] + assertEqual (toString $ pShow original) expected $ + optimizeStatement lua51Limits original + describe "collapseTailScopeCall" do it "splices a tail scope call into the enclosing function body" do -- The residual magic-do shape from issue #230: an effectful @@ -211,6 +409,30 @@ spec = describe "Lua AST Optimizer" do assertEqual (toString $ pShow original) expected $ rewriteExpWithRule (collapseTailScopeCall lua51Limits) original + it "re-collapses a tail exposed by its own merge, without a traversal" do + -- The bare rule (no bottom-up driver) must flatten both levels: + -- 'foldCallThroughScopeCall' builds nested tails at depths the + -- driver has already passed, so the rule cannot rely on it. + let original ∷ Lua.Exp = + Lua.functionDef + [] + [ Lua.return + ( Lua.scope + [ Lua.local1 [name|a|] (Lua.Integer 1) + , Lua.return + (Lua.scope [Lua.return (Lua.varName [name|a|])]) + ] + ) + ] + expected ∷ Lua.Exp = + Lua.functionDef + [] + [ Lua.local1 [name|a|] (Lua.Integer 1) + , Lua.return (Lua.varName [name|a|]) + ] + assertEqual (toString $ pShow original) expected $ + collapseTailScopeCall lua51Limits original + it "splices nested tail scope calls bottom-up in one pass" do let original ∷ Lua.Exp = Lua.functionDef diff --git a/test/ps/output/Golden.ArrayOfUnits.Test/golden.lua b/test/ps/output/Golden.ArrayOfUnits.Test/golden.lua index f329e07f..928765b6 100644 --- a/test/ps/output/Golden.ArrayOfUnits.Test/golden.lua +++ b/test/ps/output/Golden.ArrayOfUnits.Test/golden.lua @@ -115,18 +115,16 @@ return (function() [2] = Data_Unit_unit, [3] = Data_Unit_unit } - return function() + return (function() local _ = Data_Foldable_foldableArray.foldr(function(x_S_940) - return (function() - local dictApply_S_925 = Effect_applicativeEffect.Apply0() - return function(a_S_926) - return function(b_S_927) - return dictApply_S_925.apply((dictApply_S_925.Functor0()).map(function( ) - return function(x_S_934) return x_S_934 end - end)(a_S_926))(b_S_927) - end + local dictApply_S_925 = Effect_applicativeEffect.Apply0() + return (function(a_S_926) + return function(b_S_927) + return dictApply_S_925.apply((dictApply_S_925.Functor0()).map(function( ) + return function(x_S_934) return x_S_934 end + end)(a_S_926))(b_S_927) end - end)()(Effect_Console_logShow_S_w({ + end)(Effect_Console_logShow_S_w({ show = function() return "unit" end }, x_S_940)) end)(Effect_pureE(Data_Unit_unit))(arr_S_0)() @@ -135,5 +133,5 @@ return (function() }, Data_Foldable_foldableArray.foldl(function(c_S_913) return function() return 1 + c_S_913 end end)(0)(arr_S_0))() - end -end)()() + end)() +end)() diff --git a/test/ps/output/Golden.BugListGenericEq.Test/golden.lua b/test/ps/output/Golden.BugListGenericEq.Test/golden.lua index b50c8e2f..d2d0b3e2 100644 --- a/test/ps/output/Golden.BugListGenericEq.Test/golden.lua +++ b/test/ps/output/Golden.BugListGenericEq.Test/golden.lua @@ -68,32 +68,30 @@ Golden_BugListGenericEq_Test_eqList = function(dictEq) eq = function(x) return function(y) local _S_cse268 = { "Data.Generic.Rep∷Sum.Inl", {} } - return (function() - local v_S_13 = (function() - if "Golden.BugListGenericEq.Test∷List.Nil" == x[1] then - return _S_cse268 - else - return { "Data.Generic.Rep∷Sum.Inr", x[2] } - end - end)() - return function(v1_S_14) - local _S_cse270 = v1_S_14[1] - local _S_cse269 = v_S_13[1] - if "Data.Generic.Rep∷Sum.Inl" == _S_cse269 then - return "Data.Generic.Rep∷Sum.Inl" == _S_cse270 - else - return "Data.Generic.Rep∷Sum.Inr" == _S_cse269 and ("Data.Generic.Rep∷Sum.Inr" == _S_cse270 and (Data_Eq_eqRowCons_S_w(Data_Eq_eqRowCons_S_w({ - eqRecord = function() - return function() return function() return true end end - end - }, nil, { - reflectSymbol = function() return "tail" end - }, Golden_BugListGenericEq_Test_eqList(dictEq)), nil, { - reflectSymbol = function() return "head" end - }, dictEq)).eqRecord(Type_Proxy_Proxy)(v_S_13[2])(v1_S_14[2])) - end + local v_S_13 = (function() + if "Golden.BugListGenericEq.Test∷List.Nil" == x[1] then + return _S_cse268 + else + return { "Data.Generic.Rep∷Sum.Inr", x[2] } + end + end)() + return (function(v1_S_14) + local _S_cse270 = v1_S_14[1] + local _S_cse269 = v_S_13[1] + if "Data.Generic.Rep∷Sum.Inl" == _S_cse269 then + return "Data.Generic.Rep∷Sum.Inl" == _S_cse270 + else + return "Data.Generic.Rep∷Sum.Inr" == _S_cse269 and ("Data.Generic.Rep∷Sum.Inr" == _S_cse270 and (Data_Eq_eqRowCons_S_w(Data_Eq_eqRowCons_S_w({ + eqRecord = function() + return function() return function() return true end end + end + }, nil, { + reflectSymbol = function() return "tail" end + }, Golden_BugListGenericEq_Test_eqList(dictEq)), nil, { + reflectSymbol = function() return "head" end + }, dictEq)).eqRecord(Type_Proxy_Proxy)(v_S_13[2])(v1_S_14[2])) end - end)()((function() + end)((function() if "Golden.BugListGenericEq.Test∷List.Nil" == y[1] then return _S_cse268 else diff --git a/test/ps/output/Golden.Issue37.Test/golden.lua b/test/ps/output/Golden.Issue37.Test/golden.lua index bfd00469..68778909 100644 --- a/test/ps/output/Golden.Issue37.Test/golden.lua +++ b/test/ps/output/Golden.Issue37.Test/golden.lua @@ -70,12 +70,12 @@ return { baz = (function() local Bind1_S_1 = Effect_monadEffect.Bind1() local pure_S_4 = (Effect_monadEffect.Applicative0()).pure - return function(f_S_6) + return (function(f_S_6) return Bind1_S_1.bind(f_S_6)(function() return Bind1_S_1.bind(pure_S_4({ [1] = (function() local Bind1_S_216 = Effect_monadEffect.Bind1() - return function(fn1_S_218) + return (function(fn1_S_218) return Bind1_S_216.bind(fn1_S_218)(function() return Bind1_S_216.bind(fn1_S_218)(function() return Bind1_S_216.bind(fn1_S_218)(function() @@ -83,10 +83,10 @@ return { end) end) end) - end - end)()(f_S_6) + end)(f_S_6) + end)() }))(function() return pure_S_4(Data_Unit_unit) end) end) - end - end)()(Effect_pureE(Data_Unit_unit)) + end)(Effect_pureE(Data_Unit_unit)) + end)() } diff --git a/test/ps/output/Golden.LongApplyChain.Test/golden.lua b/test/ps/output/Golden.LongApplyChain.Test/golden.lua index e153133c..ce689b43 100644 --- a/test/ps/output/Golden.LongApplyChain.Test/golden.lua +++ b/test/ps/output/Golden.LongApplyChain.Test/golden.lua @@ -4,26 +4,24 @@ local Effect_Console_foreign = { } local Data_Maybe_Nothing = { "Data.Maybe∷Maybe.Nothing" } local Golden_LongApplyChain_Test_applySecond_S_w = function(a_S_133, b_S_134) - return (function() - local v_S_625 = (function() - if "Data.Maybe∷Maybe.Just" == a_S_133[1] then - return { "Data.Maybe∷Maybe.Just", function(x_S_318) return x_S_318 end } - else - return Data_Maybe_Nothing - end - end)() - return function(v1_S_626) - if "Data.Maybe∷Maybe.Just" == v_S_625[1] then - if "Data.Maybe∷Maybe.Just" == v1_S_626[1] then - return { "Data.Maybe∷Maybe.Just", (v_S_625[2](v1_S_626[2])) } - else - return Data_Maybe_Nothing - end + local v_S_625 = (function() + if "Data.Maybe∷Maybe.Just" == a_S_133[1] then + return { "Data.Maybe∷Maybe.Just", function(x_S_318) return x_S_318 end } + else + return Data_Maybe_Nothing + end + end)() + return (function(v1_S_626) + if "Data.Maybe∷Maybe.Just" == v_S_625[1] then + if "Data.Maybe∷Maybe.Just" == v1_S_626[1] then + return { "Data.Maybe∷Maybe.Just", (v_S_625[2](v1_S_626[2])) } else return Data_Maybe_Nothing end + else + return Data_Maybe_Nothing end - end)()(b_S_134) + end)(b_S_134) end local Golden_LongApplyChain_Test_compute = (function() local _S_tmp646 = Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w(Golden_LongApplyChain_Test_applySecond_S_w({ diff --git a/test/ps/output/Golden.LongEitherBind.Test/golden.lua b/test/ps/output/Golden.LongEitherBind.Test/golden.lua index 39490733..d440f679 100644 --- a/test/ps/output/Golden.LongEitherBind.Test/golden.lua +++ b/test/ps/output/Golden.LongEitherBind.Test/golden.lua @@ -52,5 +52,5 @@ return (function() else return "(Right " .. Data_Show_foreign.showIntImpl(_S_cse2149) .. ")" end - end)()) -end)()() + end)())() +end)() diff --git a/test/ps/output/Golden.LongExceptBind.Test/golden.lua b/test/ps/output/Golden.LongExceptBind.Test/golden.lua index 4055d72c..7fd5e47d 100644 --- a/test/ps/output/Golden.LongExceptBind.Test/golden.lua +++ b/test/ps/output/Golden.LongExceptBind.Test/golden.lua @@ -1175,5 +1175,5 @@ return (function() else return "(Right " .. Data_Show_foreign.showIntImpl(_S_cse1412) .. ")" end - end)()) -end)()() + end)())() +end)() diff --git a/test/ps/output/Golden.LongStackBind.Test/golden.lua b/test/ps/output/Golden.LongStackBind.Test/golden.lua index da7a84a0..785ccc9c 100644 --- a/test/ps/output/Golden.LongStackBind.Test/golden.lua +++ b/test/ps/output/Golden.LongStackBind.Test/golden.lua @@ -873,5 +873,5 @@ return (function() else return "(Right " .. Data_Show_foreign.showIntImpl(_S_cse6553) .. ")" end - end)()) -end)()() + end)())() +end)() diff --git a/test/ps/output/Golden.LongWriterBind.Test/golden.lua b/test/ps/output/Golden.LongWriterBind.Test/golden.lua index 0fb4b42a..1253701d 100644 --- a/test/ps/output/Golden.LongWriterBind.Test/golden.lua +++ b/test/ps/output/Golden.LongWriterBind.Test/golden.lua @@ -201,24 +201,22 @@ local Golden_LongWriterBind_Test_go = (function() return Golden_LongWriterBind_Test_discard(Data_Tuple_Tuple_S_w(Data_Unit_unit, { [1] = 200 }))(function( ) - return ((function( ) - local dictMonoid_S_517 = { - mempty = {}, - Semigroup0 = function( ) - return Data_Semigroup_semigroupArray + local dictMonoid_S_517 = { + mempty = {}, + Semigroup0 = function( ) + return Data_Semigroup_semigroupArray + end + } + return ((function( dictApplicative_S_518 ) + return { + pure = function( a_S_519 ) + return dictApplicative_S_518.pure(Data_Tuple_Tuple_S_w(a_S_519, dictMonoid_S_517.mempty)) + end, + Apply0 = function( ) + return Control_Monad_Writer_Trans_applyWriterT_S_w(dictMonoid_S_517.Semigroup0(), dictApplicative_S_518.Apply0()) end } - return function( dictApplicative_S_518 ) - return { - pure = function( a_S_519 ) - return dictApplicative_S_518.pure(Data_Tuple_Tuple_S_w(a_S_519, dictMonoid_S_517.mempty)) - end, - Apply0 = function( ) - return Control_Monad_Writer_Trans_applyWriterT_S_w(dictMonoid_S_517.Semigroup0(), dictApplicative_S_518.Apply0()) - end - } - end - end)()(Data_Identity_applicativeIdentity)).pure(42) + end)(Data_Identity_applicativeIdentity)).pure(42) end) end) end) diff --git a/test/ps/output/Golden.RecGroupOrder.Test/golden.lua b/test/ps/output/Golden.RecGroupOrder.Test/golden.lua index 2a7034d6..7282a01c 100644 --- a/test/ps/output/Golden.RecGroupOrder.Test/golden.lua +++ b/test/ps/output/Golden.RecGroupOrder.Test/golden.lua @@ -32,5 +32,5 @@ return (function() return { run = function() return (Lazy_record_S_0(0)).tag end, tag = "ok!" } end) record_S_1 = Lazy_record_S_0(0) - return Effect_Console_foreign.log(record_S_1.run(Data_Unit_unit)) -end)()() + return Effect_Console_foreign.log(record_S_1.run(Data_Unit_unit))() +end)() diff --git a/test/ps/output/Golden.StringCodePoints.Test/golden.lua b/test/ps/output/Golden.StringCodePoints.Test/golden.lua index d3a51bdd..60423566 100644 --- a/test/ps/output/Golden.StringCodePoints.Test/golden.lua +++ b/test/ps/output/Golden.StringCodePoints.Test/golden.lua @@ -318,7 +318,7 @@ local Data_String_CodePoints_unsafeCodePointAt0 = Data_String_CodePoints_foreign local cu0_S_25 = Data_String_CodePoints_fromEnum(Data_String_Unsafe_charAt(0)(s_S_24)) if Data_String_CodePoints_conj(Data_String_CodePoints_conj(Data_Ord_lessThanOrEq_S_w(Data_Ord_ordInt, 55296, cu0_S_25))(Data_Ord_lessThanOrEq_S_w(Data_Ord_ordInt, cu0_S_25, 56319)))("Data.Ordering∷Ordering.GT" == ((function( ) local x_S_1600 = Data_String_CodeUnits_length(s_S_24) - return function(y_S_1601) + return (function(y_S_1601) if x_S_1600 < y_S_1601 then return Data_Ordering_LT elseif x_S_1600 == y_S_1601 then @@ -326,8 +326,8 @@ local Data_String_CodePoints_unsafeCodePointAt0 = Data_String_CodePoints_foreign else return Data_Ordering_GT end - end - end)()(1))[1]) then + end)(1) + end)())[1]) then local cu1_S_27 = Data_String_CodePoints_fromEnum(Data_String_Unsafe_charAt(1)(s_S_24)) if Data_String_CodePoints_conj(Data_Ord_lessThanOrEq_S_w(Data_Ord_ordInt, 56320, cu1_S_27))(Data_Ord_lessThanOrEq_S_w(Data_Ord_ordInt, cu1_S_27, 57343)) then return (cu0_S_25 - 55296) * 1024 + (cu1_S_27 - 56320) + 65536 diff --git a/test/ps/output/Golden.TailRecM2Shadow.Test/golden.lua b/test/ps/output/Golden.TailRecM2Shadow.Test/golden.lua index e69f6c82..7d6ca6dc 100644 --- a/test/ps/output/Golden.TailRecM2Shadow.Test/golden.lua +++ b/test/ps/output/Golden.TailRecM2Shadow.Test/golden.lua @@ -107,17 +107,15 @@ return (function() local r_S_16 = Effect_bindE(f_S_11(a_S_12))(Effect_Ref_foreign._new)() local _ = Effect_foreign.untilE(function() local v0_S_17 = Effect_Ref_read(r_S_16)() - return (function() - if "Control.Monad.Rec.Class∷Step.Loop" == v0_S_17[1] then - return function() - local e_S_19 = f_S_11(v0_S_17[2])() - local _ = Effect_Ref_foreign.write(e_S_19)(r_S_16)() - return false - end - else - return Effect_pureE(true) - end - end)()() + if "Control.Monad.Rec.Class∷Step.Loop" == v0_S_17[1] then + return (function() + local e_S_19 = f_S_11(v0_S_17[2])() + local _ = Effect_Ref_foreign.write(e_S_19)(r_S_16)() + return false + end)() + else + return Effect_pureE(true)() + end end)() return Effect_functorEffect.map(Partial_Unsafe_foreign._unsafePartial(function( ) return function(v_S_20) diff --git a/test/ps/output/Golden.UncurryCtor.Test/golden.lua b/test/ps/output/Golden.UncurryCtor.Test/golden.lua index c95f17db..ad17efb3 100644 --- a/test/ps/output/Golden.UncurryCtor.Test/golden.lua +++ b/test/ps/output/Golden.UncurryCtor.Test/golden.lua @@ -82,23 +82,19 @@ return (function() local _ = Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_area(Golden_UncurryCtor_Test_Origin))() local _ = Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_pairSum(Golden_UncurryCtor_Test_Pair_S_w(20, 22)))() local _ = Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_unbox(41))() - return (function() - local mk_S_0 = Golden_UncurryCtor_Test_Tri(1)(2) - return function() - local _ = Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_area(mk_S_0(3)))() - local _ = Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_area(mk_S_0(30)))() - local _ = Effect_Console_logShow_S_w({ - show = Data_Show_foreign.showArrayImpl(Data_Show_show(Data_Show_showInt)) - }, Data_Functor_arrayMap(function(v2_S_307) - if "Data.Maybe∷Maybe.Nothing" == v2_S_307[1] then - return 0 - else - return v2_S_307[2] - end - end)(Data_Functor_arrayMap(function(value0_S_308) - return { "Data.Maybe∷Maybe.Just", value0_S_308 } - end)({ [1] = 1, [2] = 2, [3] = 3 })))() - return Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_total(Golden_UncurryCtor_Test_range))() + local mk_S_0 = Golden_UncurryCtor_Test_Tri(1)(2) + local _ = Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_area(mk_S_0(3)))() + local _ = Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_area(mk_S_0(30)))() + local _ = Effect_Console_logShow_S_w({ + show = Data_Show_foreign.showArrayImpl(Data_Show_show(Data_Show_showInt)) + }, Data_Functor_arrayMap(function(v2_S_307) + if "Data.Maybe∷Maybe.Nothing" == v2_S_307[1] then + return 0 + else + return v2_S_307[2] end - end)()() + end)(Data_Functor_arrayMap(function(value0_S_308) + return { "Data.Maybe∷Maybe.Just", value0_S_308 } + end)({ [1] = 1, [2] = 2, [3] = 3 })))() + return Golden_UncurryCtor_Test_logShow(Golden_UncurryCtor_Test_total(Golden_UncurryCtor_Test_range))() end)() diff --git a/test/ps/output/Golden.UncurryEffect.Test/golden.lua b/test/ps/output/Golden.UncurryEffect.Test/golden.lua index 2be8d12c..e7c9f740 100644 --- a/test/ps/output/Golden.UncurryEffect.Test/golden.lua +++ b/test/ps/output/Golden.UncurryEffect.Test/golden.lua @@ -29,13 +29,11 @@ local Golden_UncurryEffect_Test_countdown_S_w = function(n) local _ = Effect_Console_log("tick")() return Effect_Console_log(Data_Show_showIntImpl(n))() end)() - return (function() - if not(n < 1) and n ~= 1 then - return Golden_UncurryEffect_Test_countdown(n - 1) - else - return Effect_Console_log("done") - end - end)()() + if not(n < 1) and n ~= 1 then + return Golden_UncurryEffect_Test_countdown(n - 1)() + else + return Effect_Console_log("done")() + end end Golden_UncurryEffect_Test_countdown = function(countdown_S_p1) return function(countdown_S_p2) From 7d87eb896f0b0b6f1aad0710fa6d484550b540ca Mon Sep 17 00:00:00 2001 From: Yura Lazarev Date: Fri, 24 Jul 2026 11:41:27 +0200 Subject: [PATCH 2/2] bench: accept EffectStep counter oracles after the trailing-call fold --- bench/goldens/fnew_Bench.EffectStep.txt | 13 ++++++------- bench/goldens/tnew_Bench.EffectStep.txt | 2 +- bench/goldens/trace_effect_step.txt | 8 +++----- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/bench/goldens/fnew_Bench.EffectStep.txt b/bench/goldens/fnew_Bench.EffectStep.txt index 4d54268a..c6be304a 100644 --- a/bench/goldens/fnew_Bench.EffectStep.txt +++ b/bench/goldens/fnew_Bench.EffectStep.txt @@ -1,9 +1,9 @@ chunk: Bench.EffectStep.lua runtime: LuaJIT 2.1.1741730670 main-chunk FNEW: 11 -function-body FNEW: 12 -total FNEW: 23 -prototypes: 24 +function-body FNEW: 11 +total FNEW: 22 +prototypes: 23 function-body FNEW sites: Bench.EffectStep.lua:4 Bench.EffectStep.lua:6 @@ -13,7 +13,6 @@ function-body FNEW sites: Bench.EffectStep.lua:12 Bench.EffectStep.lua:12 Bench.EffectStep.lua:33 - Bench.EffectStep.lua:46 - Bench.EffectStep.lua:45 - Bench.EffectStep.lua:49 - Bench.EffectStep.lua:58 + Bench.EffectStep.lua:44 + Bench.EffectStep.lua:47 + Bench.EffectStep.lua:56 diff --git a/bench/goldens/tnew_Bench.EffectStep.txt b/bench/goldens/tnew_Bench.EffectStep.txt index 170c065a..6867db9b 100644 --- a/bench/goldens/tnew_Bench.EffectStep.txt +++ b/bench/goldens/tnew_Bench.EffectStep.txt @@ -3,6 +3,6 @@ runtime: LuaJIT 2.1.1741730670 main-chunk TNEW+TDUP: 4 function-body TNEW+TDUP: 1 total TNEW+TDUP: 5 -prototypes: 24 +prototypes: 23 function-body TNEW+TDUP sites: Bench.EffectStep.lua:9 TDUP diff --git a/bench/goldens/trace_effect_step.txt b/bench/goldens/trace_effect_step.txt index 92f34fef..1072e677 100644 --- a/bench/goldens/trace_effect_step.txt +++ b/bench/goldens/trace_effect_step.txt @@ -4,9 +4,8 @@ workload: n=100000 reps=4 result=1500000 aborts (distinct site -- reason): Bench.EffectStep.lua:10 -- NYI: bytecode FNEW Bench.EffectStep.lua:12 -- NYI: bytecode FNEW - Bench.EffectStep.lua:45 -- NYI: bytecode FNEW - Bench.EffectStep.lua:46 -- NYI: bytecode FNEW - Bench.EffectStep.lua:58 -- NYI: bytecode FNEW + Bench.EffectStep.lua:44 -- NYI: bytecode FNEW + Bench.EffectStep.lua:56 -- NYI: bytecode FNEW Bench.EffectStep.lua:9 -- NYI: bytecode FNEW bytecode end state (J*=compiled, I*=blacklisted): Bench.EffectStep.lua:10 IFUNCF @@ -17,9 +16,8 @@ bytecode end state (J*=compiled, I*=blacklisted): Bench.EffectStep.lua:17 IFUNCF Bench.EffectStep.lua:36 IFUNCF Bench.EffectStep.lua:37 IFUNCF - Bench.EffectStep.lua:39 IFUNCF Bench.EffectStep.lua:9 JFUNCF effect_step.lua:11 JFUNCF effect_step.lua:13 JFORI effect_step.lua:13 JFORL -counts: aborts=6 compiled=6 blacklisted=7 +counts: aborts=5 compiled=6 blacklisted=6