Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions bench/goldens/fnew_Bench.RecordFold.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
chunk: Bench.RecordFold.lua
runtime: LuaJIT 2.1.1741730670
main-chunk FNEW: 5
function-body FNEW: 9
total FNEW: 14
prototypes: 15
function-body FNEW sites:
Bench.RecordFold.lua:10
Bench.RecordFold.lua:9
Bench.RecordFold.lua:20
Bench.RecordFold.lua:19
Bench.RecordFold.lua:44
Bench.RecordFold.lua:43
Bench.RecordFold.lua:42
Bench.RecordFold.lua:51
Bench.RecordFold.lua:50
8 changes: 8 additions & 0 deletions bench/goldens/tnew_Bench.RecordFold.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
chunk: Bench.RecordFold.lua
runtime: LuaJIT 2.1.1741730670
main-chunk TNEW+TDUP: 4
function-body TNEW+TDUP: 1
total TNEW+TDUP: 5
prototypes: 15
function-body TNEW+TDUP sites:
Bench.RecordFold.lua:26 TNEW
15 changes: 15 additions & 0 deletions bench/goldens/trace_record_fold.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
spec: record_fold
runtime: LuaJIT 2.1.1741730670
workload: n=100000 reps=4 result=15000350000
aborts (distinct site -- reason):
Bench.RecordFold.lua:19 -- NYI: bytecode FNEW
Bench.RecordFold.lua:20 -- NYI: bytecode FNEW
Bench.RecordFold.lua:28 -- inner loop in root trace
Bench.RecordFold.lua:50 -- NYI: bytecode FNEW
Bench.RecordFold.lua:51 -- NYI: bytecode FNEW
bytecode end state (J*=compiled, I*=blacklisted):
Bench.RecordFold.lua:17 IFORL
Bench.RecordFold.lua:28 JLOOP
Bench.RecordFold.lua:49 IFUNCF
Bench.RecordFold.lua:50 JFUNCF
counts: aborts=5 compiled=2 blacklisted=2
16 changes: 16 additions & 0 deletions bench/macro/record_fold.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- A fold whose step builds a record, updates it, and reads the result
-- back field-wise -- never passing either table on whole. Unpacked
-- (issue #240) the step is plain arithmetic; boxed, every element
-- allocates the literal plus the update's runtime copy.
return {
artifact = "Bench.RecordFold",
n = 100000,
drive = function(mod, n)
return mod.run(n)
end,
ideal = function(n)
local acc = 0
for i = 1, n do acc = acc + i + (i + 1) * 2 end
return acc
end,
}
33 changes: 33 additions & 0 deletions changelog.d/20260727_180000_unisay_unpack_fieldwise_records.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
### Added

- Let-bound records read only field-wise unpack to scalars (#240). A
record literal or record update whose binder never flows anywhere as
a whole value exists only to be projected or updated again, yet it
still allocated its table — and every update paid a runtime copy on
top. Such a binding now explodes into per-field bindings: reads
become the field values, an update over a known literal is rebuilt
as a single literal, and a chained update coalesces onto its base
record with the patch lists merged. The fold reaches across sibling
`let` bindings, so the defaults pattern dissolves whole. A fold step
previously compiled to

```lua
local r_S_0 = { lo = i_S_0, hi = i_S_0 + 1 }
local s_S_0 = PSLUA_object_update(r_S_0, { hi = r_S_0.hi * 2 })
return acc_S_1 + s_S_0.lo + s_S_0.hi
```

now becomes allocation-free arithmetic:

```lua
return acc_S_1 + i_S_0 + (i_S_0 + 1) * 2
```

4.6× faster under LuaJIT and 5.3× under PUC Lua 5.1 on the
`Bench.RecordFold` macrobenchmark. Locally-built dictionary records
collapse the same way — `Golden.LongWriterBind` drops its Writer
dictionary tower and 57 lines of Lua. A record used as a whole value
anywhere — returned, passed on, compared — keeps its allocation, the
soundness boundary `Golden.ScalarReplacement.Test` pins from both
sides (see `propagateKnownObjectThroughLet` in
`Language.PureScript.Backend.IR.Optimizer`).
Loading