Skip to content

Commit 9e5169a

Browse files
ewowiclaude
andcommitted
Reserve the names the engine supplies to a live script
MoonLive scripts now read `width`, `height`, `depth`, `x`, `y`, `z` and `t` as system variables the engine hands them, instead of each module secretly pasting declarations on top of the script and hiding them from the UI. An effect is told the size of the layer it draws into, so it can no longer claim a different one; a layout keeps naming its own size, because it is what defines the grid in the first place. Performance: desktop 139 us/tick (7194 fps), esp32 2151 us/tick (464 fps). Core - SysVar/SysVarTable beside BuiltinTable, same shape and lookup: a host registers the names it defines, the compiler resolves them before locals and controls. `t` was a hardcoded identifier check; it is now an ordinary table entry, so there is one resolution path. - Reserved: a control declaration or a `for` variable reusing a system name fails with "name is a system variable". Those are the only two places a script can introduce a name. - The controls arena is split - script controls below kMaxCtrls, system variables above - so adding a control, which renumbers every control offset, cannot move a slot a binding cached a pointer to. - controlSlot() was bounded by controlCount_, so every system-variable write landed on a null pointer and was silently dropped. Bounded by the arena now. - A nested loop reusing the enclosing loop's variable emitted a program that never returned - a watchdog reset from a script a user can type. Refused at compile time. Pre-existing, found by review. - kCodeCap lived as four independent literals whose comments warned they must agree; one core constant now, which each backend derives its buffer from. Light domain - Each binding supplies only what it writes: a layout gets the clock, an effect adds the grid, a modifier adds the coordinate. Supplying a name is what reserves it, so `x` and `y` stay usable as loop counters where nothing hands out a coordinate, and a layout asking for `width` is an error rather than a silent zero that places no lights. - sin/cos over math16, and turn(n) - one revolution split n ways. A full turn is 65536, one past the largest number a script can write, so a circle cannot be spelled without it. - Layout scripts name their own controls (cols/rows/radius); ring.mlv added, which places 24 lights in an 11x11 box - the case where light count and grid size differ. - The script-size constant and its rationale were copy-pasted into three bindings; one kMaxScriptBytes now. Tests - Reserved names, per-binding supply, and the nested-loop hang are each pinned. The script sweep compiles every .mlv against its own binding's table, so a script cannot read a name its module never writes and still pass. Docs/CI - MIGRATING entry: a persisted layout script declaring `width` no longer compiles and its fixture goes dark until edited. MoonLiveEffect.md documents system variables and the call vocabulary, which had no home before. - Backlog: a scripted modifier cannot reshape the grid yet - system variables are read-only, so modifyLogicalSize needs a writable kind plus a read-back. Reviews - 👾 Reviewer, 11 findings: 8 fixed (the nested-loop hang, the two design findings above, four stale/duplicated comments, the SysVarTable cap). 1 skipped - folding SysVarTable's lookup into a shared template is a refactor beyond this change, and the Reviewer's own conclusion was that two types are correct. 2 were no-defect (arena sizing, reserved-name coverage), both re-verified here. - 🐇 CodeRabbit, 5 findings: 2 fixed in the monitor (a SHA mismatch no longer decodes a backtrace against the wrong ELF; an unreadable ELF degrades to raw addresses instead of ending the session), 1 doc fence tagged. 3 skipped: they describe patching a code buffer that lowerToBytes discards on overflow before it is ever copied out, and an out-of-bounds label index that newLabel() makes unreachable. Known limits - lines.mlv does not compile on Xtensa: fill plus two loops exceeds the S3 register budget. It runs on S31 and desktop. Verified against the real backend; every other shipped script compiles on all three. - Flash on esp32s31 reads +92 KB against a baseline built before this branch's Ethernet work, so most of that is not this change; the comparable S3 figure is +1.8 KB. Flash: esp32 1762368, esp32s3-n16r8 1752128, esp32s31 2024672, desktop 1137800. Tests: 1322 cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4806046 commit 9e5169a

40 files changed

Lines changed: 946 additions & 272 deletions

docs/MIGRATING.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ projectMM ships **no migration code**: the persistence layer is robust by defaul
2020

2121
## Unreleased (`next-iteration`)
2222

