From ce6fd3367850d480cddeb34e88741c2f37f8d170 Mon Sep 17 00:00:00 2001 From: Yura Lazarev Date: Mon, 27 Jul 2026 12:06:28 +0200 Subject: [PATCH 1/4] test(golden): pin the pre-#239 boxed shapes of Ref/STRef cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new Golden.RefUnbox.Test module covering the cell shapes issue #239 lowers: a loop accumulator (sumTo), modify' with distinct state/value components (splitModify), ST write returning the written value (writeBack), a cell stored inside another cell (nested), and an Effect Ref with direct read/write (main). The goldens pin the current boxed form — every cell a {value = ...} table — so the unboxing change shows as a reviewable diff against them; the hand-written eval oracle pins the runtime outputs the rewrite must preserve. --- .../output/Golden.RefUnbox.Test/corefn.json | 1 + .../Golden.RefUnbox.Test/eval/.gitignore | 1 + .../Golden.RefUnbox.Test/eval/golden.txt | 5 + test/ps/output/Golden.RefUnbox.Test/golden.ir | 530 ++++++++++++++++++ .../ps/output/Golden.RefUnbox.Test/golden.lua | 102 ++++ test/ps/src/Golden/RefUnbox/Test.purs | 63 +++ 6 files changed, 702 insertions(+) create mode 100644 test/ps/output/Golden.RefUnbox.Test/corefn.json create mode 100644 test/ps/output/Golden.RefUnbox.Test/eval/.gitignore create mode 100644 test/ps/output/Golden.RefUnbox.Test/eval/golden.txt create mode 100644 test/ps/output/Golden.RefUnbox.Test/golden.ir create mode 100644 test/ps/output/Golden.RefUnbox.Test/golden.lua create mode 100644 test/ps/src/Golden/RefUnbox/Test.purs diff --git a/test/ps/output/Golden.RefUnbox.Test/corefn.json b/test/ps/output/Golden.RefUnbox.Test/corefn.json new file mode 100644 index 00000000..ec26fc2d --- /dev/null +++ b/test/ps/output/Golden.RefUnbox.Test/corefn.json @@ -0,0 +1 @@ +{"builtWith":"0.15.16","comments":[{"LineComment":" | Non-escaping Ref/STRef cells lower to plain mutable Lua locals"},{"LineComment":" | (issue #239): `new` becomes a `local`, `read` the local itself,"},{"LineComment":" | `write`/`modify` plain assignments — no `{value = …}` heap table."},{"LineComment":" |"},{"LineComment":" | Covers the loop-accumulator shape (`sumTo`), a `modify'` whose"},{"LineComment":" | state and value differ (`splitModify`), ST `write` returning the"},{"LineComment":" | written value (`writeBack`), a cell that escapes by being stored"},{"LineComment":" | inside another cell and therefore keeps its boxed form (`nested`),"},{"LineComment":" | and an Effect `Ref` with direct `read`/`write` (`main`)."}],"decls":[{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[38,19],"start":[38,3]}},"type":"Var","value":{"identifier":"bind","moduleName":["Control","Bind"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[38,19],"start":[38,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bindST","moduleName":["Control","Monad","ST","Internal"]}},"type":"App"},"identifier":"bind"},{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[41,7],"start":[41,3]}},"type":"Var","value":{"identifier":"pure","moduleName":["Control","Applicative"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[41,15],"start":[41,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"applicativeST","moduleName":["Control","Monad","ST","Internal"]}},"type":"App"},"identifier":"pure"},{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[41,12],"start":[41,11]}},"type":"Var","value":{"identifier":"add","moduleName":["Data","Semiring"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[41,14],"start":[41,9]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"semiringInt","moduleName":["Data","Semiring"]}},"type":"App"},"identifier":"add"},{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[24,47],"start":[24,3]}},"type":"Var","value":{"identifier":"discard","moduleName":["Control","Bind"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[24,47],"start":[24,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discardUnit","moduleName":["Control","Bind"]}},"type":"App"},"identifier":"discard"},{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discard","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[24,47],"start":[24,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bindST","moduleName":["Control","Monad","ST","Internal"]}},"type":"App"},"identifier":"discard1"},{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[31,41],"start":[31,40]}},"type":"Var","value":{"identifier":"mul","moduleName":["Data","Semiring"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[31,43],"start":[31,38]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"semiringInt","moduleName":["Data","Semiring"]}},"type":"App"},"identifier":"mul"},{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discard","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[55,21],"start":[55,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bindEffect","moduleName":["Effect"]}},"type":"App"},"identifier":"discard2"},{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[55,10],"start":[55,3]}},"type":"Var","value":{"identifier":"logShow","moduleName":["Effect","Console"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[55,21],"start":[55,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"showInt","moduleName":["Data","Show"]}},"type":"App"},"identifier":"logShow"},{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[59,24],"start":[59,3]}},"type":"Var","value":{"identifier":"bind","moduleName":["Control","Bind"]}},"annotation":{"meta":{"metaType":"IsSyntheticApp"},"sourceSpan":{"end":[59,24],"start":[59,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bindEffect","moduleName":["Effect"]}},"type":"App"},"identifier":"bind1"},{"annotation":{"meta":null,"sourceSpan":{"end":[36,17],"start":[36,1]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[37,16],"start":[37,13]}},"type":"Var","value":{"identifier":"run","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[41,15],"start":[37,13]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[38,19],"start":[38,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[38,17],"start":[38,8]}},"type":"Var","value":{"identifier":"new","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[38,19],"start":[38,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[38,19],"start":[38,18]}},"type":"Literal","value":{"literalType":"IntLiteral","value":1}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[38,19],"start":[38,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[38,19],"start":[38,3]}},"argument":"r","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[39,23],"start":[39,3]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[39,19],"start":[39,8]}},"type":"Var","value":{"identifier":"write","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[39,21],"start":[39,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[39,21],"start":[39,20]}},"type":"Literal","value":{"literalType":"IntLiteral","value":7}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[39,23],"start":[39,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[39,23],"start":[39,22]}},"type":"Var","value":{"identifier":"r","sourcePos":[38,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[39,23],"start":[39,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[39,23],"start":[39,3]}},"argument":"x","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[40,20],"start":[40,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[40,18],"start":[40,8]}},"type":"Var","value":{"identifier":"read","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[40,20],"start":[40,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[40,20],"start":[40,19]}},"type":"Var","value":{"identifier":"r","sourcePos":[38,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[40,20],"start":[40,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[40,20],"start":[40,3]}},"argument":"y","body":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"pure","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[41,15],"start":[41,3]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"add","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[41,14],"start":[41,9]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[41,10],"start":[41,9]}},"type":"Var","value":{"identifier":"x","sourcePos":[39,3]}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[41,14],"start":[41,9]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[41,14],"start":[41,13]}},"type":"Var","value":{"identifier":"y","sourcePos":[40,3]}},"type":"App"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"App"},"identifier":"writeBack"},{"annotation":{"meta":null,"sourceSpan":{"end":[21,20],"start":[21,1]}},"bindType":"NonRec","expression":{"annotation":{"meta":null,"sourceSpan":{"end":[21,20],"start":[21,1]}},"argument":"n","body":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[22,14],"start":[22,11]}},"type":"Var","value":{"identifier":"run","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[25,17],"start":[22,11]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[23,21],"start":[23,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[23,19],"start":[23,10]}},"type":"Var","value":{"identifier":"new","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[23,21],"start":[23,10]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[23,21],"start":[23,20]}},"type":"Literal","value":{"literalType":"IntLiteral","value":0}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[23,21],"start":[23,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[23,21],"start":[23,3]}},"argument":"acc","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discard1","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[24,47],"start":[24,3]}},"argument":{"abstraction":{"abstraction":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[24,6],"start":[24,3]}},"type":"Var","value":{"identifier":"for","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[24,8],"start":[24,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[24,8],"start":[24,7]}},"type":"Literal","value":{"literalType":"IntLiteral","value":0}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[24,16],"start":[24,3]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"add","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[24,15],"start":[24,10]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[24,11],"start":[24,10]}},"type":"Var","value":{"identifier":"n","sourcePos":[22,1]}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[24,15],"start":[24,10]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[24,15],"start":[24,14]}},"type":"Literal","value":{"literalType":"IntLiteral","value":1}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[24,47],"start":[24,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[24,47],"start":[24,17]}},"argument":"i","body":{"abstraction":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[24,35],"start":[24,23]}},"type":"Var","value":{"identifier":"modify","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[24,43],"start":[24,23]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[24,43],"start":[24,36]}},"argument":"v","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"add","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[24,43],"start":[24,36]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"v","sourcePos":[0,0]}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[24,43],"start":[24,36]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[24,42],"start":[24,41]}},"type":"Var","value":{"identifier":"i","sourcePos":[24,18]}},"type":"App"},"type":"Abs"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[24,47],"start":[24,23]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[24,47],"start":[24,44]}},"type":"Var","value":{"identifier":"acc","sourcePos":[23,3]}},"type":"App"},"type":"Abs"},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[24,47],"start":[24,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[24,47],"start":[24,3]}},"argument":"$__unused","body":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[25,13],"start":[25,3]}},"type":"Var","value":{"identifier":"read","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[25,17],"start":[25,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[25,17],"start":[25,14]}},"type":"Var","value":{"identifier":"acc","sourcePos":[23,3]}},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"App"},"type":"Abs"},"identifier":"sumTo"},{"annotation":{"meta":null,"sourceSpan":{"end":[28,26],"start":[28,1]}},"bindType":"NonRec","expression":{"annotation":{"meta":null,"sourceSpan":{"end":[28,26],"start":[28,1]}},"argument":"n","body":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[29,20],"start":[29,17]}},"type":"Var","value":{"identifier":"run","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[33,15],"start":[29,17]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[30,19],"start":[30,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[30,17],"start":[30,8]}},"type":"Var","value":{"identifier":"new","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[30,19],"start":[30,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[30,19],"start":[30,18]}},"type":"Var","value":{"identifier":"n","sourcePos":[29,1]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[30,19],"start":[30,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[30,19],"start":[30,3]}},"argument":"r","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[31,64],"start":[31,3]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[31,21],"start":[31,8]}},"type":"Var","value":{"identifier":"modify'","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[31,62],"start":[31,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[31,61],"start":[31,23]}},"argument":"s","body":{"annotation":{"meta":null,"sourceSpan":{"end":[31,61],"start":[31,29]}},"type":"Literal","value":{"literalType":"ObjectLiteral","value":[["state",{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"mul","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[31,43],"start":[31,38]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[31,39],"start":[31,38]}},"type":"Var","value":{"identifier":"s","sourcePos":[31,24]}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[31,43],"start":[31,38]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[31,43],"start":[31,42]}},"type":"Literal","value":{"literalType":"IntLiteral","value":2}},"type":"App"}],["value",{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"add","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[31,59],"start":[31,52]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[31,53],"start":[31,52]}},"type":"Var","value":{"identifier":"s","sourcePos":[31,24]}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[31,59],"start":[31,52]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[31,59],"start":[31,56]}},"type":"Literal","value":{"literalType":"IntLiteral","value":100}},"type":"App"}]]}},"type":"Abs"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[31,64],"start":[31,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[31,64],"start":[31,63]}},"type":"Var","value":{"identifier":"r","sourcePos":[30,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[31,64],"start":[31,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[31,64],"start":[31,3]}},"argument":"v","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[32,20],"start":[32,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[32,18],"start":[32,8]}},"type":"Var","value":{"identifier":"read","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[32,20],"start":[32,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[32,20],"start":[32,19]}},"type":"Var","value":{"identifier":"r","sourcePos":[30,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[32,20],"start":[32,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[32,20],"start":[32,3]}},"argument":"s","body":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"pure","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[33,15],"start":[33,3]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"add","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[33,14],"start":[33,9]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[33,10],"start":[33,9]}},"type":"Var","value":{"identifier":"v","sourcePos":[31,3]}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[33,14],"start":[33,9]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[33,14],"start":[33,13]}},"type":"Var","value":{"identifier":"s","sourcePos":[32,3]}},"type":"App"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"App"},"type":"Abs"},"identifier":"splitModify"},{"annotation":{"meta":null,"sourceSpan":{"end":[45,14],"start":[45,1]}},"bindType":"NonRec","expression":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[46,13],"start":[46,10]}},"type":"Var","value":{"identifier":"run","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[51,18],"start":[46,10]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[47,24],"start":[47,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[47,21],"start":[47,12]}},"type":"Var","value":{"identifier":"new","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[47,24],"start":[47,12]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[47,24],"start":[47,22]}},"type":"Literal","value":{"literalType":"IntLiteral","value":21}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[47,24],"start":[47,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[47,24],"start":[47,3]}},"argument":"inner","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[48,27],"start":[48,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[48,21],"start":[48,12]}},"type":"Var","value":{"identifier":"new","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[48,27],"start":[48,12]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[48,27],"start":[48,22]}},"type":"Var","value":{"identifier":"inner","sourcePos":[47,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[48,27],"start":[48,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[48,27],"start":[48,3]}},"argument":"outer","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[49,27],"start":[49,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[49,21],"start":[49,11]}},"type":"Var","value":{"identifier":"read","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[49,27],"start":[49,11]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[49,27],"start":[49,22]}},"type":"Var","value":{"identifier":"outer","sourcePos":[48,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[49,27],"start":[49,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[49,27],"start":[49,3]}},"argument":"cell","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[50,33],"start":[50,3]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[50,20],"start":[50,8]}},"type":"Var","value":{"identifier":"modify","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[50,28],"start":[50,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[50,28],"start":[50,21]}},"argument":"v","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"mul","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[50,28],"start":[50,21]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"v","sourcePos":[0,0]}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[50,28],"start":[50,21]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[50,27],"start":[50,26]}},"type":"Literal","value":{"literalType":"IntLiteral","value":2}},"type":"App"},"type":"Abs"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[50,33],"start":[50,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[50,33],"start":[50,29]}},"type":"Var","value":{"identifier":"cell","sourcePos":[49,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[50,33],"start":[50,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[50,33],"start":[50,3]}},"argument":"$__unused","body":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[51,13],"start":[51,3]}},"type":"Var","value":{"identifier":"read","moduleName":["Control","Monad","ST","Internal"]}},"annotation":{"meta":null,"sourceSpan":{"end":[51,18],"start":[51,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[51,18],"start":[51,14]}},"type":"Var","value":{"identifier":"cell","sourcePos":[49,3]}},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"App"},"identifier":"nested"},{"annotation":{"meta":null,"sourceSpan":{"end":[53,20],"start":[53,1]}},"bindType":"NonRec","expression":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discard2","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[55,21],"start":[55,3]}},"argument":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"logShow","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[55,21],"start":[55,3]}},"argument":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[55,17],"start":[55,12]}},"type":"Var","value":{"identifier":"sumTo","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[55,20],"start":[55,12]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[55,20],"start":[55,18]}},"type":"Literal","value":{"literalType":"IntLiteral","value":10}},"type":"App"},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[55,21],"start":[55,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[55,21],"start":[55,3]}},"argument":"$__unused","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discard2","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[56,26],"start":[56,3]}},"argument":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"logShow","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[56,26],"start":[56,3]}},"argument":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[56,23],"start":[56,12]}},"type":"Var","value":{"identifier":"splitModify","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[56,25],"start":[56,12]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[56,25],"start":[56,24]}},"type":"Literal","value":{"literalType":"IntLiteral","value":3}},"type":"App"},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[56,26],"start":[56,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[56,26],"start":[56,3]}},"argument":"$__unused","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discard2","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[57,20],"start":[57,3]}},"argument":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"logShow","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[57,20],"start":[57,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[57,20],"start":[57,11]}},"type":"Var","value":{"identifier":"writeBack","moduleName":["Golden","RefUnbox","Test"]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[57,20],"start":[57,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[57,20],"start":[57,3]}},"argument":"$__unused","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discard2","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[58,17],"start":[58,3]}},"argument":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"logShow","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[58,17],"start":[58,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[58,17],"start":[58,11]}},"type":"Var","value":{"identifier":"nested","moduleName":["Golden","RefUnbox","Test"]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[58,17],"start":[58,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[58,17],"start":[58,3]}},"argument":"$__unused","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind1","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[59,24],"start":[59,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[59,21],"start":[59,14]}},"type":"Var","value":{"identifier":"new","moduleName":["Effect","Ref"]}},"annotation":{"meta":null,"sourceSpan":{"end":[59,24],"start":[59,14]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[59,24],"start":[59,22]}},"type":"Literal","value":{"literalType":"IntLiteral","value":10}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[59,24],"start":[59,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[59,24],"start":[59,3]}},"argument":"counter","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind1","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[60,24],"start":[60,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[60,16],"start":[60,8]}},"type":"Var","value":{"identifier":"read","moduleName":["Effect","Ref"]}},"annotation":{"meta":null,"sourceSpan":{"end":[60,24],"start":[60,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[60,24],"start":[60,17]}},"type":"Var","value":{"identifier":"counter","sourcePos":[59,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[60,24],"start":[60,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[60,24],"start":[60,3]}},"argument":"v","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"discard2","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[61,28],"start":[61,3]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[61,12],"start":[61,3]}},"type":"Var","value":{"identifier":"write","moduleName":["Effect","Ref"]}},"annotation":{"meta":null,"sourceSpan":{"end":[61,20],"start":[61,3]}},"argument":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"add","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[61,19],"start":[61,14]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[61,15],"start":[61,14]}},"type":"Var","value":{"identifier":"v","sourcePos":[60,3]}},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[61,19],"start":[61,14]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[61,19],"start":[61,18]}},"type":"Literal","value":{"literalType":"IntLiteral","value":1}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[61,28],"start":[61,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[61,28],"start":[61,21]}},"type":"Var","value":{"identifier":"counter","sourcePos":[59,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[61,28],"start":[61,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[61,28],"start":[61,3]}},"argument":"$__unused","body":{"abstraction":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"bind1","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[62,24],"start":[62,3]}},"argument":{"abstraction":{"annotation":{"meta":{"metaType":"IsForeign"},"sourceSpan":{"end":[62,16],"start":[62,8]}},"type":"Var","value":{"identifier":"read","moduleName":["Effect","Ref"]}},"annotation":{"meta":null,"sourceSpan":{"end":[62,24],"start":[62,8]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[62,24],"start":[62,17]}},"type":"Var","value":{"identifier":"counter","sourcePos":[59,3]}},"type":"App"},"type":"App"},"annotation":{"meta":null,"sourceSpan":{"end":[62,24],"start":[62,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[62,24],"start":[62,3]}},"argument":"w","body":{"abstraction":{"annotation":{"meta":null,"sourceSpan":{"end":[0,0],"start":[0,0]}},"type":"Var","value":{"identifier":"logShow","moduleName":["Golden","RefUnbox","Test"]}},"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[63,3]}},"argument":{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[63,11]}},"type":"Var","value":{"identifier":"w","sourcePos":[62,3]}},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"type":"Abs"},"type":"App"},"identifier":"main"}],"exports":["sumTo","splitModify","writeBack","nested","main"],"foreign":[],"imports":[{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Control","Applicative"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Control","Bind"]},{"annotation":{"meta":null,"sourceSpan":{"end":[14,35],"start":[14,1]}},"moduleName":["Control","Monad","ST"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Control","Monad","ST","Internal"]},{"annotation":{"meta":null,"sourceSpan":{"end":[15,37],"start":[15,1]}},"moduleName":["Control","Monad","ST","Ref"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Data","Semiring"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Data","Show"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Effect"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Effect","Console"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Effect","Ref"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Golden","RefUnbox","Test"]},{"annotation":{"meta":null,"sourceSpan":{"end":[12,15],"start":[12,1]}},"moduleName":["Prelude"]},{"annotation":{"meta":null,"sourceSpan":{"end":[63,12],"start":[10,1]}},"moduleName":["Prim"]}],"moduleName":["Golden","RefUnbox","Test"],"modulePath":"src/Golden/RefUnbox/Test.purs","reExports":{},"sourceSpan":{"end":[63,12],"start":[10,1]}} \ No newline at end of file diff --git a/test/ps/output/Golden.RefUnbox.Test/eval/.gitignore b/test/ps/output/Golden.RefUnbox.Test/eval/.gitignore new file mode 100644 index 00000000..d2dc29bb --- /dev/null +++ b/test/ps/output/Golden.RefUnbox.Test/eval/.gitignore @@ -0,0 +1 @@ +actual.txt diff --git a/test/ps/output/Golden.RefUnbox.Test/eval/golden.txt b/test/ps/output/Golden.RefUnbox.Test/eval/golden.txt new file mode 100644 index 00000000..da66ca05 --- /dev/null +++ b/test/ps/output/Golden.RefUnbox.Test/eval/golden.txt @@ -0,0 +1,5 @@ +55 +109 +14 +42 +11 diff --git a/test/ps/output/Golden.RefUnbox.Test/golden.ir b/test/ps/output/Golden.RefUnbox.Test/golden.ir new file mode 100644 index 00000000..eddbf6c0 --- /dev/null +++ b/test/ps/output/Golden.RefUnbox.Test/golden.ir @@ -0,0 +1,530 @@ +UberModule + { uberModuleBindings = + [ Standalone + ( QName + { qnameModuleName = ModuleName "Data.Show", qnameName = Name "foreign" + }, ForeignImport Nothing + ( ModuleName "Data.Show" ) ".spago/p/prelude/5718c84fdde6247749cb053e816df696c30fe691/src/Data/Show.purs" + [ ( Nothing, Name "showIntImpl" ) ] + ), Standalone + ( QName + { qnameModuleName = ModuleName "Effect.Ref", qnameName = Name "foreign" + }, ForeignImport Nothing + ( ModuleName "Effect.Ref" ) ".spago/p/refs/ee6e959ec21b752622e105a14816e7983b21b9a5/src/Effect/Ref.purs" + [ ( Nothing, Name "_new" ), ( Nothing, Name "read" ), ( Nothing, Name "write" ) ] + ), Standalone + ( QName + { qnameModuleName = ModuleName "Effect.Ref", qnameName = Name "read" }, ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Effect.Ref" ) ( Name "foreign" ) ) ) + ( PropName "read" ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Control.Monad.ST.Internal", qnameName = Name "foreign" + }, ForeignImport Nothing + ( ModuleName "Control.Monad.ST.Internal" ) ".spago/p/st/6d41fd264a05b9a089e79322b22b906597ea7c36/src/Control/Monad/ST/Internal.purs" + [ + ( Nothing, Name "pure_" ), + ( Nothing, Name "bind_" ), + ( Nothing, Name "run" ), + ( Nothing, Name "for" ), + ( Nothing, Name "new" ), + ( Nothing, Name "read" ), + ( Nothing, Name "modifyImpl" ), + ( Nothing, Name "write" ) + ] + ), Standalone + ( QName + { qnameModuleName = ModuleName "Control.Monad.ST.Internal", qnameName = Name "modifyImpl" + }, ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "foreign" ) ) ) + ( PropName "modifyImpl" ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Control.Monad.ST.Internal", qnameName = Name "new" + }, ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "foreign" ) ) ) + ( PropName "new" ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Control.Monad.ST.Internal", qnameName = Name "read" + }, ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "foreign" ) ) ) + ( PropName "read" ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Control.Monad.ST.Internal", qnameName = Name "run" + }, ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "foreign" ) ) ) + ( PropName "run" ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Effect.Console", qnameName = Name "foreign" + }, ForeignImport Nothing + ( ModuleName "Effect.Console" ) ".spago/p/console/f82835a0b873aafe6bd7b14dd30cc150553d4ab9/src/Effect/Console.purs" + [ ( Nothing, Name "log" ) ] + ), Standalone + ( QName + { qnameModuleName = ModuleName "Data.Semiring", qnameName = Name "semiringInt" + }, LiteralObject Nothing + [ + ( PropName "add", AbsN Nothing + ( ParamNamed Nothing ( Name "x$434" ) :| [] ) + ( AbsN Nothing + ( ParamNamed Nothing ( Name "y$435" ) :| [] ) + ( PrimBinOp Nothing PrimAdd + ( Ref Nothing ( Local ( Name "x$434" ) ) ) + ( Ref Nothing ( Local ( Name "y$435" ) ) ) + ) + ) + ), + ( PropName "zero", LiteralInt Nothing 0 ), + ( PropName "mul", AbsN Nothing + ( ParamNamed Nothing ( Name "x$432" ) :| [] ) + ( AbsN Nothing + ( ParamNamed Nothing ( Name "y$433" ) :| [] ) + ( PrimBinOp Nothing PrimMul + ( Ref Nothing ( Local ( Name "x$432" ) ) ) + ( Ref Nothing ( Local ( Name "y$433" ) ) ) + ) + ) + ), + ( PropName "one", LiteralInt Nothing 1 ) + ] + ), Standalone + ( QName + { qnameModuleName = ModuleName "Golden.RefUnbox.Test", qnameName = Name "logShow" + }, AbsN Nothing + ( ParamNamed Nothing ( Name "a$5" ) :| [] ) + ( AppN Nothing + ( ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Effect.Console" ) ( Name "foreign" ) ) ) + ( PropName "log" ) + ) + ( AppN Nothing + ( ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Data.Show" ) ( Name "foreign" ) ) ) + ( PropName "showIntImpl" ) + ) + ( Ref Nothing ( Local ( Name "a$5" ) ) :| [] ) :| [] + ) + ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Golden.RefUnbox.Test", qnameName = Name "writeBack" + }, AppN Nothing + ( Ref Nothing ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "run" ) ) ) + ( AbsN Nothing + ( ParamUnused Nothing :| [] ) + ( Let Nothing + ( Standalone + ( Nothing, Name "r", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "new" ) ) + ) + ( LiteralInt Nothing 1 :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) :| + [ Standalone + ( Nothing, Name "x", AppN Nothing + ( AppN Nothing + ( AppN Nothing + ( ObjectProp Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "foreign" ) ) + ) + ( PropName "write" ) + ) + ( LiteralInt Nothing 7 :| [] ) + ) + ( Ref Nothing ( Local ( Name "r" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "y", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "read" ) ) + ) + ( Ref Nothing ( Local ( Name "r" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) + ] + ) + ( PrimBinOp Nothing PrimAdd + ( Ref Nothing ( Local ( Name "x" ) ) ) + ( Ref Nothing ( Local ( Name "y" ) ) ) + ) + ) :| [] + ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Golden.RefUnbox.Test", qnameName = Name "sumTo" + }, AbsN Nothing + ( ParamNamed Nothing ( Name "n" ) :| [] ) + ( AppN Nothing + ( Ref Nothing ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "run" ) ) ) + ( AbsN Nothing + ( ParamUnused Nothing :| [] ) + ( Let Nothing + ( Standalone + ( Nothing, Name "acc", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "new" ) ) + ) + ( LiteralInt Nothing 0 :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) :| + [ Standalone + ( Nothing, Name "_", AppN Nothing + ( AppN Nothing + ( AppN Nothing + ( AppN Nothing + ( ObjectProp Nothing + ( Ref Nothing + ( Imported + ( ModuleName "Control.Monad.ST.Internal" ) + ( Name "foreign" ) + ) + ) + ( PropName "for" ) + ) + ( LiteralInt Nothing 0 :| [] ) + ) + ( PrimBinOp Nothing PrimAdd + ( Ref Nothing ( Local ( Name "n" ) ) ) + ( LiteralInt Nothing 1 ) :| [] + ) + ) + ( AbsN Nothing + ( ParamNamed Nothing ( Name "i" ) :| [] ) + ( AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported + ( ModuleName "Control.Monad.ST.Internal" ) + ( Name "modifyImpl" ) + ) + ) + ( AbsN Nothing + ( ParamNamed Nothing ( Name "s$472" ) :| [] ) + ( Let Nothing + ( Standalone + ( Nothing, Name "s'$473", PrimBinOp Nothing PrimAdd + ( Ref Nothing ( Local ( Name "s$472" ) ) ) + ( Ref Nothing ( Local ( Name "i" ) ) ) + ) :| [] + ) + ( LiteralObject Nothing + [ + ( PropName "state", Ref Nothing ( Local ( Name "s'$473" ) ) ), + ( PropName "value", Ref Nothing ( Local ( Name "s'$473" ) ) ) + ] + ) + ) :| [] + ) + ) + ( Ref Nothing ( Local ( Name "acc" ) ) :| [] ) + ) :| [] + ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) + ] + ) + ( AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "read" ) ) + ) + ( Ref Nothing ( Local ( Name "acc" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) + ) :| [] + ) + ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Golden.RefUnbox.Test", qnameName = Name "splitModify" + }, AbsN Nothing + ( ParamNamed Nothing ( Name "n" ) :| [] ) + ( AppN Nothing + ( Ref Nothing ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "run" ) ) ) + ( AbsN Nothing + ( ParamUnused Nothing :| [] ) + ( Let Nothing + ( Standalone + ( Nothing, Name "r", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "new" ) ) + ) + ( Ref Nothing ( Local ( Name "n" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) :| + [ Standalone + ( Nothing, Name "v", AppN Nothing + ( AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported + ( ModuleName "Control.Monad.ST.Internal" ) + ( Name "modifyImpl" ) + ) + ) + ( AbsN Nothing + ( ParamNamed Nothing ( Name "s" ) :| [] ) + ( LiteralObject Nothing + [ + ( PropName "state", PrimBinOp Nothing PrimMul + ( Ref Nothing ( Local ( Name "s" ) ) ) + ( LiteralInt Nothing 2 ) + ), + ( PropName "value", PrimBinOp Nothing PrimAdd + ( Ref Nothing ( Local ( Name "s" ) ) ) + ( LiteralInt Nothing 100 ) + ) + ] + ) :| [] + ) + ) + ( Ref Nothing ( Local ( Name "r" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "s0", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "read" ) ) + ) + ( Ref Nothing ( Local ( Name "r" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) + ] + ) + ( PrimBinOp Nothing PrimAdd + ( Ref Nothing ( Local ( Name "v" ) ) ) + ( Ref Nothing ( Local ( Name "s0" ) ) ) + ) + ) :| [] + ) + ) + ), Standalone + ( QName + { qnameModuleName = ModuleName "Golden.RefUnbox.Test", qnameName = Name "nested" + }, AppN Nothing + ( Ref Nothing ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "run" ) ) ) + ( AbsN Nothing + ( ParamUnused Nothing :| [] ) + ( Let Nothing + ( Standalone + ( Nothing, Name "inner", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "new" ) ) + ) + ( LiteralInt Nothing 21 :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) :| + [ Standalone + ( Nothing, Name "outer", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "new" ) ) + ) + ( Ref Nothing ( Local ( Name "inner" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "cell", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "read" ) ) + ) + ( Ref Nothing ( Local ( Name "outer" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "_", AppN Nothing + ( AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported + ( ModuleName "Control.Monad.ST.Internal" ) + ( Name "modifyImpl" ) + ) + ) + ( AbsN Nothing + ( ParamNamed Nothing ( Name "s$459" ) :| [] ) + ( Let Nothing + ( Standalone + ( Nothing, Name "s'$460", PrimBinOp Nothing PrimMul + ( Ref Nothing ( Local ( Name "s$459" ) ) ) + ( LiteralInt Nothing 2 ) + ) :| [] + ) + ( LiteralObject Nothing + [ + ( PropName "state", Ref Nothing ( Local ( Name "s'$460" ) ) ), + ( PropName "value", Ref Nothing ( Local ( Name "s'$460" ) ) ) + ] + ) + ) :| [] + ) + ) + ( Ref Nothing ( Local ( Name "cell" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) + ] + ) + ( AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Control.Monad.ST.Internal" ) ( Name "read" ) ) + ) + ( Ref Nothing ( Local ( Name "cell" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) + ) :| [] + ) + ) + ], uberModuleForeigns = [], uberModuleExports = + [ + ( Name "sumTo", Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "sumTo" ) ) + ), + ( Name "splitModify", Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "splitModify" ) ) + ), + ( Name "writeBack", Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "writeBack" ) ) + ), + ( Name "nested", Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "nested" ) ) + ), + ( Name "main", AbsN Nothing + ( ParamUnused Nothing :| [] ) + ( Let Nothing + ( Standalone + ( Nothing, Name "_", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "logShow" ) ) + ) + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "sumTo" ) ) + ) + ( LiteralInt Nothing 10 :| [] ) :| [] + ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) :| + [ Standalone + ( Nothing, Name "_", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "logShow" ) ) + ) + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "splitModify" ) ) + ) + ( LiteralInt Nothing 3 :| [] ) :| [] + ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "_", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "logShow" ) ) + ) + ( Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "writeBack" ) ) :| [] + ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "_", AppN Nothing + ( AppN Nothing + ( Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "logShow" ) ) + ) + ( Ref Nothing + ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "nested" ) ) :| [] + ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "counter$0", AppN Nothing + ( AppN Nothing + ( ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Effect.Ref" ) ( Name "foreign" ) ) ) + ( PropName "_new" ) + ) + ( LiteralInt Nothing 10 :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "v$1", AppN Nothing + ( AppN Nothing + ( Ref Nothing ( Imported ( ModuleName "Effect.Ref" ) ( Name "read" ) ) ) + ( Ref Nothing ( Local ( Name "counter$0" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "_", AppN Nothing + ( AppN Nothing + ( AppN Nothing + ( ObjectProp Nothing + ( Ref Nothing ( Imported ( ModuleName "Effect.Ref" ) ( Name "foreign" ) ) ) + ( PropName "write" ) + ) + ( AppN Nothing + ( AppN Nothing + ( ObjectProp Nothing + ( Ref Nothing + ( Imported ( ModuleName "Data.Semiring" ) ( Name "semiringInt" ) ) + ) + ( PropName "add" ) + ) + ( Ref Nothing ( Local ( Name "v$1" ) ) :| [] ) + ) + ( LiteralInt Nothing 1 :| [] ) :| [] + ) + ) + ( Ref Nothing ( Local ( Name "counter$0" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ), Standalone + ( Nothing, Name "w$2", AppN Nothing + ( AppN Nothing + ( Ref Nothing ( Imported ( ModuleName "Effect.Ref" ) ( Name "read" ) ) ) + ( Ref Nothing ( Local ( Name "counter$0" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) + ] + ) + ( AppN Nothing + ( AppN Nothing + ( Ref Nothing ( Imported ( ModuleName "Golden.RefUnbox.Test" ) ( Name "logShow" ) ) ) + ( Ref Nothing ( Local ( Name "w$2" ) ) :| [] ) + ) + ( Ref Nothing ( Imported ( ModuleName "Prim" ) ( Name "$magicDoRun" ) ) :| [] ) + ) + ) + ) + ] + } \ No newline at end of file diff --git a/test/ps/output/Golden.RefUnbox.Test/golden.lua b/test/ps/output/Golden.RefUnbox.Test/golden.lua new file mode 100644 index 00000000..69365978 --- /dev/null +++ b/test/ps/output/Golden.RefUnbox.Test/golden.lua @@ -0,0 +1,102 @@ +local Data_Show_foreign = { showIntImpl = function(n) return tostring(n) end } +local Effect_Ref_foreign = { + _new = function(val) return function() return { value = val } end end, + read = function(ref) return function() return ref.value end end, + write = function(val) + return function(ref) return function() ref.value = val end end + end +} +local Effect_Ref_read = Effect_Ref_foreign.read +local Control_Monad_ST_Internal_foreign = { + pure_ = function(a) return function() return a end end, + bind_ = function(a) + return function(f) return function() return f(a())() end end + end, + run = function(f) return f() end, + _for_ = function(lo) + return function(hi) + return function(f) + return function() for i = lo, hi - 1 do f(i)() end end + end + end + end, + new = function(val) return function() return { value = val } end end, + read = function(ref) return function() return ref.value end end, + modifyImpl = function(f) + return function(ref) + return function() + local t = f(ref.value) + ref.value = t.state + return t.value + end + end + end, + write = function(a) + return function(ref) return function() ref.value = a return a end end + end +} +local Control_Monad_ST_Internal_modifyImpl = Control_Monad_ST_Internal_foreign.modifyImpl +local Control_Monad_ST_Internal_new = Control_Monad_ST_Internal_foreign.new +local Control_Monad_ST_Internal_read = Control_Monad_ST_Internal_foreign.read +local Control_Monad_ST_Internal_run = Control_Monad_ST_Internal_foreign.run +local Effect_Console_foreign = { + log = function(s) return function() print(s) end end +} +local Data_Semiring_semiringInt = { + add = function(x_S_0) return function(y_S_0) return x_S_0 + y_S_0 end end, + zero = 0, + mul = function(x_S_1) return function(y_S_1) return x_S_1 * y_S_1 end end, + one = 1 +} +local Golden_RefUnbox_Test_logShow = function(a_S_0) + return Effect_Console_foreign.log(Data_Show_foreign.showIntImpl(a_S_0)) +end +local Golden_RefUnbox_Test_writeBack = Control_Monad_ST_Internal_run(function() + local r = Control_Monad_ST_Internal_new(1)() + local x = Control_Monad_ST_Internal_foreign.write(7)(r)() + local y = Control_Monad_ST_Internal_read(r)() + return x + y +end) +local Golden_RefUnbox_Test_sumTo = function(n) + return Control_Monad_ST_Internal_run(function() + local acc = Control_Monad_ST_Internal_new(0)() + for i = 0, n + 1 - 1 do + Control_Monad_ST_Internal_modifyImpl(function(s_S_0) + local sPrime_S_0 = s_S_0 + i + return { state = sPrime_S_0, value = sPrime_S_0 } + end)(acc)() + end + return Control_Monad_ST_Internal_read(acc)() + end) +end +local Golden_RefUnbox_Test_splitModify = function(n) + return Control_Monad_ST_Internal_run(function() + local r = Control_Monad_ST_Internal_new(n)() + local v = Control_Monad_ST_Internal_modifyImpl(function(s) + return { state = s * 2, value = s + 100 } + end)(r)() + local s0 = Control_Monad_ST_Internal_read(r)() + return v + s0 + end) +end +local Golden_RefUnbox_Test_nested = Control_Monad_ST_Internal_run(function() + local inner = Control_Monad_ST_Internal_new(21)() + local outer = Control_Monad_ST_Internal_new(inner)() + local cell = Control_Monad_ST_Internal_read(outer)() + local _ = Control_Monad_ST_Internal_modifyImpl(function(s_S_1) + local sPrime_S_1 = s_S_1 * 2 + return { state = sPrime_S_1, value = sPrime_S_1 } + end)(cell)() + return Control_Monad_ST_Internal_read(cell)() +end) +return (function() + local _ = Golden_RefUnbox_Test_logShow(Golden_RefUnbox_Test_sumTo(10))() + local _ = Golden_RefUnbox_Test_logShow(Golden_RefUnbox_Test_splitModify(3))() + local _ = Golden_RefUnbox_Test_logShow(Golden_RefUnbox_Test_writeBack)() + local _ = Golden_RefUnbox_Test_logShow(Golden_RefUnbox_Test_nested)() + local counter_S_0 = Effect_Ref_foreign._new(10)() + local v_S_0 = Effect_Ref_read(counter_S_0)() + local _ = Effect_Ref_foreign.write(Data_Semiring_semiringInt.add(v_S_0)(1))(counter_S_0)() + local w_S_0 = Effect_Ref_read(counter_S_0)() + return Golden_RefUnbox_Test_logShow(w_S_0)() +end)() diff --git a/test/ps/src/Golden/RefUnbox/Test.purs b/test/ps/src/Golden/RefUnbox/Test.purs new file mode 100644 index 00000000..6b03b008 --- /dev/null +++ b/test/ps/src/Golden/RefUnbox/Test.purs @@ -0,0 +1,63 @@ +-- | Non-escaping Ref/STRef cells lower to plain mutable Lua locals +-- | (issue #239): `new` becomes a `local`, `read` the local itself, +-- | `write`/`modify` plain assignments — no `{value = …}` heap table. +-- | +-- | Covers the loop-accumulator shape (`sumTo`), a `modify'` whose +-- | state and value differ (`splitModify`), ST `write` returning the +-- | written value (`writeBack`), a cell that escapes by being stored +-- | inside another cell and therefore keeps its boxed form (`nested`), +-- | and an Effect `Ref` with direct `read`/`write` (`main`). +module Golden.RefUnbox.Test where + +import Prelude + +import Control.Monad.ST (for, run) +import Control.Monad.ST.Ref as STRef +import Effect (Effect) +import Effect.Console (logShow) +import Effect.Ref as Ref + +-- | Fully unboxable: allocate, mutate in a loop, read once at the end. +sumTo :: Int -> Int +sumTo n = run do + acc <- STRef.new 0 + for 0 (n + 1) \i -> STRef.modify (_ + i) acc + STRef.read acc + +-- | `modify'` with distinct state and value components. +splitModify :: Int -> Int +splitModify n = run do + r <- STRef.new n + v <- STRef.modify' (\s -> { state: s * 2, value: s + 100 }) r + s <- STRef.read r + pure (v + s) + +-- | ST `write` returns the written value. +writeBack :: Int +writeBack = run do + r <- STRef.new 1 + x <- STRef.write 7 r + y <- STRef.read r + pure (x + y) + +-- | The inner cell is stored in the outer cell — used as a whole value — +-- | so it keeps its `{value = …}` table; the outer cell still unboxes. +nested :: Int +nested = run do + inner <- STRef.new 21 + outer <- STRef.new inner + cell <- STRef.read outer + _ <- STRef.modify (_ * 2) cell + STRef.read cell + +main :: Effect Unit +main = do + logShow (sumTo 10) + logShow (splitModify 3) + logShow writeBack + logShow nested + counter <- Ref.new 10 + v <- Ref.read counter + Ref.write (v + 1) counter + w <- Ref.read counter + logShow w From 5b3006ca76c5870390fc4f51784f0d9956d992e1 Mon Sep 17 00:00:00 2001 From: Yura Lazarev Date: Mon, 27 Jul 2026 12:20:54 +0200 Subject: [PATCH 2/4] feat(codegen): unbox non-escaping Ref/STRef cells to mutable locals A Ref/STRef cell compiles to a one-field heap table ({value = ...}) with every read/write/modify going through its field. A Lua local captured by inner closures is itself a shared mutable slot (an upvalue), so when the cell never flows anywhere as a whole value the table buys nothing: a Let-bound run of new whose every use is a recognised operation now lowers to a plain local, reads to the local itself, writes and modifies to assignments. A literal modify function beta-reduces at emission, so its {state, value} record is never allocated either. Recognition is by qualified name over the Effect.Ref and Control.Monad.ST.Internal primitives plus the ST functor's foreign map_ (the void (modify f r) shape), in the two codegen-time head forms the loop matcher already uses. Only run positions lower; a cell used as a first-class value anywhere keeps its boxed form, so a miss is a missed optimisation, never a miscompile. NativeLoop.runStatements statementizes chunk-lowered runs (an unboxed-cell run returns its value in a tail return, which a run-for-effect must discard), and dropValue drops discarded reads of module-scope table fields, which the void wrapper's unit value otherwise leaves as a dead local per loop iteration. Closes #239 --- lib/Language/PureScript/Backend/Lua.hs | 233 ++++++---- .../PureScript/Backend/Lua/NativeLoop.hs | 30 +- .../PureScript/Backend/Lua/RefUnbox.hs | 436 ++++++++++++++++++ pslua.cabal | 1 + .../Golden.MixedDiscardFloat.Test/golden.lua | 11 +- .../Golden.MixedEffectSTDo.Test/golden.lua | 9 +- .../Golden.NativeLoopsST.Test/golden.lua | 38 +- .../Golden.RecordSurgery.Test/golden.lua | 6 +- .../ps/output/Golden.RefUnbox.Test/golden.lua | 48 +- .../output/Golden.STDoBlock.Test/golden.lua | 11 +- 10 files changed, 672 insertions(+), 151 deletions(-) create mode 100644 lib/Language/PureScript/Backend/Lua/RefUnbox.hs diff --git a/lib/Language/PureScript/Backend/Lua.hs b/lib/Language/PureScript/Backend/Lua.hs index 52173b5d..d5d0e19a 100644 --- a/lib/Language/PureScript/Backend/Lua.hs +++ b/lib/Language/PureScript/Backend/Lua.hs @@ -29,6 +29,7 @@ import Language.PureScript.Backend.Lua.Loopify qualified as Loopify import Language.PureScript.Backend.Lua.Name qualified as Lua import Language.PureScript.Backend.Lua.Name qualified as Name import Language.PureScript.Backend.Lua.NativeLoop qualified as NativeLoop +import Language.PureScript.Backend.Lua.RefUnbox qualified as RefUnbox import Language.PureScript.Backend.Lua.Types (ParamF (..)) import Language.PureScript.Backend.Lua.Types qualified as Lua import Language.PureScript.Backend.Types (AppOrModule (..)) @@ -79,19 +80,19 @@ fromUberModule foreigns needsRuntimeLazy appOrModule uber = (`evalStateT` 0) do ((bindings, returnStat), usesObjectUpdate) ← (`runAccumT` NoObjectUpdate) do foreignBindings ← forM (Linker.uberModuleForeigns uber) \(IR.QName modname name, irExp) → do - exp ← asExpression <$> fromIR foreigns Set.empty modname irExp + exp ← asExpression <$> fromIR foreigns Set.empty Set.empty modname irExp pure $ mkBinding modname (fromName name) exp bindings ← Linker.uberModuleBindings uber & foldMapM \case IR.Standalone (IR.QName modname name, irExp) → do - exp ← fromIR foreigns Set.empty modname irExp + exp ← fromIR foreigns Set.empty Set.empty modname irExp pure . DList.singleton $ mkBinding modname (fromName name) (asExpression exp) IR.RecursiveGroup recGroup → do recBinds ← forM (toList recGroup) \(IR.QName modname name, irExp) → (modname,name,) . asExpression - <$> fromIR foreigns Set.empty modname irExp + <$> fromIR foreigns Set.empty Set.empty modname irExp -- A recursive member references itself (and its siblings) -- through the module-scope table, mirroring the Ref case of -- 'fromIR'. @@ -109,11 +110,11 @@ fromUberModule foreigns needsRuntimeLazy appOrModule uber = (`evalStateT` 0) do | (modname, name, exp) ← recBinds ] ) - \group → do + \dispatchGroup → do selector ← freshName "$sel" slots ← - replicateM (Loopify.dispatchArity group) (freshName "$a") - let leader = NE.head (Loopify.dispatchMembers group) + replicateM (Loopify.dispatchArity dispatchGroup) (freshName "$a") + let leader = NE.head (Loopify.dispatchMembers dispatchGroup) -- The leader's Self already carries the qualified -- module-scope field, so the dispatcher derives its -- name from it directly. @@ -128,7 +129,7 @@ fromUberModule foreigns needsRuntimeLazy appOrModule uber = (`evalStateT` 0) do dispatcherSelf selector slots - group + dispatchGroup pure ((dispatcherName, dispatcherExp), wrappers) let wrapperByIndex = concatMap snd dispatched pure $ @@ -153,11 +154,11 @@ fromUberModule foreigns needsRuntimeLazy appOrModule uber = (`evalStateT` 0) do AsModule modname → Lua.table <$> forM (uberModuleExports uber) \(fromName → name, expr) → Lua.tableRowNV name . asExpression - <$> fromIR foreigns mempty modname expr + <$> fromIR foreigns Set.empty mempty modname expr AsApplication modname ident → do case List.lookup name (uberModuleExports uber) of Just expr → do - entry ← fromIR foreigns mempty modname expr + entry ← fromIR foreigns Set.empty mempty modname expr pure $ Lua.functionCall (asExpression entry) [] _ → Oops.throw $ AppEntryPointNotFound modname ident where @@ -215,11 +216,15 @@ fromIR ∷ ∀ e . e `CouldBe` Error ⇒ Tagged "foreign" (Path Abs Dir) + → Set IR.Name + {- ^ Cells lowered to mutable locals, in scope for this expression + ("Language.PureScript.Backend.Lua.RefUnbox"). + -} → Set Lua.Name → ModuleName → IR.Exp → LuaM e (Either Lua.Chunk Lua.Exp) -fromIR foreigns topLevelNames modname ir = case ir of +fromIR foreigns unboxed topLevelNames modname ir = case ir of IR.LiteralInt _ann i → pure . Right $ Lua.Integer i IR.LiteralFloat _ann d → @@ -338,6 +343,19 @@ fromIR foreigns topLevelNames modname ir = case ir of loopRun@IR.AppN {} | Just loop ← NativeLoop.matchLoopRun loopRun → Left <$> NativeLoop.lowerLoop go freshName loop + -- The run of a recognised operation on an unboxed cell (issue #239); + -- see Language.PureScript.Backend.Lua.RefUnbox. A read is a plain + -- expression (the local itself); a mutating run lowers to a chunk + -- ending in a return of the run's value — spliced when it sits in a + -- tail, wrapped in a scope call otherwise (the cell then mutates + -- through the closure's upvalue, which is the same slot). + opRun@IR.AppN {} + | Just (cell, use) ← RefUnbox.matchOpRun opRun + , Set.member cell unboxed → do + (stmts, value) ← RefUnbox.lowerOpRun goExp freshName (fromName cell) use + pure case stmts of + [] → Right value + _ → Left (stmts <> [Lua.return value]) IR.AppN _ann fn args → do e ← goExp fn -- See Note [Nullary functions and Prim.undefined]. PS inserts a @@ -368,86 +386,141 @@ fromIR foreigns topLevelNames modname ir = case ir of (Lua.varName Fixture.moduleName) (qualifyName modname' (fromName name)) -- Standalone bindings become a sequence of 'local' statements, which - -- matches Note [Sequential scoping of Let bindings] + -- matches Note [Sequential scoping of Let bindings]. The bindings are + -- processed left to right carrying the set of cells unboxed so far + -- (issue #239): a binding may extend it, and everything after — + -- including the body — compiles under the extended set. IR.Let _ann bindings bodyExp → do - body ← go bodyExp - recs ← - bindings & foldMapM \case + (recs, unboxed') ← goBindings unboxed (toList bindings) + body ← fromIR foreigns unboxed' topLevelNames modname bodyExp + pure . Left . DList.toList $ + recs <> either DList.fromList (DList.singleton . Lua.return) body + where + goWith env = fromIR foreigns env topLevelNames modname + goExpWith env = asExpression <<$>> goWith env + + goBindings + ∷ Set IR.Name + → [IR.Grouping (IR.Ann, IR.Name, IR.Exp)] + → LuaM e (DList.DList Lua.Statement, Set IR.Name) + goBindings env = \case + [] → pure (mempty, env) + grouping : rest → do + (stmts, env') ← goGrouping env grouping rest + first (stmts <>) <$> goBindings env' rest + + goGrouping + ∷ Set IR.Name + → IR.Grouping (IR.Ann, IR.Name, IR.Exp) + → [IR.Grouping (IR.Ann, IR.Name, IR.Exp)] + → LuaM e (DList.DList Lua.Statement, Set IR.Name) + goGrouping env grouping rest = case grouping of + -- A cell allocation whose every use in the remaining scope goes + -- through the recognised operations unboxes: the binder holds the + -- initial value directly, and the uses lower against it (issue + -- #239); see Language.PureScript.Backend.Lua.RefUnbox. + IR.Standalone (_bindAnn, name, expr) + | name /= IR.discardName + , Just initial ← RefUnbox.matchNewRun expr + , RefUnbox.usedOnlyThroughOps + name + (concatMap (fmap (\(_, _, e) → e) . IR.listGrouping) rest <> [bodyExp]) → do + initExp ← goExpWith env initial + pure + ( DList.singleton (Lua.local1 (fromName name) initExp) + , Set.insert name env + ) + -- A statement whose RHS runs a recognised operation on an + -- unboxed cell lowers to the operation's statements, the binder + -- receiving the run's value. + | Just (cell, use) ← RefUnbox.matchOpRun expr + , Set.member cell env → do + stmts ← + RefUnbox.lowerOpRunBinding + (goExpWith env) + freshName + ( if name == IR.discardName + then Nothing + else Just (fromName name) + ) + (fromName cell) + use + pure (DList.fromList stmts, env) -- A statement whose RHS is a recognised loop run emits the -- native loop directly instead of `local x = `. The -- run yields no values, so the binder reads nil either way: -- `local x` declares exactly that, and the discard binder — never -- referenced (the 'RefToDiscard' lint pins this) — needs no -- declaration at all. - IR.Standalone (_ann, name, expr) - | Just loop ← NativeLoop.matchLoopRun expr → do - loopStmts ← NativeLoop.lowerLoop go freshName loop - pure $ - DList.fromList loopStmts + | Just loop ← NativeLoop.matchLoopRun expr → do + loopStmts ← NativeLoop.lowerLoop (goWith env) freshName loop + pure + ( DList.fromList loopStmts <> if name == IR.discardName then mempty else DList.singleton (Lua.local0 (fromName name)) - IR.Standalone (_ann, name, expr) → - DList.singleton . Lua.local1 (fromName name) <$> goExp expr - IR.RecursiveGroup grp → do - compiled ← forM (toList grp) \(_ann, name, expr) → do - luaExp ← goExp expr - -- The self-reference mirrors the Ref case below: through the - -- module-scope table for a top-level name, plain otherwise. - let luaName = fromName name - (target, self) - | Set.member (qualifyName modname luaName) topLevelNames = - ( qualifyName modname luaName - , Loopify.SelfField - Fixture.moduleName - (qualifyName modname luaName) - ) - | otherwise = (luaName, Loopify.SelfLocal luaName) - pure (name, target, self, luaExp) - -- The members of every mutual tail-call cycle dispatch through - -- a shared local (issue #234), mirroring the top-level case in - -- 'fromUberModule'. - dispatched ← - forM - ( Loopify.planGroupDispatch - [(self, luaExp) | (_, _, self, luaExp) ← compiled] + , env ) - \group → do - selector ← freshName "$sel" - slots ← - replicateM (Loopify.dispatchArity group) (freshName "$a") - let leader = NE.head (Loopify.dispatchMembers group) - dispatcherName = - Name.makeSafe $ - Name.toText (Loopify.selfName (Loopify.dispatchSelf leader)) - <> "$loop" - (dispatcherExp, wrappers) = - Loopify.emitDispatchGroup - (Loopify.SelfLocal dispatcherName) - selector - slots - group - pure ((dispatcherName, dispatcherExp), wrappers) - let wrapperByIndex = concatMap snd dispatched - binds = - [ Lua.local0 dispatcherName - | ((dispatcherName, _), _) ← dispatched - ] - <> [Lua.local0 target | (_, target, _, _) ← compiled] - assignments = - [ Lua.assign (Lua.VarName dispatcherName) dispatcherExp - | ((dispatcherName, dispatcherExp), _) ← dispatched - ] - <> [ Lua.assign (Lua.VarName target) luaExp' - | (index, (_, target, self, luaExp)) ← - zip [0 ..] compiled - , let luaExp' = case List.lookup index wrapperByIndex of - Just wrapper → wrapper - Nothing → Loopify.loopify self luaExp - ] - pure $ DList.fromList binds <> DList.fromList assignments - pure . Left . DList.toList $ - recs <> either DList.fromList (DList.singleton . Lua.return) body + IR.Standalone (_bindAnn, name, expr) → do + luaExp ← goExpWith env expr + pure (DList.singleton (Lua.local1 (fromName name) luaExp), env) + IR.RecursiveGroup grp → do + compiled ← forM (toList grp) \(_bindAnn, name, expr) → do + luaExp ← goExpWith env expr + -- The self-reference mirrors the Ref case below: through the + -- module-scope table for a top-level name, plain otherwise. + let luaName = fromName name + (target, self) + | Set.member (qualifyName modname luaName) topLevelNames = + ( qualifyName modname luaName + , Loopify.SelfField + Fixture.moduleName + (qualifyName modname luaName) + ) + | otherwise = (luaName, Loopify.SelfLocal luaName) + pure (name, target, self, luaExp) + -- The members of every mutual tail-call cycle dispatch through + -- a shared local (issue #234), mirroring the top-level case in + -- 'fromUberModule'. + dispatched ← + forM + ( Loopify.planGroupDispatch + [(self, luaExp) | (_, _, self, luaExp) ← compiled] + ) + \dispatchGroup → do + selector ← freshName "$sel" + slots ← + replicateM (Loopify.dispatchArity dispatchGroup) (freshName "$a") + let leader = NE.head (Loopify.dispatchMembers dispatchGroup) + dispatcherName = + Name.makeSafe $ + Name.toText (Loopify.selfName (Loopify.dispatchSelf leader)) + <> "$loop" + (dispatcherExp, wrappers) = + Loopify.emitDispatchGroup + (Loopify.SelfLocal dispatcherName) + selector + slots + dispatchGroup + pure ((dispatcherName, dispatcherExp), wrappers) + let wrapperByIndex = concatMap snd dispatched + binds = + [ Lua.local0 dispatcherName + | ((dispatcherName, _), _) ← dispatched + ] + <> [Lua.local0 target | (_, target, _, _) ← compiled] + assignments = + [ Lua.assign (Lua.VarName dispatcherName) dispatcherExp + | ((dispatcherName, dispatcherExp), _) ← dispatched + ] + <> [ Lua.assign (Lua.VarName target) luaExp' + | (index, (_, target, self, luaExp)) ← + zip [0 ..] compiled + , let luaExp' = case List.lookup index wrapperByIndex of + Just wrapper → wrapper + Nothing → Loopify.loopify self luaExp + ] + pure (DList.fromList binds <> DList.fromList assignments, env) -- A multi-value return: only reachable in a multi-value tail position -- (Note [Multi-value results] in ...Backend.IR.Types), so it lowers to -- the final @return e₁, …, eₙ@ of the enclosing chunk. Each element is @@ -532,7 +605,7 @@ fromIR foreigns topLevelNames modname ir = case ir of scopeBody = stats <> [Lua.ann (Lua.Return [Lua.ann foreignExports])] where go ∷ IR.Exp → LuaM e (Either Lua.Chunk Lua.Exp) - go = fromIR foreigns topLevelNames modname + go = fromIR foreigns unboxed topLevelNames modname goExp ∷ IR.Exp → LuaM e Lua.Exp goExp = asExpression <<$>> go diff --git a/lib/Language/PureScript/Backend/Lua/NativeLoop.hs b/lib/Language/PureScript/Backend/Lua/NativeLoop.hs index 1839363a..eca2622f 100644 --- a/lib/Language/PureScript/Backend/Lua/NativeLoop.hs +++ b/lib/Language/PureScript/Backend/Lua/NativeLoop.hs @@ -43,11 +43,9 @@ combinator before code generation, so ordinary code is unaffected; an @inline never@ directive pinning such an alias undissolved leaves the foreign call in place — a missed optimization, never a miscompile, since the foreign implementation is what stays. @Golden.NativeLoopsAliasPin@ -pins that shape. Threading a resolver in here would close it, but -recognition is due to move into the IR (issue #239 needs the loop -visible to the optimizer, which a codegen-time match cannot provide), -and the alias question disappears once a lift keys off the foreign -import itself. +pins that shape. Threading a resolver in here would close it, but the +shape only arises under an explicit directive, and the question +disappears if recognition ever moves onto the foreign import itself. == Evaluation order and sharing @@ -77,14 +75,16 @@ spliced — the enclosing function already holds up to a magic-do chunk's is exactly the cost the foreign implementation had. -} module Language.PureScript.Backend.Lua.NativeLoop - ( NativeLoop + ( NativeLoop (..) , matchLoopRun , lowerLoop + , dropValue ) where import Data.Map.Strict qualified as Map import Language.PureScript.Backend.IR qualified as IR import Language.PureScript.Backend.IR.Linker (foreignAccessorQName) +import Language.PureScript.Backend.Lua.Fixture qualified as Fixture import Language.PureScript.Backend.Lua.Name qualified as Name import Language.PureScript.Backend.Lua.Types qualified as Lua @@ -259,8 +259,12 @@ lowerLoop compile fresh = \case e → compile (IR.App IR.noAnn e (IR.EffectRunArg IR.noAnn)) <&> \case Right ex → dropValue ex - -- A nested recognised loop lowers to statements already. - Left chunk → chunk + -- A chunk-lowered run: a nested recognised loop (statements, no + -- tail return) or an unboxed-cell operation + -- ("Language.PureScript.Backend.Lua.RefUnbox") whose tail + -- returns the operation's value — the run discards it, so tail + -- returns rewrite to evaluation statements. + Left chunk → statementize chunk -- Bind a non-atomic expression to a fresh local so the loop reads it -- without re-evaluating; a bare name or scalar literal is used as-is. @@ -346,7 +350,10 @@ statementizeAnn block = case reverse block of {- | Evaluate an expression in statement position, discarding its value: a call becomes a call statement, an effect-free atom disappears, and anything else is bound to the throwaway @_@ local so its evaluation -(and any error it raises) is preserved. +(and any error it raises) is preserved. A field read off the +module-scope table also disappears: that table is built by codegen as a +plain table, so the read cannot trigger a metamethod — unlike a general +field read, which stays bound. -} dropValue ∷ Lua.Exp → [Lua.Statement] dropValue = \case @@ -360,6 +367,11 @@ dropValue = \case Lua.String _ → [] Lua.Function {} → [] Lua.Var (Lua.Ann (Lua.VarName _)) → [] + Lua.Var + ( Lua.Ann + (Lua.VarField (Lua.Ann (Lua.Var (Lua.Ann (Lua.VarName m)))) _field) + ) + | m == Fixture.moduleName → [] e → [Lua.local1 discardLocal e] {- | The throwaway binder for a discarded non-call value; Lua allows the diff --git a/lib/Language/PureScript/Backend/Lua/RefUnbox.hs b/lib/Language/PureScript/Backend/Lua/RefUnbox.hs new file mode 100644 index 00000000..5a9febbe --- /dev/null +++ b/lib/Language/PureScript/Backend/Lua/RefUnbox.hs @@ -0,0 +1,436 @@ +{- | Unbox non-escaping Ref/STRef cells to mutable Lua locals (issue #239). + +A @Ref@/@STRef@ cell compiles to a one-field heap table: the foreign +@new@ allocates @{value = v}@, @read@ returns @ref.value@, @write@ and +@modify@ assign it. When the cell never flows anywhere as a whole value, +the table buys nothing over a plain Lua local — so a @Let@-bound run of +@new@ whose cell qualifies lowers to @local r = v@, a @read@ run to @r@ +itself, and @write@/@modify@ runs to plain assignments. The per-cell +table allocation and the per-operation field indirection disappear. + +== Why capture is not escape + +A Lua local read or assigned from an inner @function@ becomes an +upvalue: a shared, mutable slot that outlives the block when a closure +holds it, with every closure created in the same scope sharing the one +slot. That is exactly the aliasing behaviour of the @{value = …}@ table, +so occurrences of the cell under lambdas — loop bodies, nested magic-do +chunks — need no special treatment: the assignment @r = v@ mutates the +shared slot wherever it executes. The only thing the table provides that +a local cannot is first-class identity — the cell passed to an unknown +function, stored in a structure, or returned. The analysis therefore +asks one question: is every occurrence of the cell the reference +argument of a recognised operation? Any other occurrence keeps the cell +boxed. + +== Recognition + +Operations are recognised by qualified name, the identity that survives +linking (Note [Canonical Effect/ST heads]), in the two forms a foreign +takes at codegen time — the plain imported reference and the dissolved +foreign-accessor read — same as the loop matcher in +"Language.PureScript.Backend.Lua.NativeLoop". The table covers the cell +primitives of @Effect.Ref@ and @Control.Monad.ST.Internal@ (@_new@/@new@, +@read@, @write@, @modifyImpl@) plus the ST functor's foreign @map_@ as a +wrapper: @void (modify f r)@ inlines to @map_ (\\_ → unit) (modifyImpl f +r)@, so without the wrapper the bread-and-butter @modify_@ shape would +keep every cell boxed. @Effect.Ref.newWithSelf@ hands the cell to its +own initialiser — inherent escape — and is deliberately absent. +@Effect@'s functor has no foreign @map@ (it goes through a lazily tied +dictionary), so Effect cells survive only direct @read@/@write@/@modify'@ +uses; the dictionary shapes fall outside the table and keep their cells +boxed, which is always sound. + +== Where uses may sit + +A recognised operation lowers in two positions: + + * /Run/ position — the application carries magic-do's + 'IR.EffectRunArg' marker. This is where magic-do puts every + statement and chain tail, and where + 'Language.PureScript.Backend.Lua.NativeLoop.runStatements' runs a + loop's literal body lambda once per iteration (it attaches the same + marker before compiling), so the analysis checks such a lambda body + as if the marker were present. + + * Everything else is /value/ position: the operation's thunk is a + first-class value (stored, pre-bound and called per iteration, …). + Lowering there would have to reproduce the boxed thunk's evaluation + timing; the analysis simply rejects the cell instead, which keeps + the table and the foreign implementation — a missed optimisation, + never a miscompile. + +== What a run lowers to + +Each run becomes statements plus a value expression: + + * @read r@ — no statements; the value is @r@. + * @write v r@ — @r = v@; the value is @nil@ for @Effect.Ref@ (its + foreign returns nothing) and @r@ for ST (its foreign returns the + written value). + * @modifyImpl f r@ with @f@ a literal lambda ending in a manifest + @{state, value}@ record — the beta the boxed closure call would + perform, done at emission: bind the parameter to @r@, emit the + body's @Let@ spine as locals, pre-bind the record fields in written + order, assign the state field to @r@ and pass the value field on. + The record is never allocated. Any other @f@ falls back to + @local t = f(r); r = t.state@ with value @t.value@. + * @map_ g m@ — the statements of @m@'s run; the value applies @g@ to + @m@'s value. A literal lambda @g@ is bound directly (its parameter + becomes a local holding @m@'s value), matching the boxed call's + timing; a non-atomic non-literal @g@ is pre-bound before @m@'s + statements, since the boxed code evaluates @map_@'s argument before + running the action. + +A run in statement position ('lowerOpRunBinding') scopes its lowering in +a @do … end@ block whenever it declares locals: magic-do budgets ~150 +statements per chunk, and modify spines spilling unscoped locals into +the enclosing function could breach Lua's @LUAI_MAXVARS@ (200 active +locals). A run in expression position compiles to a chunk ending in +@return value@; consumers either splice it as a tail or wrap it in a +scope call, and 'NativeLoop.runStatements' rewrites the tail return into +evaluation statements when the run's value is discarded. +-} +module Language.PureScript.Backend.Lua.RefUnbox + ( OpUse + , matchNewRun + , matchOpRun + , usedOnlyThroughOps + , lowerOpRun + , lowerOpRunBinding + ) where + +import Control.Lens (toListOf) +import Data.Map.Strict qualified as Map +import Data.Set qualified as Set +import Language.PureScript.Backend.IR qualified as IR +import Language.PureScript.Backend.IR.Linker (foreignAccessorQName) +import Language.PureScript.Backend.Lua.Name qualified as Name +import Language.PureScript.Backend.Lua.NativeLoop qualified as NativeLoop +import Language.PureScript.Backend.Lua.Types qualified as Lua + +-------------------------------------------------------------------------------- +-- Recognition ----------------------------------------------------------------- + +{- | A recognised use of a cell: the saturated application of one of the +operations to the cell (before any 'IR.EffectRunArg'), carrying the +non-reference arguments. +-} +data OpUse + = ReadUse + | -- | The written value. + WriteUse WriteResult IR.Exp + | -- | The @s → {state, value}@ function. + ModifyUse IR.Exp + | -- | @map_ g@ around an inner use. + MapUse IR.Exp OpUse + +{- | What a run of the foreign @write@ evaluates to: the @Effect.Ref@ +implementation returns nothing (@nil@ at the call site), the ST one +returns the written value. +-} +data WriteResult = WriteResultNil | WriteResultWritten + +data OpKind = ReadKind | WriteKind WriteResult | ModifyKind | MapKind + +{- | The cell operations, keyed by the qualified name recognition +matches on (see the module haddock). +-} +opKinds ∷ Map IR.QName OpKind +opKinds = + Map.fromList + [ (IR.QName effectRefModule (IR.Name "read"), ReadKind) + , (IR.QName stModule (IR.Name "read"), ReadKind) + , (IR.QName effectRefModule (IR.Name "write"), WriteKind WriteResultNil) + , (IR.QName stModule (IR.Name "write"), WriteKind WriteResultWritten) + , (IR.QName effectRefModule (IR.Name "modifyImpl"), ModifyKind) + , (IR.QName stModule (IR.Name "modifyImpl"), ModifyKind) + , (IR.QName stModule (IR.Name "map_"), MapKind) + ] + +-- | The cell allocators. +newNames ∷ Set IR.QName +newNames = + Set.fromList + [ IR.QName effectRefModule (IR.Name "_new") + , IR.QName stModule (IR.Name "new") + ] + +effectRefModule, stModule ∷ IR.ModuleName +effectRefModule = IR.ModuleName "Effect.Ref" +stModule = IR.ModuleName "Control.Monad.ST.Internal" + +{- | Recognise the run of a cell allocation: @new v EffectRunArg@. +Yields the initial value. +-} +matchNewRun ∷ IR.Exp → Maybe IR.Exp +matchNewRun = \case + IR.AppN _ann inner (IR.EffectRunArg _ :| []) → do + let (hd, args) = IR.unwindApp inner + qname ← headQName hd + guard (Set.member qname newNames) + case args of + [v] → Just v + _ → Nothing + _ → Nothing + +{- | Recognise the run of a cell operation: @use EffectRunArg@. Yields +the cell (a local binder under GUC) and the parsed use. +-} +matchOpRun ∷ IR.Exp → Maybe (IR.Name, OpUse) +matchOpRun = \case + IR.AppN _ann inner (IR.EffectRunArg _ :| []) → matchOpUse inner + _ → Nothing + +matchOpUse ∷ IR.Exp → Maybe (IR.Name, OpUse) +matchOpUse expr = do + let (hd, args) = IR.unwindApp expr + kind ← (`Map.lookup` opKinds) =<< headQName hd + case (kind, args) of + (ReadKind, [IR.Ref _ (IR.Local cell)]) → + Just (cell, ReadUse) + (WriteKind result, [v, IR.Ref _ (IR.Local cell)]) → + Just (cell, WriteUse result v) + (ModifyKind, [f, IR.Ref _ (IR.Local cell)]) → + Just (cell, ModifyUse f) + (MapKind, [g, inner]) → + second (MapUse g) <$> matchOpUse inner + _ → Nothing + +{- | The qualified name an operation head denotes: a plain imported +reference, or the dissolved foreign-accessor read — the two forms of +'Language.PureScript.Backend.Lua.NativeLoop.headQName'. +-} +headQName ∷ IR.Exp → Maybe IR.QName +headQName = \case + IR.Ref _ann (IR.Imported modname name) → Just (IR.QName modname name) + expr → foreignAccessorQName expr + +-------------------------------------------------------------------------------- +-- Analysis -------------------------------------------------------------------- + +{- | Does every occurrence of the cell in the given expressions sit in a +position the lowering rewrites? Run positions qualify — a direct +'IR.EffectRunArg' run anywhere, and the body of a literal loop lambda +(run per iteration by 'NativeLoop.runStatements'). A naked occurrence — +the cell flowing somewhere as a whole value — disqualifies it. +-} +usedOnlyThroughOps ∷ IR.Name → [IR.Exp] → Bool +usedOnlyThroughOps cell = all ok + where + ok ∷ IR.Exp → Bool + ok expr + | Just (r, use) ← matchOpRun expr = + if r == cell + then all ok (opUseArgs use) + else all ok (toListOf IR.subexpressions expr) + | Just loop ← NativeLoop.matchLoopRun expr = case loop of + NativeLoop.ForeachLoop arr f → ok arr && okLoopFunction f + NativeLoop.ForRangeLoop lo hi f → ok lo && ok hi && okLoopFunction f + -- The while condition and a non-thunk body are compiled as + -- values (pre-bound, called per iteration); a thunk body's + -- statements carry their own run markers. Plain recursion + -- checks all of these correctly. + NativeLoop.WhileLoop cond body → ok cond && ok body + | IR.Ref _ (IR.Local r) ← expr = r /= cell + | otherwise = all ok (toListOf IR.subexpressions expr) + + -- The literal unary lambda of a for/foreach run has its body run per + -- iteration: a thunk body is spliced (its statements carry their own + -- run markers), any other body is run as a whole, so it is checked + -- with the marker 'NativeLoop.runStatements' attaches before + -- compiling. A non-lambda argument is pre-bound and called: a value. + okLoopFunction ∷ IR.Exp → Bool + okLoopFunction = \case + IR.AbsN _ (_param :| []) body + | IR.AbsN _ (IR.ParamUnused _ :| []) _ ← body → ok body + | otherwise → ok (IR.App IR.noAnn body (IR.EffectRunArg IR.noAnn)) + f → ok f + +{- | The non-reference arguments of a use: ordinary expressions the +analysis recurses into and the lowering compiles. +-} +opUseArgs ∷ OpUse → [IR.Exp] +opUseArgs = \case + ReadUse → [] + WriteUse _result v → [v] + ModifyUse f → [f] + MapUse g inner → g : opUseArgs inner + +-------------------------------------------------------------------------------- +-- Lowering -------------------------------------------------------------------- + +{- | Lower the run of a recognised use of an unboxed cell to statements +plus the run's value. The first argument compiles an IR expression to a +Lua expression (the caller's own recursion), the second mints a fresh +Lua-side local name from a prefix. +-} +lowerOpRun + ∷ ∀ m + . Monad m + ⇒ (IR.Exp → m Lua.Exp) + → (Text → m Name.Name) + → Name.Name + → OpUse + → m ([Lua.Statement], Lua.Exp) +lowerOpRun compileExp fresh cell = go + where + go ∷ OpUse → m ([Lua.Statement], Lua.Exp) + go = \case + ReadUse → pure ([], Lua.varName cell) + WriteUse result v → do + v' ← compileExp v + pure + ( [Lua.assignVar cell v'] + , case result of + WriteResultNil → Lua.Nil + WriteResultWritten → Lua.varName cell + ) + ModifyUse f → lowerModify f + MapUse g inner → case g of + -- \_ → gbody: the boxed call evaluates gbody after running the + -- inner action, its value discarded (but still evaluated — a + -- non-atomic value is a call whose effects must be kept). + IR.AbsN _ (IR.ParamUnused _ :| []) gbody → do + (stmts, innerValue) ← go inner + gbody' ← compileExp gbody + pure (stmts <> NativeLoop.dropValue innerValue, gbody') + -- \x → gbody: bind the inner value to the parameter and continue + -- with the body — the beta the boxed closure call performs. + IR.AbsN _ (IR.ParamNamed _ x :| []) gbody → do + (stmts, innerValue) ← go inner + gbody' ← compileExp gbody + pure (stmts <> [Lua.local1 (fromName x) innerValue], gbody') + -- Anything else: call the compiled function on the inner value. + -- The boxed code evaluates the function expression before running + -- the action, so a non-atomic one is pre-bound to keep that order. + _ → do + g' ← compileExp g + (gPre, gAtom) ← preBind "$g" g' + (stmts, innerValue) ← go inner + pure (gPre <> stmts, Lua.functionCall gAtom [innerValue]) + + lowerModify ∷ IR.Exp → m ([Lua.Statement], Lua.Exp) + lowerModify f + | IR.AbsN _ (param :| []) fbody ← f + , Just (spine, fields) ← recordTail fbody = do + let paramStmt = case param of + IR.ParamNamed _ann s → [Lua.local1 (fromName s) (Lua.varName cell)] + IR.ParamUnused _ann → [] + spineStmts ← forM spine \(name, expr) → + Lua.local1 (fromName name) <$> compileExp expr + -- The record build evaluates its fields in written order before + -- the foreign assigns the state; pre-binding non-atomic fields + -- keeps that order across the cell assignment. + let ((prop1, fieldExp1), (_prop2, fieldExp2)) = fields + (pre1, atom1) ← preBind "$v" =<< compileExp fieldExp1 + (pre2, atom2) ← preBind "$v" =<< compileExp fieldExp2 + let fieldStmts = pre1 <> pre2 + (stateExp, valueExp) + | prop1 == statePropName = (atom1, atom2) + | otherwise = (atom2, atom1) + pure + ( paramStmt + <> spineStmts + <> fieldStmts + <> [Lua.assignVar cell stateExp] + , valueExp + ) + | otherwise = do + f' ← compileExp f + t ← fresh "$t" + pure + ( + [ Lua.local1 t (Lua.functionCall f' [Lua.varName cell]) + , Lua.assignVar cell (Lua.varField (Lua.varName t) stateName) + ] + , Lua.varField (Lua.varName t) valueName + ) + + preBind ∷ Text → Lua.Exp → m ([Lua.Statement], Lua.Exp) + preBind prefix e + | isAtom e = pure ([], e) + | otherwise = do + name ← fresh prefix + pure ([Lua.local1 name e], Lua.varName name) + + isAtom ∷ Lua.Exp → Bool + isAtom = \case + Lua.Nil → True + Lua.Boolean _ → True + Lua.Integer _ → True + Lua.Float _ → True + Lua.String _ → True + Lua.Var (Lua.Ann (Lua.VarName _)) → True + _ → False + +{- | Lower the run of a recognised use in @Let@-statement position: the +binder receives the run's value (or nothing, for the discard binder). +A lowering that declares locals is scoped in a @do … end@ block so +modify spines cannot accumulate against the enclosing function's +@LUAI_MAXVARS@ budget; the binder, declared outside, is assigned within. +-} +lowerOpRunBinding + ∷ ∀ m + . Monad m + ⇒ (IR.Exp → m Lua.Exp) + → (Text → m Name.Name) + → Maybe Name.Name + → Name.Name + → OpUse + → m [Lua.Statement] +lowerOpRunBinding compileExp fresh binder cell use = do + (stmts, value) ← lowerOpRun compileExp fresh cell use + let declaresLocals = any declaresLocal stmts + pure case binder of + Nothing + | declaresLocals → + [Lua.Do (Lua.ann <$> (stmts <> NativeLoop.dropValue value))] + | otherwise → stmts <> NativeLoop.dropValue value + Just name + | declaresLocals → + [ Lua.local0 name + , Lua.Do (Lua.ann <$> (stmts <> [Lua.assignVar name value])) + ] + | otherwise → stmts <> [Lua.local1 name value] + where + declaresLocal ∷ Lua.Statement → Bool + declaresLocal = \case + Lua.Local {} → True + Lua.LocalFunction {} → True + _ → False + +-------------------------------------------------------------------------------- +-- Helpers --------------------------------------------------------------------- + +{- | Split a modify function body into its @Let@ spine and the final +manifest two-field @{state, value}@ record build, fields in written +order. 'Nothing' when the body has any other tail, a recursive spine +group, or extra fields — those fall back to calling the compiled +function. +-} +recordTail + ∷ IR.Exp + → Maybe ([(IR.Name, IR.Exp)], ((IR.PropName, IR.Exp), (IR.PropName, IR.Exp))) +recordTail = \case + IR.LiteralObject _ann [field1@(p1, _), field2@(p2, _)] + | (p1 == statePropName && p2 == valuePropName) + || (p1 == valuePropName && p2 == statePropName) → + Just ([], (field1, field2)) + IR.Let _ann bindings body → do + spine ← forM (toList bindings) \case + IR.Standalone (_ann, name, expr) → Just (name, expr) + IR.RecursiveGroup _ → Nothing + first (spine <>) <$> recordTail body + _ → Nothing + +statePropName, valuePropName ∷ IR.PropName +statePropName = IR.PropName "state" +valuePropName = IR.PropName "value" + +stateName, valueName ∷ Name.Name +stateName = Name.unsafeName "state" +valueName = Name.unsafeName "value" + +fromName ∷ IR.Name → Name.Name +fromName = Name.makeSafe . IR.nameToText diff --git a/pslua.cabal b/pslua.cabal index 3d9d4373..d474f89c 100644 --- a/pslua.cabal +++ b/pslua.cabal @@ -163,6 +163,7 @@ library Language.PureScript.Backend.Lua.Parser Language.PureScript.Backend.Lua.Printer Language.PureScript.Backend.Lua.Promote + Language.PureScript.Backend.Lua.RefUnbox Language.PureScript.Backend.Lua.Renumber Language.PureScript.Backend.Lua.Run Language.PureScript.Backend.Lua.Traversal diff --git a/test/ps/output/Golden.MixedDiscardFloat.Test/golden.lua b/test/ps/output/Golden.MixedDiscardFloat.Test/golden.lua index 12a807d8..795a741a 100644 --- a/test/ps/output/Golden.MixedDiscardFloat.Test/golden.lua +++ b/test/ps/output/Golden.MixedDiscardFloat.Test/golden.lua @@ -1,5 +1,6 @@ +local M = {} local Data_Unit_foreign = { unit = {} } -local Data_Unit_unit = Data_Unit_foreign.unit +M.Data_Unit_unit = Data_Unit_foreign.unit local Data_Show_foreign = { showIntImpl = function(n) return tostring(n) end } local Control_Monad_ST_Internal_foreign = { map_ = function(f) @@ -20,11 +21,9 @@ local Effect_Console_foreign = { } local Effect_Console_log = Effect_Console_foreign.log local Golden_MixedDiscardFloat_Test_stCount = Control_Monad_ST_Internal_foreign.run(function( ) - local r = Control_Monad_ST_Internal_foreign.new(1)() - local _ = Control_Monad_ST_Internal_foreign.map_(function() - return Data_Unit_unit - end)(Control_Monad_ST_Internal_foreign.write(2)(r))() - return Control_Monad_ST_Internal_foreign.read(r)() + local r = 1 + r = 2 + return r end) return (function() local _ = Effect_Console_log("st:")() diff --git a/test/ps/output/Golden.MixedEffectSTDo.Test/golden.lua b/test/ps/output/Golden.MixedEffectSTDo.Test/golden.lua index f610bbcb..4ff0400e 100644 --- a/test/ps/output/Golden.MixedEffectSTDo.Test/golden.lua +++ b/test/ps/output/Golden.MixedEffectSTDo.Test/golden.lua @@ -23,12 +23,9 @@ local Effect_Console_foreign = { local Effect_Console_log = Effect_Console_foreign.log local Golden_MixedEffectSTDo_Test_tally = function(start) return Control_Monad_ST_Internal_foreign.run(function() - local ref = Control_Monad_ST_Internal_foreign.new(start)() - local _ = Control_Monad_ST_Internal_foreign.modifyImpl(function(s_S_0) - local sPrime_S_0 = s_S_0 * 2 - return { state = sPrime_S_0, value = sPrime_S_0 } - end)(ref)() - local n = Control_Monad_ST_Internal_foreign.read(ref)() + local ref = start + do local s_S_0 = ref local sPrime_S_0 = s_S_0 * 2 ref = sPrime_S_0 end + local n = ref return n + 3 end) end diff --git a/test/ps/output/Golden.NativeLoopsST.Test/golden.lua b/test/ps/output/Golden.NativeLoopsST.Test/golden.lua index 60862575..aa5fb9d4 100644 --- a/test/ps/output/Golden.NativeLoopsST.Test/golden.lua +++ b/test/ps/output/Golden.NativeLoopsST.Test/golden.lua @@ -1,5 +1,6 @@ +local M = {} local Data_Unit_foreign = { unit = {} } -local Data_Unit_unit = Data_Unit_foreign.unit +M.Data_Unit_unit = Data_Unit_foreign.unit local Data_Show_foreign = { showIntImpl = function(n) return tostring(n) end } local Control_Monad_ST_Internal_foreign = { map_ = function(f) @@ -49,34 +50,30 @@ local Golden_NativeLoopsST_Test_logShow = function(a_S_0) end local Golden_NativeLoopsST_Test_sumTo = function(n) return Control_Monad_ST_Internal_run(function() - local acc = Control_Monad_ST_Internal_new(0)() + local acc = 0 for i = 0, n + 1 - 1 do - Control_Monad_ST_Internal_modifyImpl(function(s_S_0) - local sPrime_S_0 = s_S_0 + i - return { state = sPrime_S_0, value = sPrime_S_0 } - end)(acc)() + local s_S_0 = acc + local sPrime_S_0 = s_S_0 + i + acc = sPrime_S_0 end - return Control_Monad_ST_Internal_read(acc)() + return acc end) end local Golden_NativeLoopsST_Test_sumArray = function(xs) return Control_Monad_ST_Internal_run(function() - local acc = Control_Monad_ST_Internal_new(0)() + local acc = 0 for _S_i0 = 1, #(xs) do local x = xs[_S_i0] - Control_Monad_ST_Internal_map_(function() - return Data_Unit_unit - end)(Control_Monad_ST_Internal_modifyImpl(function(s_S_1) - local sPrime_S_1 = s_S_1 + x - return { state = sPrime_S_1, value = sPrime_S_1 } - end)(acc))() + local s_S_1 = acc + local sPrime_S_1 = s_S_1 + x + acc = sPrime_S_1 end - return Control_Monad_ST_Internal_read(acc)() + return acc end) end local Golden_NativeLoopsST_Test_countDown = function(start) return Control_Monad_ST_Internal_run(function() - local steps = Control_Monad_ST_Internal_new(0)() + local steps = 0 local value = Control_Monad_ST_Internal_new(start)() do local _S_cond0 = Control_Monad_ST_Internal_map_(function(v) @@ -87,13 +84,12 @@ local Golden_NativeLoopsST_Test_countDown = function(start) local sPrime_S_2 = s_S_2 - 1 return { state = sPrime_S_2, value = sPrime_S_2 } end)(value)() - Control_Monad_ST_Internal_modifyImpl(function(s_S_3) - local sPrime_S_3 = s_S_3 + 1 - return { state = sPrime_S_3, value = sPrime_S_3 } - end)(steps)() + local s_S_3 = steps + local sPrime_S_3 = s_S_3 + 1 + steps = sPrime_S_3 end end - return Control_Monad_ST_Internal_read(steps)() + return steps end) end return (function() diff --git a/test/ps/output/Golden.RecordSurgery.Test/golden.lua b/test/ps/output/Golden.RecordSurgery.Test/golden.lua index 5a7b78f8..2dfe1b8c 100644 --- a/test/ps/output/Golden.RecordSurgery.Test/golden.lua +++ b/test/ps/output/Golden.RecordSurgery.Test/golden.lua @@ -24,7 +24,7 @@ local Effect_Console_foreign = { log = function(s) return function() print(s) end end } local Effect_Console_log = Effect_Console_foreign.log -local Effect_Ref_foreign = { +M.Effect_Ref_foreign = { _new = function(val) return function() return { value = val } end end, read = function(ref) return function() return ref.value end end } @@ -40,8 +40,8 @@ M.Golden_RecordSurgery_Test_replaced = { a = 3 } M.Golden_RecordSurgery_Test_inserted = { a = 1, b = 2 } M.Golden_RecordSurgery_Test_deleted = { b = 2 } return (function() - local ref_S_0 = Effect_Ref_foreign._new({ k = 9, n = 1 })() - local dyn_S_0 = Effect_Ref_foreign.read(ref_S_0)() + local ref_S_0 = { k = 9, n = 1 } + local dyn_S_0 = ref_S_0 local _ = Golden_RecordSurgery_Test_logShow(3)() local _ = Golden_RecordSurgery_Test_logShow(3)() local _ = Golden_RecordSurgery_Test_logShow(2)() diff --git a/test/ps/output/Golden.RefUnbox.Test/golden.lua b/test/ps/output/Golden.RefUnbox.Test/golden.lua index 69365978..369c8c43 100644 --- a/test/ps/output/Golden.RefUnbox.Test/golden.lua +++ b/test/ps/output/Golden.RefUnbox.Test/golden.lua @@ -1,3 +1,4 @@ +local M = {} local Data_Show_foreign = { showIntImpl = function(n) return tostring(n) end } local Effect_Ref_foreign = { _new = function(val) return function() return { value = val } end end, @@ -6,7 +7,7 @@ local Effect_Ref_foreign = { return function(ref) return function() ref.value = val end end end } -local Effect_Ref_read = Effect_Ref_foreign.read +M.Effect_Ref_read = Effect_Ref_foreign.read local Control_Monad_ST_Internal_foreign = { pure_ = function(a) return function() return a end end, bind_ = function(a) @@ -52,37 +53,42 @@ local Golden_RefUnbox_Test_logShow = function(a_S_0) return Effect_Console_foreign.log(Data_Show_foreign.showIntImpl(a_S_0)) end local Golden_RefUnbox_Test_writeBack = Control_Monad_ST_Internal_run(function() - local r = Control_Monad_ST_Internal_new(1)() - local x = Control_Monad_ST_Internal_foreign.write(7)(r)() - local y = Control_Monad_ST_Internal_read(r)() + local r = 1 + r = 7 + local x = r + local y = r return x + y end) local Golden_RefUnbox_Test_sumTo = function(n) return Control_Monad_ST_Internal_run(function() - local acc = Control_Monad_ST_Internal_new(0)() + local acc = 0 for i = 0, n + 1 - 1 do - Control_Monad_ST_Internal_modifyImpl(function(s_S_0) - local sPrime_S_0 = s_S_0 + i - return { state = sPrime_S_0, value = sPrime_S_0 } - end)(acc)() + local s_S_0 = acc + local sPrime_S_0 = s_S_0 + i + acc = sPrime_S_0 end - return Control_Monad_ST_Internal_read(acc)() + return acc end) end local Golden_RefUnbox_Test_splitModify = function(n) return Control_Monad_ST_Internal_run(function() - local r = Control_Monad_ST_Internal_new(n)() - local v = Control_Monad_ST_Internal_modifyImpl(function(s) - return { state = s * 2, value = s + 100 } - end)(r)() - local s0 = Control_Monad_ST_Internal_read(r)() + local r = n + local v + do + local s = r + local _S_v0 = s * 2 + local _S_v1 = s + 100 + r = _S_v0 + v = _S_v1 + end + local s0 = r return v + s0 end) end local Golden_RefUnbox_Test_nested = Control_Monad_ST_Internal_run(function() local inner = Control_Monad_ST_Internal_new(21)() - local outer = Control_Monad_ST_Internal_new(inner)() - local cell = Control_Monad_ST_Internal_read(outer)() + local outer = inner + local cell = outer local _ = Control_Monad_ST_Internal_modifyImpl(function(s_S_1) local sPrime_S_1 = s_S_1 * 2 return { state = sPrime_S_1, value = sPrime_S_1 } @@ -94,9 +100,9 @@ return (function() local _ = Golden_RefUnbox_Test_logShow(Golden_RefUnbox_Test_splitModify(3))() local _ = Golden_RefUnbox_Test_logShow(Golden_RefUnbox_Test_writeBack)() local _ = Golden_RefUnbox_Test_logShow(Golden_RefUnbox_Test_nested)() - local counter_S_0 = Effect_Ref_foreign._new(10)() - local v_S_0 = Effect_Ref_read(counter_S_0)() - local _ = Effect_Ref_foreign.write(Data_Semiring_semiringInt.add(v_S_0)(1))(counter_S_0)() - local w_S_0 = Effect_Ref_read(counter_S_0)() + local counter_S_0 = 10 + local v_S_0 = counter_S_0 + counter_S_0 = Data_Semiring_semiringInt.add(v_S_0)(1) + local w_S_0 = counter_S_0 return Golden_RefUnbox_Test_logShow(w_S_0)() end)() diff --git a/test/ps/output/Golden.STDoBlock.Test/golden.lua b/test/ps/output/Golden.STDoBlock.Test/golden.lua index acc6526b..3e9fc5d5 100644 --- a/test/ps/output/Golden.STDoBlock.Test/golden.lua +++ b/test/ps/output/Golden.STDoBlock.Test/golden.lua @@ -1,3 +1,4 @@ +local M = {} local Data_Show_foreign = { showIntImpl = function(n) return tostring(n) end } local Control_Monad_ST_Internal_foreign = { pure_ = function(a) return function() return a end end, @@ -17,7 +18,7 @@ local Control_Monad_ST_Internal_foreign = { end end } -local Control_Monad_ST_Internal_modifyImpl = Control_Monad_ST_Internal_foreign.modifyImpl +M.Control_Monad_ST_Internal_modifyImpl = Control_Monad_ST_Internal_foreign.modifyImpl local Effect_Console_foreign = { log = function(s) return function() print(s) end end } @@ -27,10 +28,10 @@ local Golden_STDoBlock_Test_sumTwice = function(n) local sPrime_S_0 = s_S_0 + n return { state = sPrime_S_0, value = sPrime_S_0 } end - local ref = Control_Monad_ST_Internal_foreign.new(0)() - local _ = Control_Monad_ST_Internal_modifyImpl(_S_cse0)(ref)() - local _ = Control_Monad_ST_Internal_modifyImpl(_S_cse0)(ref)() - local total = Control_Monad_ST_Internal_foreign.read(ref)() + local ref = 0 + do local _S_t0 = _S_cse0(ref) ref = _S_t0.state local _ = _S_t0.value end + do local _S_t1 = _S_cse0(ref) ref = _S_t1.state local _ = _S_t1.value end + local total = ref return total + 1 end) end From 772677196e68d5c36955c0dd852c5936f26cb093 Mon Sep 17 00:00:00 2001 From: Yura Lazarev Date: Mon, 27 Jul 2026 12:21:08 +0200 Subject: [PATCH 3/4] bench(macro): add the RefLoop spec for the unboxed ST accumulator A hot ST loop accumulating through a non-escaping local STRef, with the hand-written Lua for-loop as its ideal. The committed counter oracles pin the unboxed shape: one steady-state TNEW/TDUP site in the whole artifact (the dead foreign new closure) and the hot loop trace-compiled (JFORL) instead of aborted on the per-iteration closures. Wall-clock on this machine: PUC Lua 5.1 0.0347s -> 0.0009s, LuaJIT 0.024s -> 0.0001s at n=100000. --- bench/goldens/fnew_Bench.RefLoop.txt | 19 +++++++++++++++++++ bench/goldens/tnew_Bench.RefLoop.txt | 8 ++++++++ bench/goldens/trace_ref_loop.txt | 10 ++++++++++ bench/macro/ref_loop.lua | 15 +++++++++++++++ test/ps/src/Bench/RefLoop.purs | 17 +++++++++++++++++ 5 files changed, 69 insertions(+) create mode 100644 bench/goldens/fnew_Bench.RefLoop.txt create mode 100644 bench/goldens/tnew_Bench.RefLoop.txt create mode 100644 bench/goldens/trace_ref_loop.txt create mode 100644 bench/macro/ref_loop.lua create mode 100644 test/ps/src/Bench/RefLoop.purs diff --git a/bench/goldens/fnew_Bench.RefLoop.txt b/bench/goldens/fnew_Bench.RefLoop.txt new file mode 100644 index 00000000..31384f3f --- /dev/null +++ b/bench/goldens/fnew_Bench.RefLoop.txt @@ -0,0 +1,19 @@ +chunk: Bench.RefLoop.lua +runtime: LuaJIT 2.1.1741730670 +main-chunk FNEW: 8 +function-body FNEW: 12 +total FNEW: 20 +prototypes: 21 +function-body FNEW sites: + Bench.RefLoop.lua:6 + Bench.RefLoop.lua:6 + Bench.RefLoop.lua:9 + Bench.RefLoop.lua:9 + Bench.RefLoop.lua:17 + Bench.RefLoop.lua:16 + Bench.RefLoop.lua:15 + Bench.RefLoop.lua:19 + Bench.RefLoop.lua:20 + Bench.RefLoop.lua:28 + Bench.RefLoop.lua:27 + Bench.RefLoop.lua:41 diff --git a/bench/goldens/tnew_Bench.RefLoop.txt b/bench/goldens/tnew_Bench.RefLoop.txt new file mode 100644 index 00000000..81cc5967 --- /dev/null +++ b/bench/goldens/tnew_Bench.RefLoop.txt @@ -0,0 +1,8 @@ +chunk: Bench.RefLoop.lua +runtime: LuaJIT 2.1.1741730670 +main-chunk TNEW+TDUP: 5 +function-body TNEW+TDUP: 1 +total TNEW+TDUP: 6 +prototypes: 21 +function-body TNEW+TDUP sites: + Bench.RefLoop.lua:19 TDUP diff --git a/bench/goldens/trace_ref_loop.txt b/bench/goldens/trace_ref_loop.txt new file mode 100644 index 00000000..ba0c3bfd --- /dev/null +++ b/bench/goldens/trace_ref_loop.txt @@ -0,0 +1,10 @@ +spec: ref_loop +runtime: LuaJIT 2.1.1741730670 +workload: n=100000 reps=4 result=4999950000 +aborts (distinct site -- reason): + Bench.RefLoop.lua:41 -- NYI: bytecode FNEW +bytecode end state (J*=compiled, I*=blacklisted): + Bench.RefLoop.lua:11 JFUNCF + Bench.RefLoop.lua:35 JFORI + Bench.RefLoop.lua:35 JFORL +counts: aborts=1 compiled=3 blacklisted=0 diff --git a/bench/macro/ref_loop.lua b/bench/macro/ref_loop.lua new file mode 100644 index 00000000..7c40eade --- /dev/null +++ b/bench/macro/ref_loop.lua @@ -0,0 +1,15 @@ +-- A hot ST loop accumulating through a non-escaping local STRef. Unboxed +-- (issue #239) the loop body is plain arithmetic on a Lua local; boxed, +-- every iteration allocates the modify record and indexes the cell table. +return { + artifact = "Bench.RefLoop", + n = 100000, + drive = function(mod, n) + return mod.run(n) + end, + ideal = function(n) + local acc = 0 + for i = 0, n - 1 do acc = acc + i end + return acc + end, +} diff --git a/test/ps/src/Bench/RefLoop.purs b/test/ps/src/Bench/RefLoop.purs new file mode 100644 index 00000000..b5757287 --- /dev/null +++ b/test/ps/src/Bench/RefLoop.purs @@ -0,0 +1,17 @@ +-- | A hot ST loop accumulating through a non-escaping local `STRef` +-- | (issue #239). Boxed, every iteration pays the cell's field accesses +-- | plus a fresh `{state, value}` record from `modify`, and the cell +-- | itself is a heap table; unboxed, the loop is straight-line +-- | arithmetic on a plain Lua local with no table allocation at all. +module Bench.RefLoop where + +import Prelude + +import Control.Monad.ST as ST +import Control.Monad.ST.Ref as STRef + +run :: Int -> Int +run n = ST.run do + acc <- STRef.new 0 + ST.for 0 n \i -> void (STRef.modify (_ + i) acc) + STRef.read acc From 23cda9403a8c2c5e5adcdafd28bc2e4ffee52690 Mon Sep 17 00:00:00 2001 From: Yura Lazarev Date: Mon, 27 Jul 2026 12:21:09 +0200 Subject: [PATCH 4/4] docs(changelog): fragment for #239 cell unboxing --- .../20260727_150000_unisay_unbox_ref_cells.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 changelog.d/20260727_150000_unisay_unbox_ref_cells.md diff --git a/changelog.d/20260727_150000_unisay_unbox_ref_cells.md b/changelog.d/20260727_150000_unisay_unbox_ref_cells.md new file mode 100644 index 00000000..4f156369 --- /dev/null +++ b/changelog.d/20260727_150000_unisay_unbox_ref_cells.md @@ -0,0 +1,43 @@ +### Added + +- Non-escaping `Ref`/`STRef` cells unbox to plain mutable Lua locals + (#239). A cell compiles to a one-field heap table — `new v` allocates + `{value = v}`, every `read`/`write`/`modify` pays a field access — yet + a Lua local captured by inner closures is itself a shared mutable slot + (an upvalue), so when the cell never flows anywhere as a whole value + the table buys nothing. A `Let`-bound run of `new` whose every use is + a recognised operation now lowers to `local r = v`, reads to `r`, and + writes/modifies to assignments, with a literal `modify` function + beta-reduced at emission so its `{state, value}` record is never + allocated. An ST loop accumulator previously compiled to + + ```lua + local acc = Control_Monad_ST_Internal_new(0)() + for i = 0, n + 1 - 1 do + Control_Monad_ST_Internal_modifyImpl(function(s_S_0) + local sPrime_S_0 = s_S_0 + i + return { state = sPrime_S_0, value = sPrime_S_0 } + end)(acc)() + end + return Control_Monad_ST_Internal_read(acc)() + ``` + + now becomes allocation-free straight-line code: + + ```lua + local acc = 0 + for i = 0, n + 1 - 1 do + local s_S_0 = acc + local sPrime_S_0 = s_S_0 + i + acc = sPrime_S_0 + end + return acc + ``` + + Recognition is by qualified name over the cell primitives of + `Effect.Ref` and `Control.Monad.ST.Internal` plus the ST functor's + foreign `map_` (the shape `void (modify f r)` inlines to), and only + run positions lower. A cell used as a first-class value anywhere — + stored, returned, passed to an unknown function — keeps its boxed + form, the soundness guard pinned by `Golden.RefUnbox.Test` (see + `Language.PureScript.Backend.Lua.RefUnbox`).