Commit 71c5dbe
A MoonLive script calls its own functions, and itself
A script's class can now define helper functions and call them, including
calling itself. These are real calls: the callee gets its own frame when it
runs, so one helper can call another and a function can recurse. Recursion is
bounded at run time, so a runaway script leaves the picture wrong where it
stopped instead of resetting the device.
Perf: classic ESP32 1726144 bytes flash (+2732, +0.16% over 2f4c292), tick
2151us / 464fps unchanged; desktop tick 133us. crosshair.mlv (three functions,
two calls per frame) costs 204->219us on the classic; a script with no local
call emits no guard code and is byte-identical.
Core
- IrOp::CallScript, carrying the callee's FUNCTION NUMBER. An IR index was the
first choice and is wrong: the spill pass shifts every index past its first
inserted Reload, so the call named an op that no longer started a function.
- IrProgram::swap() now swaps the function table. It did not, so the spill
pass's remapped boundaries were discarded and the lowering opened a frame two
ops early, mid-statement.
- The recursion depth guard, in the CALLEE's prologue: one copy per function
rather than one per call site, and none at all unless the script calls. A
refusing callee returns, so there is no branch-around at the call site and no
counter to restore across a call. The counter is one arena byte (kDepthSlot),
zeroed per run so a refused frame cannot shrink the next frame's budget.
- The guard is emitted AFTER the host arguments are parked. Both it and the
epilogue reach the arena through the parked frame copy, and a refusing
activation jumps straight to the epilogue.
- An EMPTY function still balances the counter. Its whole body is the argument
parking, so it never reached the flush point while its epilogue still
decremented: two calls wrapped the byte to 255 and the next legal call was
refused as too deep. Found by the Reviewer.
- kAsmLabels/kAsmFixups (48/96) moved into core from three identical private
copies. 16/32 was sized when a script was one routine; a class allocates a
label per function, so crosshair.mlv failed with the generic "too large".
Costs 640 bytes of compile-path stack (lowerWith 480 -> 1120 bytes).
Light domain
- moonlive/effects/crosshair.mlv: the worked example, two helpers and a tick.
Platform
- callLabel(Label) on all three assemblers, reusing the branch fixup machinery
with a per-ISA discriminator, because a call's displacement is encoded
differently from a branch's.
- alignForEntry(): Xtensa requires a 4-byte-aligned `entry`. The toolchain
rejects anything else outright ("unaligned entry instruction") and CALLn
encodes its target in 4-byte units, so an unaligned callee is not
expressible. Instructions are 2 or 3 bytes, so a function following another
lands anywhere. This is what `.align 4` does in hand-written assembly.
- A local call PASSES THE HOST ARGUMENTS ON. Each prologue parks
buf/nLights/cpl/t/ctrls out of the argument registers into its own frame, so
a bare call left the callee parking garbage and its first control read
faulted at EXCVADDR 0x9. On Xtensa they go in a10..a14, since call8 rotates
the window by 8. The contract now lives once in core.
- The host backend's fixup kinds became a FixKind enum, matching its siblings;
the dead condition-code packing went with it.
Tests
- Argument passing one and two calls deep, an empty function not consuming the
budget, and unbounded recursion that must return AND be runnable again at
full depth. Each control-checked by reverting its fix.
- Per-ISA: every function in a class starts where a call can reach it.
- The structural checker gained a calling class and a recursive one, so every
prologue it walks carries the argument reload and the guard. Control-checked
by dropping the window reserve from the frame calculation.
- An assembler stack-budget tripwire, in table entries rather than host bytes.
Docs/CI
- moonlive/README.md: local calls, the recursion bound, and the
declare-helpers-above-callers rule.
- Plan-20260817 step 1 and verification items 1, 2, 5 marked done, with the
three hardware-only traps recorded. Item 5 is done WITH A CAVEAT: a refused
call is silent, where the plan asked for a reported error. That needs a
diagnostic channel from the emitted block to the binding, which does not
exist yet.
Reviews
- 18 findings (Fable). Fixed: the empty-function counter bug; a test named
"calls itself" that contained no self-call (removed, the unbounded test pins
it honestly); a comment describing the abandoned crashing design;
CallScript.imm documented as an IR index in two places; the missing
callLabel/alignForEntry in the assembler contract list; a redundant arena
reload per prologue; the contract duplicated across three backends; dead
fnSeen; host magic fixup numbers; a misplaced comment; two false comments; a
duplicate test; the tripwire's loose arithmetic; the depth limit's off-by-one
between code, pseudo-code and README; US spelling and em-dashes.
Verified on all four boards (S3, S31, classic, P4). The classic separately ran
a deliberately unbounded recursion for 110 seconds at 109fps without resetting.
KPI baselines: 28 scenario values the gate rewrote under build load (up to
+1048%) were reverted; the 7 kept are +1.0% to +6.7% and desktop-only. A quiet
re-run reproduced the kept values and produced no over-10% deltas.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent 2f4c292 commit 71c5dbe
28 files changed
Lines changed: 870 additions & 130 deletions
File tree
- docs
- history/plans
- metrics
- moondeck/moonlive
- moonlive
- effects
- src
- core/moonlive
- platform
- desktop
- esp32
- test
- scenarios/light
- unit/core
Lines changed: 102 additions & 25 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
206 | 206 | | |
207 | 207 | | |
208 | 208 | | |
209 | | - | |
210 | | - | |
211 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
| |||
219 | 219 | | |
220 | 220 | | |
221 | 221 | | |
222 | | - | |
223 | | - | |
224 | | - | |
225 | | - | |
226 | | - | |
227 | | - | |
228 | | - | |
229 | | - | |
230 | | - | |
231 | | - | |
232 | | - | |
233 | | - | |
234 | | - | |
235 | | - | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
240 | 300 | | |
241 | 301 | | |
242 | 302 | | |
| |||
408 | 468 | | |
409 | 469 | | |
410 | 470 | | |
411 | | - | |
| 471 | + | |
412 | 472 | | |
413 | | - | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
414 | 478 | | |
415 | 479 | | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
416 | 485 | | |
417 | 486 | | |
418 | 487 | | |
419 | 488 | | |
420 | 489 | | |
421 | 490 | | |
422 | | - | |
423 | | - | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
424 | 501 | | |
425 | 502 | | |
426 | 503 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | | - | |
5 | | - | |
| 4 | + | |
| 5 | + | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | | - | |
10 | | - | |
| 9 | + | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
| 18 | + | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
30 | 30 | | |
31 | | - | |
32 | | - | |
| 31 | + | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
36 | | - | |
37 | | - | |
| 36 | + | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | | - | |
45 | | - | |
| 44 | + | |
| 45 | + | |
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
| 52 | + | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | | - | |
| 56 | + | |
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
66 | | - | |
| 66 | + | |
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
73 | | - | |
| 73 | + | |
74 | 74 | | |
75 | | - | |
| 75 | + | |
76 | 76 | | |
77 | 77 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
| 11 | + | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
| 27 | + | |
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
34 | | - | |
35 | | - | |
36 | | - | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
37 | 37 | | |
38 | | - | |
39 | | - | |
| 38 | + | |
| 39 | + | |
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
52 | | - | |
53 | | - | |
54 | | - | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
62 | 62 | | |
63 | | - | |
| 63 | + | |
64 | 64 | | |
65 | 65 | | |
66 | 66 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
63 | 68 | | |
64 | 69 | | |
65 | 70 | | |
| |||
0 commit comments