23+
### MoonLive: a script can no longer declare a name the engine supplies (2026-08-10)
24+
25+
`t` (elapsed milliseconds), `width`/`height`/`depth` (the logical grid) and `x`/`y`/`z` (the light a modifier is transforming) are now **system variables** the engine supplies, so a script cannot declare one. Previously each binding faked them by prepending hidden declarations to the script, which meant an effect could declare its own `width` and quietly disagree with the layer it was drawing into.
26+
27+
Each module supplies only the names it writes, so what is reserved depends on the module: a layout gets `t` alone, an effect adds the grid, a modifier adds the coordinate. **`x` and `y` remain usable as loop counters in a layout or an effect.**
28+
29+
**Action: *update a file*, for scripted layouts only.**
30+
31+
A **layout** is the one script that legitimately used those names for its own controls: it *defines* where lights are, so it has no grid to be handed. A persisted layout script declaring `uint8_t width = 16;` now fails to compile with `name is a system variable`, and the layout places no lights — the fixture is **dark** until the script is edited.
32+
33+
| What | Why | What to do |
34+
|---|---|---|
35+
| A scripted layout declaring `width`/`height` | The name is what the layout is defining, so the declaration is a compile error and no lights are placed | Edit the script's `source` control, renaming its own controls (the shipped `grid.mlv` uses `cols`/`rows`) |
36+
| A scripted **modifier** using `x`, `y` or `z` as a loop variable | A modifier IS handed a coordinate under those names, so they cannot also be counters there | Rename the loop variable to something the modifier is not handed (`i`, `n`) |
37+
38+
Effects and modifiers need no change: they were already being handed these values, just through a preamble instead of by name. The error names the clash, and the module shows it on its card, so a broken script says why rather than failing silently.
39+
2340
### The `Layers` container is renamed to `Effects` (2026-08-08)
2441

2542
The three top-level light containers are now **Layouts, Effects, Drivers** — L.E.D. The old name sat one character from its own child (`Layers` holding `Layer`s) and read as a near-twin of `Layouts`, which is the pair a newcomer actually has to tell apart. The tree is unchanged in shape: `Effects``Layer`s → effects and modifiers.

docs/backlog/backlog-light.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ The LED-driver increments **shipped**: increment 1 (RMT/WS2812B single-strand on
284284

285285
**Why it waits.** It is fourteen effects' worth of change across audio-reactive and simulation families, each needing its own judgement about whether to clear, fade, or seed differently — not a mechanical sweep. `unit_Effects_gridsweep.cpp` already measures it (`afterFirst`) and asserts only the settled frame, so the number is visible without blocking.
286286

287+
- **A scripted modifier that reshapes the grid** (2026-08-10). `ModifierBase::modifyLogicalSize` lets a modifier change the logical `width`/`height`/`depth` — a Multiply kaleidoscope grows the grid, a crop shrinks it — and a compiled modifier uses it. A SCRIPTED one cannot: system variables are read-only, so `MoonLiveModifier` writes the box in and never reads it back. Needs a writable system variable — the binding reads the slots after the script returns and reports the result through `modifyLogicalSize` — which is a new `SysVarKind` (or a mutable flag on `SysVar`) plus the read-back, not a new builtin. Until then a scripted modifier can fold coordinates but not resize the grid they live in.
288+
287289
- **Drain MoonLive's `print()` through a queue** (2026-08-09). `print(v)` writes to serial directly, and an EFFECT script runs on the render tick — so a print inside one blocks the frame for as long as the UART takes. The burst cap bounds it (a handful of writes per compile, then a compare and a return), but bounded is not free, and `tick()` is annotated `MM_NONBLOCKING`.
288290

289291
**What it costs when it comes:** a small preallocated record queue the built-in writes into, drained from a housekeeping path through the existing platform output seam. The budget and the burst-spent message stay as they are; only where the bytes are written moves. Worth doing when a script is left with a print in it on a real fixture, which is the case the cap exists for.

docs/metrics/repo-health.json

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,72 @@
11
{
2-
"commit": "32ed3f66",
2+
"commit": "4806046b",
33
"flash": {
4-
"esp32": 1744736,
4+
"esp32": 1762368,
55
"esp32p4-eth": 1600848,
66
"esp32p4-eth-wifi": 1793760,
7-
"esp32s3-n16r8": 1750288,
7+
"esp32s3-n16r8": 1752128,
88
"esp32s3-n8r8": 1666992,
9-
"esp32s31": 1932624,
10-
"desktop": 1120664
9+
"esp32s31": 2024592,
10+
"desktop": 1137800
1111
},
1212
"perf": {
1313
"desktop": {
14-
"tick_us": 150,
15-
"fps": 6666
14+
"tick_us": 138,
15+
"fps": 7246
1616
},
1717
"esp32": {
1818
"tick_us": 2151,
1919
"fps": 464
2020
}
2121
},
2222
"loc": {
23-
"core": 16810,
24-
"light": 24100,
25-
"platform": 12832,
23+
"core": 16946,
24+
"light": 24264,
25+
"platform": 12841,
2626
"ui": 6468,
27-
"test": 40933,
28-
"moondeck": 20233
27+
"test": 41137,
28+
"moondeck": 20308
2929
},
3030
"comments": {
3131
"core": {
32-
"lines": 6316,
33-
"ratio": 0.409
32+
"lines": 6380,
33+
"ratio": 0.41
3434
},
3535
"light": {
36-
"lines": 9317,
37-
"ratio": 0.428
36+
"lines": 9403,
37+
"ratio": 0.429
3838
},
3939
"platform": {
40-
"lines": 4350,
40+
"lines": 4353,
4141
"ratio": 0.374
4242
},
4343
"ui": {
4444
"lines": 1670,
4545
"ratio": 0.274
4646
},
4747
"test": {
48-
"lines": 7094,
48+
"lines": 7141,
4949
"ratio": 0.2
5050
},
5151
"moondeck": {
52-
"lines": 3215,
53-
"ratio": 0.182
52+
"lines": 3235,
53+
"ratio": 0.183
5454
}
5555
},
5656
"tests": {
57-
"cases": 1314,
57+
"cases": 1322,
5858
"scenarios": 23
5959
},
6060
"docs": {
6161
"md_files": 178,
62-
"md_lines": 24322,
62+
"md_lines": 24389,
6363
"plans_files": 91,
64-
"backlog_lines": 3621,
64+
"backlog_lines": 3623,
6565
"lessons_lines": 454,
6666
"claude_md_lines": 135
6767
},
6868
"complexity": {
69-
"functions": 2454,
69+
"functions": 2470,
7070
"over_threshold": 151,
7171
"worst_ccn": 93
7272
}

docs/metrics/repo-health.md

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,51 @@
11
# Repo health
22

3-
Measured at `32ed3f66`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.**
3+
Measured at `4806046b`. Generated by [`moondeck/check/repo_health.py`](../../moondeck/check/repo_health.py) on every KPI-gate run. **Do not edit by hand.**
44

55
Current state only; the trend is this file's git history (`git log -p docs/metrics/repo-health.md`). Nothing here fails a build: the numbers make growth visible, the judgment stays human.
66

77
## Firmware size
88

99
| Target | Flash |
1010
|---|---:|
11-
| desktop | 1,094 KB (−0 KB) |
12-
| esp32 | 1,704 KB |
13-
| esp32p4-eth | 1,563 KB (−0 KB) ✓ |
11+
| desktop | 1,111 KB (+17 KB) |
12+
| esp32 | 1,721 KB (+17 KB) ⚠ |
13+
| esp32p4-eth | 1,563 KB |
1414
| esp32p4-eth-wifi | 1,752 KB |
15-
| esp32s3-n16r8 | 1,709 KB (+0 KB) ⚠ |
15+
| esp32s3-n16r8 | 1,711 KB (+2 KB) ⚠ |
1616
| esp32s3-n8r8 | 1,628 KB |
17-
| esp32s31 | 1,887 KB |
17+
| esp32s31 | 1,977 KB (+90 KB) ⚠ |
1818

1919
## Render performance
2020

2121
| Target | Tick | FPS |
2222
|---|---:|---:|
23-
| desktop | 150 µs (+1 µs) | 6,666 (−45) ⚠ |
24-
| esp32 | 2,151 µs (−2,013 µs) ✓ | 464 (+224) ✓ |
23+
| desktop | 138 µs (−12 µs) | 7,246 (+580) ✓ |
24+
| esp32 | 2,151 µs | 464 |
2525

2626
## Code
2727

2828
| Area | Lines | Comments | Comment share |
2929
|---|---:|---:|---:|
30-
| core | 16,810 (+12) ⚠ | 6,316 | 40.9 % |
31-
| light | 24,100 (+52) ⚠ | 9,317 | 42.8 % (+0.1 %) ⚠ |
32-
| platform | 12,832 (+15) ⚠ | 4,350 | 37.4 % |
30+
| core | 16,946 (+136) ⚠ | 6,380 | 41.0 % (+0.1 %) ⚠ |
31+
| light | 24,264 (+164) ⚠ | 9,403 | 42.9 % (+0.1 %) ⚠ |
32+
| platform | 12,841 (+9) ⚠ | 4,353 | 37.4 % |
3333
| ui | 6,468 | 1,670 | 27.4 % |
34-
| test | 40,933 (+188) ⚠ | 7,094 | 20.0 % |
35-
| moondeck | 20,233 (+85) ⚠ | 3,215 | 18.2 % |
34+
| test | 41,137 (+204) ⚠ | 7,141 | 20.0 % |
35+
| moondeck | 20,308 (+75) ⚠ | 3,235 | 18.3 % (+0.1 %) ⚠ |
3636

3737
## Tests
3838

3939
| Kind | Count |
4040
|---|---:|
41-
| unit cases | 1,314 (+5) ✓ |
41+
| unit cases | 1,322 (+8) ✓ |
4242
| scenarios | 23 |
4343

4444
## Complexity
4545

4646
| Metric | Value |
4747
|---|---:|
48-
| functions | 2,454 |
48+
| functions | 2,470 (+16) ✓ |
4949
| over threshold | 151 |
5050
| worst CCN | 93 |
5151

@@ -54,9 +54,9 @@ Current state only; the trend is this file's git history (`git log -p docs/metri
5454
| Metric | Value |
5555
|---|---:|
5656
| markdown files | 178 |
57-
| markdown lines | 24,322 (+4) ⚠ |
57+
| markdown lines | 24,389 (+67) ⚠ |
5858
| plan files | 91 |
59-
| backlog lines | 3,621 |
59+
| backlog lines | 3,623 (+2) ⚠ |
6060
| lessons lines | 454 |
6161
| CLAUDE.md lines | 135 |
6262

docs/moonmodules/light/MoonLiveEffect.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,45 @@ The functions are **not built into the compiler** — `setRGB`, `fill`, `random1
2727
2828
Declaring the variable is what **creates** the control: `uint8_t <name> = <default>;` becomes a `<name>` slider (default `<default>`, range `0..255`). The trailing `// @control <min>..<max>` only **adjusts that control's range**; it's optional. A declared name used in a statement reads the control's **current** value. Editing a control's slider does **not** recompile — the value lands in the engine's control-values arena and the next render tick reads it (the live-edit guarantee, the *no-reboot* principle). Editing the `source` recompiles and re-derives the control set; a control kept across the edit keeps its slider value, a removed control's saved value drops. Stage 1 is `uint8` only.
2929
30+
### System variables — what the engine hands a script
31+
32+
Some names are **reserved**: the engine defines them, the script only reads them, and a declaration that reuses one is a compile error (`name is a system variable`). Each module supplies the names it actually writes, so a name a script cannot be given is simply unknown there rather than silently reading 0.
33+
34+
| name | what it is | layout | effect | modifier |
35+
|---|---|:-:|:-:|:-:|
36+
| `t` | elapsed milliseconds — the clock an animation is written against | ✓ | ✓ | ✓ |
37+
| `width`, `height`, `depth` | the **logical grid** the script renders into | | ✓ | ✓ |
38+
| `x`, `y`, `z` | the light being transformed | | | ✓ |
39+
40+
Supplying a name is also what reserves it, so the tight lists are what leave `x` and `y` usable as ordinary loop counters in a layout or an effect — neither is handed a coordinate.
41+
42+
`width`/`height`/`depth` are the Layer's own dimensions, derived from the layouts and the modifier chain. An effect is *told* its canvas rather than declaring it: a size restated as a control is a second answer that can disagree with the first, and a script that sets `width` to 16 on an 8×8 panel draws off the edge. A [layout](MoonLiveLayout.md) is upstream of that grid — it is what the dimensions are derived *from* — so it is not given them at all, and names its own controls instead (`cols`, `rows`).
43+
44+
Reserving is what makes the guarantee hold: without it a declaration would silently shadow the value the engine handed in, and the script would disagree with its layer with no error anywhere.
45+
46+
### The vocabulary — what a script can call
47+
48+
Registered by the light domain, not built into the compiler (the core owns only the grammar and a generic call/inline mechanism), so the list is one edit in `MoonLiveBuiltins_light.h`.
49+
50+
| call | does |
51+
|---|---|
52+
| `setRGB(index, r, g, b)` | write one light |
53+
| `setXYZ(index, x, y, z)` | write one position (a [modifier](MoonLiveModifier.md)) |
54+
| `fill(r, g, b)` | write every light |
55+
| `addLight(x, y, z)` | place the next light (a [layout](MoonLiveLayout.md)) |
56+
| `random16(n)` | a value in `[0, n)` |
57+
| `mod(a, b)` | `a % b` — the wrap a cyclic animation needs |
58+
| `beat(bpm, t)` | a `0..65535` sawtooth at `bpm` |
59+
| `beatsin(bpm, t, high)` | a sine `0..high` at `bpm` |
60+
| `scale(value, n)` | a `0..65535` value onto `0..n` — lands a wave on an axis |
61+
| `sin(angle)`, `cos(angle)` | the circle; one turn is `0..65535`, result biased to `0..65535` centred at 32768 |
62+
| `turn(n)` | one revolution split `n` ways — the angle step for placing `n` points on a circle |
63+
| `print(v)` | log a value and return it ([what it costs](../../../moonlive/README.md#debugging-print)) |
64+
65+
`sin`/`cos` return an **unsigned** wave, so a coordinate comes from scaling by the full span and not by half of it: `scale(cos(a), radius * 2 + 1)` sweeps a whole axis, where scaling by `radius` alone would only ever reach one side of centre.
66+
67+
`turn(n)` exists because a full revolution is 65536 — one past the largest number a script can write — and the grammar has no division. Without it, placing `n` points evenly on a circle is not expressible.
68+
3069
### Wire contract — control declaration
3170
3271
The controls are **derived from `source`** (one per declared `uint8` control; the optional `@control` annotation only refines a control's range), then **surfaced in `/api/state`** — the device JSON view the integrator consumes — as regular `uint8` controls alongside `source`. So an integrator sees and writes them exactly like any other control — e.g. `POST /api/control` with `{"module": "ML", "control": "speed", "value": 80}`; they're fully present in the device JSON, just authored in the script rather than fixed in the module. The script's `\n` line breaks are standard JSON string escapes the device decodes, so a multi-line `source` round-trips.

docs/moonmodules/light/MoonLiveLayout.md

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,52 @@ A [layout](layouts.md) is the one part of the pipeline that differs for every ph
1111
The script places every light itself, with a loop. That is the difference from a scripted modifier: the Layer calls a modifier once per light, so its script transforms a single coordinate — a layout has no such per-light call to ride on.
1212

1313
``c
14-
uint8_t width = 16; // @control 1..64
15-
uint8_t height = 16; // @control 1..64
14+
uint8_t cols = 16; // @control 1..64
15+
uint8_t rows = 16; // @control 1..64
1616

17-
for (yy = 0; yy < height; yy = yy + 1) {
18-
for (xx = 0; xx < width; xx = xx + 1) {
19-
addLight(xx, yy, 0);
17+
for (y = 0; y < rows; y = y + 1) {
18+
for (x = 0; x < cols; x = x + 1) {
19+
addLight(x, y, 0);
2020
}
2121
}
2222
``
2323

2424
That is the default: a plain grid, one light per cell. `addLight(x, y, z)` places the next light along the strand — no index, because the order the script calls it in *is* the strand order.
2525

26-
The `width` and `height` lines are the script's own controls, not something the module hands it. A layout is never told how big it is: the pipeline works out the bounding box from the coordinates the layouts actually place, so a size passed in from outside would be a second answer that could disagree with the first.
26+
The `cols` and `rows` lines are the script's own controls, not something the module hands it. A layout is never told how big it is: the pipeline works out the bounding box from the coordinates the layouts actually place, so a size passed in from outside would be a second answer that could disagree with the first.
27+
28+
They are named `cols`/`rows` because `width`, `height` and `depth` are [system variables](MoonLiveEffect.md#system-variables--what-the-engine-hands-a-script) — the logical grid the Layer hands an effect or a modifier. A layout is upstream of that grid, so it names its own controls.
2729

2830
A few shapes that are one line here and a new class otherwise:
2931

3032
``c
3133
// a strand that runs right to left
32-
for (i = 0; i < width; i = i + 1) { addLight(width - 1 - i, 0, 0); }
34+
for (i = 0; i < cols; i = i + 1) { addLight(cols - 1 - i, 0, 0); }
3335

3436
// a diagonal
35-
for (i = 0; i < width; i = i + 1) { addLight(i, i, 0); }
37+
for (i = 0; i < cols; i = i + 1) { addLight(i, i, 0); }
3638

3739
// two rows, stacked
38-
for (i = 0; i < width; i = i + 1) { addLight(i, 0, 0); addLight(i, 1, 0); }
40+
for (i = 0; i < cols; i = i + 1) { addLight(i, 0, 0); addLight(i, 1, 0); }
41+
42+
// a circle: lights and grid cells are not the same number
43+
uint8_t count = 24; // @control 3..255
44+
uint8_t radius = 5; // @control 1..127
45+
for (i = 0; i < count; i = i + 1) {
46+
addLight(scale(cos(i * turn(count)), radius * 2 + 1),
47+
scale(sin(i * turn(count)), radius * 2 + 1), 0);
48+
}
3949
``
4050

4151
### What a script can read
4252

43-
A script reads whatever it declares. `uint8_t width = 16; // @control 1..64` becomes a real slider in the UI, and the loop reads it — which is how a panel gets resized without editing code.
53+
A script reads whatever it declares. `uint8_t cols = 16; // @control 1..64` becomes a real slider in the UI, and the loop reads it — which is how a panel gets resized without editing code.
54+
55+
It also reads `t`, the elapsed milliseconds, which is a [system variable](MoonLiveEffect.md#system-variables--what-the-engine-hands-a-script) rather than a control — the only one a layout is given. `width`/`height`/`depth` name the grid a layout is *defining*, so asking for one is a compile error rather than a silent zero; `x` and `y` are free to use as loop counters.
4456

4557
### Seeing inside a script
4658

47-
`print(v)` logs a value and returns it, so it wraps any part of an expression: `addLight(print(xx), yy, 0)`.
59+
`print(v)` logs a value and returns it, so it wraps any part of an expression: `addLight(print(x), y, 0)`.
4860
It is for debugging and comes back out again — [what print costs](../../../moonlive/README.md#debugging-print).
4961

5062
## How the count is known

docs/moonmodules/light/MoonLiveModifier.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ setXYZ(0, (width - 1 - x) * 2, y, z); // mirror, then stretch
2121

2222
### What a script can read
2323

24-
| name | is |
25-
|---|---|
26-
| `x`, `y`, `z` | the position of the light being folded |
27-
| `width`, `height`, `depth` | the box that position lives in |
24+
`x`, `y`, `z` (the light being folded) and `width`, `height`, `depth` (the box it lives in) are [system variables](MoonLiveEffect.md#system-variables--what-the-engine-hands-a-script) — the engine writes them per call, and a script cannot declare a name that shadows one.
2825

2926
`width` matters more than it looks. A mirror written against a fixed `255` sends every light of a 16-wide grid far outside the grid, the Layer discards each one as out of bounds, and the fixture goes black — with no error anywhere, because the script itself ran perfectly.
3027

moondeck/MoonDeck.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ Reads serial at 115200 baud. Output streams to MoonDeck's log and is saved to `e
956956

957957
**Panic backtraces are decoded.** A crash prints `Backtrace: 0x4038456d:0x3fcae310 …`, which says nothing on its own; with `--firmware` each address is resolved against that build's ELF and the function, file and line print underneath:
958958

959-
```
959+
```text
960960
Guru Meditation Error: Core 0 panic'ed (LoadProhibited)
961961
Backtrace: 0x4210b93b:0x3fcc8fa0 0x4200fbf8:0x3fcc8fc0
962962
#0 src/light/moonlive/MoonLiveLayout.h:119

0 commit comments

Comments
 (0)