Skip to content

MoonLive: scripts on the filesystem, and a compiler that sizes itself to them - #65

Open
ewowi wants to merge 2 commits into
mainfrom
next-iteration
Open

MoonLive: scripts on the filesystem, and a compiler that sizes itself to them#65
ewowi wants to merge 2 commits into
mainfrom
next-iteration

Conversation

@ewowi

@ewowi ewowi commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Two steps of the MoonLive scalability plan: the compiler stops paying a fixed price per script, and scripts stop living in RAM.

Scripts live on the filesystem

A scripted module carried its script as a fixed 1 KB array, plus a second 1 KB copy to notice edits, plus a name pool — resident whether or not a script was loaded. Six modules held ~16 KB of a classic ESP32's 320 KB for text that was mostly empty.

Now the module holds a name (~32 B). The script is read into a right-sized buffer to compile and freed immediately, so nothing script-sized stays in RAM, and a script is bounded by the filesystem rather than by an array nobody can grow.

The UI loads, edits and saves the file through the /api/file endpoints that already existed — this needed no new backend surface. The rebuild check became a 4-byte FNV-1a hash; it only ever answered "did this change".

The 7-statement wall is gone

IrProgram's op array was a ~2 KB stack member on a 12 KB main task — the same cost for a one-statement script as a full one. Growing it would have traded a compile limit for a stack overflow (this project has lost a P4 to a large stack frame before). It is now heap-allocated, sized from a token count, and RAII-owned.

Seven sequential statements used to fail; forty compile. kMaxIrOps 64 → 4096 is a sanity bound now, not the working limit.

Three bugs, each caught by verification rather than by reading

  • A uint16_t wrap I introduced. Widening count left four uint8_t loop counters iterating over it — three lowerers and IrProgram::hasInline — which wrapped at 256 ops and spun forever. On a device that is a watchdog reset from a script that merely got long. Bisected (60 statements fine, 80 hung). The regression test hangs when the fix is reverted, which is the only reason it is worth having.
  • A dangling pointer. DeclaredControl::name pointed into the source text, which the new loader frees as soon as compiling ends. It surfaced as a control literally named \x05. The engine now copies the names it publishes — which also made three per-binding name pools redundant.
  • /moonlive/ did not exist on a fresh device, and the write endpoint does not create parent directories, so the first script save returned a 500.

Also

ParlioLedDriver asks the platform for its 65535-byte transfer cap instead of naming the number in the light domain, and an over-capacity frame now reports the ceiling in lights per pin on both the reinit and tick paths — the KB figure was the one a user could not act on.

Breaking

source is gone, so a persisted script is an unknown key and is ignored. A MoonLive module boots with no script and renders nothing until one is named. MIGRATING says where to find the old text (/.config/Layouts.json as "N.source") and how to restore it as a file.

Verification

1326 tests, 20 scenarios inside their contracts, GCC build clean, all 10 gates green.

Desktop-verified end to end: a 16×12 scripted grid layout with a scripted lines effect, both compiled from files written over the API, surviving a restart and reloading from persistence.

Not yet run on hardware — the boards were unreachable while this was written. That is the next step, and it matters here: this changes how every scripted module loads, on the platform the work is specifically aimed at.

Known limits

lines.mlv with z-planes still does not compile on any backend — three sweeps with a nested loop name more live values than 14 registers hold, verified with a 64 KB code buffer so it is the register ceiling, not code size. That is step 3 of the plan: spilling to the stack, on its own branch.

Summary by CodeRabbit

  • New Features

    • MoonLive scripts are now stored as files in /moonlive/ and selected through a script control.
    • Script-defined controls are restored automatically after compilation.
    • Larger scripts are supported, with clear diagnostics when scripts exceed limits.
    • LED output now reports actionable capacity errors before transmission.
  • Bug Fixes

    • Prevented long scripts from hanging during compilation.
    • Fixed control names remaining valid after compilation.
    • Improved device monitoring startup and script compilation error handling.
  • Documentation

    • Added migration guidance for converting existing inline scripts to files.
    • Updated MoonLive module documentation and repository metrics.

A scripted module carried its script as a fixed 1 KB array, plus a second copy to notice
edits — resident whether or not a script was loaded, so six modules held ~16 KB of a
classic ESP32's 320 KB for text that was mostly empty. The script now lives in a file; the
module holds its name, reads it into a right-sized buffer to compile, and frees it. Scripts
are bounded by the filesystem instead of by an array nobody can grow.

Performance: desktop 132 us/tick (7575 fps), esp32 2151 us/tick (464 fps).

Light domain
- A `script` control (~32 B) replaces the `source` textarea in all three bindings. The UI
  loads, edits and saves the file through the /api/file endpoints that already existed, so
  this needed no new backend surface. A fresh module reports "no script — set the script
  name" and renders nothing, rather than every new module compiling the same default.
- The rebuild check is a 4-byte FNV-1a of the script text, not a second copy of it. It only
  ever answered "did this change".
- Per-binding control-name pools are gone: the engine owns the names it publishes now, so
  three private copies of the same fact went with them.
- /moonlive/ is created on demand — the write endpoint does not make parent directories, so
  a first save on a fresh device failed with nowhere obvious to look.

Core
- The engine copies declared control NAMES out of the source before returning. They pointed
  into the source text, which the caller is now free to release the moment compile() ends —
  and does. A control briefly appeared named "\x05" before this was found.
- IrProgram's op array is heap-allocated and sized from a token count, RAII-owned (destructor
  frees, copy deleted). It was a ~2 KB stack member on a 12 KB main task, the same cost for a
  one-statement script as a full one — so growing it would have traded a compile limit for a
  stack overflow. SEVEN sequential statements used to fail; forty compile. kMaxIrOps 64 →
  4096 is now a sanity bound, not the working limit.
- Widening that count to uint16_t left four uint8_t loop counters iterating over it — three
  lowerers and IrProgram::hasInline — which wrapped at 256 ops and spun forever. On a device
  that is a watchdog reset from a script that merely got long. Bisected (60 statements fine,
  80 hung); the regression test HANGS when the fix is reverted, which is how it was checked.
- ParlioLedDriver asks the platform for its 65535-byte transfer cap rather than naming the
  number in the light domain, and an over-capacity frame reports the ceiling in lights per
  pin on both the reinit and tick paths — the KB figure was the one a user could not act on.

Tests
- A shared fixture writes each script to a file, so tests exercise the path that ships. It is
  thread-local: the concurrency test compiles from two threads, and a shared name buffer had
  them compiling each other's script.
- Tests that relied on a built-in default script now name one. There is no default any more.

Docs/CI
- MIGRATING: `source` is gone, so a persisted script is an unknown key and ignored — the entry
  says where to find the text (/.config/Layouts.json as "N.source") and how to restore it.
- The three module specs, and the plan's step 1 marked done with what actually shipped.

Verified on the desktop: a 16x12 scripted grid layout with a scripted lines effect, both
compiled from files written over the API, surviving a restart and reloading from persistence.
Not yet run on hardware — the boards were unreachable; that is next.

Flash: esp32 1762368, esp32s3-n16r8 1752992, esp32s31 2025600, esp32p4-eth 1603952,
desktop 1138184. Tests: 1326 cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

MoonLive modules now persist script filenames under /moonlive/ and compile scripts from files. The compiler uses heap-backed IR and staging buffers with larger limits. Platform lowering counters and Parlio transfer-capacity reporting were updated. Tests, documentation, tooling, and metrics reflect these changes.

Changes

MoonLive filesystem scripts

Layer / File(s) Summary
Filesystem script loading and module integration
src/light/moonlive/*, src/core/moonlive/*, docs/moonmodules/light/*, docs/MIGRATING.md, test/unit/light/*
Modules replace persisted source text with script filenames. compileScriptFile validates, reads, hashes, and compiles files. Tests use generated script files and validate recompilation and path handling.

Expandable IR and compilation limits

Layer / File(s) Summary
Heap-backed compilation and code generation
src/core/moonlive/*, src/platform/*moonlive*, moondeck/moonlive/disasm.py, test/unit/core/unit_moonlive_compiler.cpp
IrProgram, compiler staging, and assembler buffers use heap allocation. Capacity is reserved before parsing. Widened counters support larger programs, and oversized scripts report "script too large".

Parlio transfer-budget reporting

Layer / File(s) Summary
Platform capacity and LED-driver checks
src/platform/platform.h, src/platform/*/platform*parlio.cpp, src/light/drivers/*LedDriver.h, docs/performance.md
Platforms report Parlio transfer limits. LED-driver checks use explicit capacity for runtime and initialization paths and fit rounded frames within the capacity.

Supporting updates

Layer / File(s) Summary
Tooling, monitoring, backlog, metrics, and baselines
moondeck/run/monitor_esp32.py, docs/backlog/*, docs/metrics/*, test/scenarios/light/*
Desktop disassembly builds include the platform allocator. Serial capture opens before log-level changes. Watchdog investigation notes, repository metrics, and scenario measurements were updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MoonLiveModule
  participant MoonLiveScriptFile
  participant FileSystem
  participant MoonLive
  MoonLiveModule->>MoonLiveScriptFile: compileScriptFile(script filename)
  MoonLiveScriptFile->>FileSystem: read /moonlive script
  FileSystem-->>MoonLiveScriptFile: source text and content hash
  MoonLiveScriptFile->>MoonLive: compile temporary source buffer
  MoonLive-->>MoonLiveModule: compiled controls or error status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: filesystem-backed MoonLive scripts and compiler buffers sized to script requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch next-iteration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/MIGRATING.md`:
- Around line 23-39: Update the older migration guidance for layout users so it
no longer instructs them to edit the removed source control. Direct them to edit
the corresponding .mlv file through the File Manager, then set the module’s
script control to that filename, consistent with the current filesystem-based
behavior described in the migration document.

In `@src/core/moonlive/MoonLiveIr.h`:
- Line 6: Remove the platform dependency from IrProgram in MoonLiveIr.h by
replacing direct platform::alloc()/platform::free() usage with an injected
core-neutral allocation interface, or relocating runtime allocation ownership
outside src/core. Ensure src/core contains no platform includes and that
disasm.py no longer needs to link the desktop platform implementation solely for
IR storage.

In `@src/light/drivers/ParallelLedDriver.h`:
- Around line 1868-1872: Update reportOverCapacity() to calculate the maximum
light count using the same padded, 64-byte-aligned frame size as
frameBytesFor(), while treating a zero DMA budget as unbounded. Ensure the
reported limit cannot allow a frame exceeding the configured budget, and
preserve the existing one-report-per-geometry behavior at the call site.

In `@src/light/moonlive/MoonLiveEffect.h`:
- Around line 35-49: Update MoonLiveEffect::affectsPrepare() to check for the
"script" control instead of "source", ensuring script filename changes trigger
prepare and recompilation. Add a control-system test that changes the script
control and verifies prepare is invoked.

In `@src/light/moonlive/MoonLiveLayout.h`:
- Around line 118-133: Invalidate the cached compilation when the registered
script control changes, since controls_.addText() updates script_ without
invoking setScript(). Update the relevant MoonLiveLayout control/change handling
so compiledHash_ and engine state cannot satisfy the early-return check for a
new filename, while preserving setScript() behavior. Add a test that changes the
registered script control and verifies the layout recompiles and uses the new
file.

In `@src/light/moonlive/MoonLiveScriptFile.h`:
- Around line 47-50: Update the validation in MoonLiveScriptFile’s script-name
handling before constructing path to accept only a basename with the supported
.mlv suffix. Reject any name containing '/' or '\' and reject traversal
components such as ".."; preserve the existing missing-name error behavior, then
build the path only after validation.
- Around line 47-70: Add a MoonLive operation that invalidates the currently
compiled code without clearing the control arena, then invoke it and reset
hashOut to zero on every failure path before engine.compile() in
MoonLiveScriptFile loading. Cover invalid names, missing/empty/oversized files,
allocation failure, and read failure while preserving existing error messages
and successful compilation behavior.

In `@src/platform/platform.h`:
- Around line 1168-1172: Update the documentation for parlioMaxTransferBytes()
to state that a return value of 0 means no transfer bound, not zero usable
bytes, while positive values represent the hardware’s maximum single-transfer
ceiling. Keep the existing declaration and surrounding allocation guidance
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8b3ecfe3-901d-4e75-a6e2-57e8911ac97a

📥 Commits

Reviewing files that changed from the base of the PR and between 38a28dc and 234e01e.

📒 Files selected for processing (29)
  • docs/MIGRATING.md
  • docs/history/plans/Plan-20260809 - MoonLive scales — right-sized IR, and the stack as the register overflow.md
  • docs/metrics/repo-health.json
  • docs/metrics/repo-health.md
  • docs/moonmodules/light/MoonLiveEffect.md
  • docs/moonmodules/light/MoonLiveLayout.md
  • docs/moonmodules/light/MoonLiveModifier.md
  • moondeck/moonlive/disasm.py
  • src/core/moonlive/MoonLive.cpp
  • src/core/moonlive/MoonLive.h
  • src/core/moonlive/MoonLiveBuiltins.h
  • src/core/moonlive/MoonLiveCompiler.cpp
  • src/core/moonlive/MoonLiveIr.h
  • src/light/drivers/ParallelLedDriver.h
  • src/light/drivers/ParlioLedDriver.h
  • src/light/moonlive/MoonLiveEffect.h
  • src/light/moonlive/MoonLiveLayout.h
  • src/light/moonlive/MoonLiveModifier.h
  • src/light/moonlive/MoonLiveScriptFile.h
  • src/platform/desktop/moonlive_lower_host.cpp
  • src/platform/desktop/platform_desktop.cpp
  • src/platform/esp32/moonlive_lower_riscv.cpp
  • src/platform/esp32/moonlive_lower_xtensa.cpp
  • src/platform/esp32/platform_esp32_parlio.cpp
  • src/platform/platform.h
  • test/unit/core/unit_moonlive_compiler.cpp
  • test/unit/light/MoonLiveScriptFixture.h
  • test/unit/light/unit_MoonLiveLayout.cpp
  • test/unit/light/unit_MoonLiveModifier.cpp
💤 Files with no reviewable changes (1)
  • src/core/moonlive/MoonLiveBuiltins.h

Comment thread docs/MIGRATING.md
#include <cstdint>
#include <cstddef>
#include "core/moonlive/MoonLiveBuiltins.h" // InlineOp (a neutral opcode tag)
#include "platform/platform.h" // alloc/free — the op array is sized to the script

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Keep the core layer independent from the platform layer.

MoonLiveIr.h now imports platform/platform.h, and IrProgram calls platform::alloc() and platform::free(). This breaks the src/core/** boundary. Inject a core-neutral allocation interface, or move the allocation owner outside src/core. The dependency also forces moondeck/moonlive/disasm.py to link the desktop platform implementation.

As per path instructions: “src/core/** … Must be platform-independent — no platform includes.” Based on learnings: “inject a core-neutral executable-code placement interface into MoonLive or relocate the runtime placement layer outside src/core.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/moonlive/MoonLiveIr.h` at line 6, Remove the platform dependency
from IrProgram in MoonLiveIr.h by replacing direct
platform::alloc()/platform::free() usage with an injected core-neutral
allocation interface, or relocating runtime allocation ownership outside
src/core. Ensure src/core contains no platform includes and that disasm.py no
longer needs to link the desktop platform implementation solely for IR storage.

Sources: Coding guidelines, Path instructions, Learnings

Comment thread src/light/drivers/ParallelLedDriver.h
Comment thread src/light/moonlive/MoonLiveEffect.h
Comment thread src/light/moonlive/MoonLiveLayout.h
Comment thread src/light/moonlive/MoonLiveScriptFile.h
Comment on lines +47 to +70
if (!name || !name[0]) { err = "no script — set the script name"; return false; }

char path[96];
std::snprintf(path, sizeof(path), "%s/%s", kScriptDir, name);

const long size = platform::fsSize(path);
if (size < 0) { err = "script not found"; return false; }
if (size == 0) { err = "script is empty"; return false; }
if (size > kScriptFileMax) { err = "script too large"; return false; }

// +1 for the NUL the lexer reads as End. fsRead null-terminates on success, but the buffer has
// to have room for it.
char* text = static_cast<char*>(platform::alloc(static_cast<size_t>(size) + 1));
if (!text) { err = "no memory for the script"; return false; }

const int read = platform::fsRead(path, text, static_cast<size_t>(size) + 1);
if (read <= 0) { platform::free(text); err = "script could not be read"; return false; }

if (hashOut) *hashOut = scriptHash(text, static_cast<size_t>(read));
const bool ok = engine.compile(text, builtins, sysvars);
if (!ok) err = engine.error();
// Freed on BOTH paths, before returning: the text has done its job either way, and a failed
// compile is exactly when a device can least afford to leak.
platform::free(text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate prior code when file loading fails.

These failure paths return before engine.compile() runs. An existing program therefore remains ok(): an effect keeps rendering, a layout keeps placing old coordinates, and a modifier keeps applying its old mapping while the status reports the new file error.

Add a MoonLive operation that drops code while preserving the control arena. Call it on every pre-compile file failure and reset hashOut to zero.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/moonlive/MoonLiveScriptFile.h` around lines 47 - 70, Add a MoonLive
operation that invalidates the currently compiled code without clearing the
control arena, then invoke it and reset hashOut to zero on every failure path
before engine.compile() in MoonLiveScriptFile loading. Cover invalid names,
missing/empty/oversized files, allocation failure, and read failure while
preserving existing error messages and successful compilation behavior.

Comment thread src/platform/platform.h
Hardware found what 1228 tests did not: naming a script never recompiled anything. The
effect still asked whether the "source" control had changed - a control renamed to
"script" - and the layout cached its compiled program behind a hash that a control write
never cleared. Both held a new filename while running the previous script.

Performance: desktop 127 us/tick (7874 fps), esp32 2151 us/tick (464 fps).

Light domain
- MoonLiveEffect::affectsPrepare tests "script". Found on a P4: the effect showed the new
  name and dyn=0, having compiled nothing. The unit tests call prepare() directly, so the
  control-change path had no coverage at all — which is why they passed.
- MoonLiveLayout invalidates its compiled hash when the script control is written.
  addText binds the buffer directly, so a control write never reached setScript() and
  compile()'s early-return kept the old program. Pinned by a test that fails without it.
- A script name is a BASENAME ending in .mlv, rejected otherwise. It was pasted straight
  into the path, so `../.config/NetworkModule.json` would have read the device's saved
  credentials as a script. The fixed directory is the boundary; now it holds.
- reportOverCapacity counts down through frameBytesFor instead of dividing. The frame is
  64-byte rounded, so the division overshot by one: it reported 898 lights per lane, whose
  frame rounds to 65536 against a 65535 cap. A limit that still fails is worse than none.

Core
- MoonLive::compile's staging buffer and each assembler's buf_ are heap-allocated, RAII
  owned, with every write and both branch patchers guarded against a failed allocation.
  That is ~4.1 KB off a compile chain sharing a 12 KB task — the plan named this ("buf_
  inside the assembler, itself a stack local") and step 1 had only done IrProgram, while
  raising kCodeCap 768 → 2048 grew what remained.

Scripts/MoonDeck
- The monitor opens its serial port before probing the network. raised_log_level contacts
  every device in moondeck.json at a 3 s timeout each; with a dozen registered and most
  powered off, that was half a minute before the first byte — losing the boot output it was
  pointed at.

Docs/CI
- MIGRATING no longer tells a layout user to edit the `source` control it just removed.
- The Parlio ceilings are the corrected 897/673/442/332, and platform.h says plainly that a
  0 transfer cap means NO bound rather than zero bytes.
- Backlog: MoonLive compiling watchdogs a classic ESP32 — `rst:0x8 (TG1WDT_SYS_RESET)`,
  captured on serial while adding one layout. Not a panic and not the stack overflow I first
  chased: the compile simply takes longer than the 12 s task watchdog allows while the
  render task waits. The stack work above did not change it. The entry records the measured
  signature, the ruled-out theories, and to measure before assuming which part is slow.

Verified on the P4: layout 256 lights 16x16 (268 B) and effect (988 B), both compiled from
files written over the API. The classic still resets, now with the watchdog signature.

Flash: esp32 1715008, esp32s3-n16r8 1753792, esp32s31 2025600, esp32p4-eth 1603920,
desktop 1138376. Tests: 1328 cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/light/moonlive/MoonLiveScriptFile.h (1)

51-83: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate compiled code on every script-loader failure.

A failed file load returns before MoonLive::compile() calls freeCode(). The old program remains executable while the module reports an error.

  • src/light/moonlive/MoonLiveScriptFile.h#L51-L83: call engine.freeCode() and set *hashOut to zero, when provided, before every pre-compile failure return.
  • src/light/moonlive/MoonLiveEffect.h#L72-L77: ensure a failed script load leaves engine_.ok() false so tick() renders no prior program.
  • src/light/moonlive/MoonLiveLayout.h#L125-L140: ensure a failed script load leaves engine_.ok() false so lightCount() and forEachCoord() do not run prior coordinates.
  • test/unit/light/unit_MoonLiveLayout.cpp#L458-L470: compile a valid script first, then select an invalid name and assert zero lights.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/light/moonlive/MoonLiveScriptFile.h` around lines 51 - 83, Invalidate
compiled state on every script-load failure: in
src/light/moonlive/MoonLiveScriptFile.h:51-83, before each pre-compile failure
return, call engine.freeCode() and zero hashOut when provided. In
src/light/moonlive/MoonLiveEffect.h:72-77 and
src/light/moonlive/MoonLiveLayout.h:125-140, ensure failed loads leave
engine_.ok() false so prior programs and coordinates are not used. In
test/unit/light/unit_MoonLiveLayout.cpp:458-470, first compile a valid script,
then select an invalid name and assert zero lights.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/backlog/backlog-light.md`:
- Around line 293-298: Update the MoonLive watchdog entry’s causal wording to
state only that the compile path did not return before the twelve-second
task-watchdog deadline. Remove or qualify claims that CPU compilation itself
exceeded twelve seconds, while preserving the listed LittleFS and
platform::alloc blocking possibilities and the recommendation to measure
compileScriptFile.

In `@moondeck/run/monitor_esp32.py`:
- Around line 103-113: Update the monitoring setup around the serial handle and
the raised_log_level/open(LOG_FILE, "w") context managers so ser.close() is
performed by an outer finally covering context setup and the monitoring body.
Remove the inner-only cleanup and preserve the existing serial error handling
and monitoring behavior.

In `@src/core/moonlive/MoonLive.cpp`:
- Around line 51-56: Remove the direct platform::alloc and platform::free calls
from the Staging helper in MoonLive. Introduce and inject a core-neutral
memory/code-placement interface into MoonLive for staging allocation and
release, or relocate the runtime placement ownership to the platform layer,
while preserving Staging’s lifetime management and validity check.

In `@test/scenarios/light/scenario_MoonLive_pipeline.json`:
- Line 61: Update the MoonLive pipeline scenario to create isolated
/moonlive/*.mlv file fixtures and set every module’s script control to the
corresponding filename before recording the baseline. Add equivalent
filesystem-fixture support to the in-process runner so the scenario executes
consistently there. Remove any source-based setup or compatibility coverage.

---

Duplicate comments:
In `@src/light/moonlive/MoonLiveScriptFile.h`:
- Around line 51-83: Invalidate compiled state on every script-load failure: in
src/light/moonlive/MoonLiveScriptFile.h:51-83, before each pre-compile failure
return, call engine.freeCode() and zero hashOut when provided. In
src/light/moonlive/MoonLiveEffect.h:72-77 and
src/light/moonlive/MoonLiveLayout.h:125-140, ensure failed loads leave
engine_.ok() false so prior programs and coordinates are not used. In
test/unit/light/unit_MoonLiveLayout.cpp:458-470, first compile a valid script,
then select an invalid name and assert zero lights.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 39efedd1-79fd-4d30-8927-a304870451e6

📥 Commits

Reviewing files that changed from the base of the PR and between 234e01e and 97d004f.

📒 Files selected for processing (21)
  • docs/MIGRATING.md
  • docs/backlog/backlog-light.md
  • docs/metrics/repo-health.json
  • docs/metrics/repo-health.md
  • docs/performance.md
  • moondeck/run/monitor_esp32.py
  • src/core/moonlive/MoonLive.cpp
  • src/light/drivers/ParallelLedDriver.h
  • src/light/moonlive/MoonLiveEffect.h
  • src/light/moonlive/MoonLiveLayout.h
  • src/light/moonlive/MoonLiveScriptFile.h
  • src/platform/desktop/moonlive_asm_host.cpp
  • src/platform/desktop/moonlive_asm_host.h
  • src/platform/esp32/moonlive_asm_riscv.cpp
  • src/platform/esp32/moonlive_asm_riscv.h
  • src/platform/esp32/moonlive_asm_xtensa.cpp
  • src/platform/esp32/moonlive_asm_xtensa.h
  • src/platform/platform.h
  • test/scenarios/light/scenario_MoonLive_pipeline.json
  • test/scenarios/light/scenario_peripheral_grid_sweep.json
  • test/unit/light/unit_MoonLiveLayout.cpp

Comment on lines +293 to +298
- **MoonLive compiling watchdogs a classic ESP32** (2026-08-12). Naming a script on an Olimex Gateway resets the board with `rst:0x8 (TG1WDT_SYS_RESET)` — the TASK watchdog at 12 s, not a panic: there is no `Guru Meditation`, no backtrace, and the last serial lines are ordinary ticks. So the compile is not crashing, it is taking longer than twelve seconds with the render task waiting on it, and the watchdog does its job. Bench-captured on serial while adding one `MoonLiveLayout` with `grid.mlv`; the P4 compiles the same script in well under a second.

**Not a stack overflow** — that was the earlier theory and it was wrong. ~4.1 KB was moved off the compile chain (`MoonLive::compile`'s staging buffer and each assembler's `buf_`, both now heap, RAII-owned) which was worth doing on its own merits (the plan named it) but did not change this: the board still resets, now with the watchdog signature rather than `Double exception`. The earlier `Double exception` runs came from a board carrying persisted WiFi credentials, a separate issue.

**Where to look:** the classic's ticks already read ~9 ms with `renderWait` ~8 ms BEFORE any compile, so the render loop has almost no slack. Either the compile is genuinely that slow on a 240 MHz single-issue Xtensa with no PSRAM, or something in the path blocks (the LittleFS read, `platform::alloc` under a fragmented heap). Measure first — instrument `compileScriptFile` with timings and run it on the classic — before assuming which. Moving the compile off the render task is the likely fix, but it is a scheduling change and wants its own cycle.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Separate the watchdog observation from the unverified cause.

The evidence shows that the compile path did not return before the 12-second task-watchdog deadline. It does not prove that CPU compilation itself exceeded 12 seconds because Line 297 still lists LittleFS and platform::alloc blocking as alternatives. Replace the causal wording with “the compile path did not return before twelve seconds.”

As per coding guidelines, **/*.md: “Documentation must describe the system as it currently exists; specs precede implementation, and breaking changes must be recorded in `docs/MIGRATING.md`.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/backlog/backlog-light.md` around lines 293 - 298, Update the MoonLive
watchdog entry’s causal wording to state only that the compile path did not
return before the twelve-second task-watchdog deadline. Remove or qualify claims
that CPU compilation itself exceeded twelve seconds, while preserving the listed
LittleFS and platform::alloc blocking possibilities and the recommendation to
measure compileScriptFile.

Source: Coding guidelines

Comment on lines +103 to +113
# OPEN THE PORT FIRST. raised_log_level contacts every device in moondeck.json over HTTP at a
# 3 s timeout each — with a dozen registered and most powered off, that is half a minute of
# blocking before a single byte is read, and the boot output you were monitoring FOR is already
# gone. The log level is a nicety; the serial stream is the point.
try:
ser = serial.Serial(args.port, args.baud, timeout=1)
except serial.SerialException as e:
print(f"Cannot open {args.port}: {e}")
sys.exit(1)

with raised_log_level(active_device_ips(), LOG_INFO):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Make serial cleanup cover context setup.

ser opens at Line 108, but ser.close() is only reached from the inner finally at Lines 183-188. If active_device_ips(), raised_log_level.__enter__(), or open(LOG_FILE, "w") raises, the monitoring body is never entered and the serial handle remains open. Move the existing close into an outer finally that covers both context managers.

🧰 Tools
🪛 ast-grep (0.45.1)

[warning] 113-113: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(LOG_FILE, "w")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@moondeck/run/monitor_esp32.py` around lines 103 - 113, Update the monitoring
setup around the serial handle and the raised_log_level/open(LOG_FILE, "w")
context managers so ser.close() is performed by an outer finally covering
context setup and the monitoring body. Remove the inner-only cleanup and
preserve the existing serial error handling and monitoring behavior.

Comment on lines +51 to +56
namespace {
struct Staging {
uint8_t* p = static_cast<uint8_t*>(platform::alloc(kCodeCap));
~Staging() { platform::free(p); }
explicit operator bool() const { return p != nullptr; }
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Move memory ownership behind a core-neutral interface.

Lines 53-54 add direct platform::alloc() and platform::free() calls in src/core. This breaks the required core/platform boundary.

Inject a core-neutral compiler-memory and executable-code-placement interface into MoonLive, or move the runtime placement layer into src/platform.

As per path instructions, src/core/** must be platform-independent. Based on learnings, MoonLive requires a single core/platform-boundary change for executable-memory ownership.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/moonlive/MoonLive.cpp` around lines 51 - 56, Remove the direct
platform::alloc and platform::free calls from the Staging helper in MoonLive.
Introduce and inject a core-neutral memory/code-placement interface into
MoonLive for staging allocation and release, or relocate the runtime placement
ownership to the platform layer, while preserving Staging’s lifetime management
and validity check.

Sources: Path instructions, Learnings

"desktop-macos": {
"tick_us": [
5,
4,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- scenario files ---'
git ls-files 'test/scenarios/light/scenario_MoonLive_pipeline.json' 'test/scenarios/light/*MoonLive*' 'src/core/**' 'src/platform/**' 'docs/**' | sed -n '1,160p'

printf '%s\n' '--- pipeline scenario relevant fields ---'
rg -n -C 4 '"source"|"script|filename|file|baseline|observed|at"|/moonlive|MoonLive' test/scenarios/light/scenario_MoonLive_pipeline.json || true

printf '%s\n' '--- changed-file summary ---'
git diff --stat
git diff -- test/scenarios/light/scenario_MoonLive_pipeline.json | sed -n '1,220p'

printf '%s\n' '--- source and script-name references ---'
rg -n -C 3 'key:\s*["'\'']source|["'\'']source["'\'']|script.?name|filename|/moonlive|MoonLive' src test docs CMakeLists.txt --glob '!**/build/**' | sed -n '1,320p'

Repository: MoonModules/projectMM

Length of output: 42733


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- MoonLive control declarations and file loading ---'
rg -n -C 5 'script|source|moonlive|readFile|writeFile|FileManager|set_control' src/core/moonlive src/light/moonlive src/core test/scenarios test --glob '!test/doctest.h' | sed -n '1,420p'

printf '%s\n' '--- scenario runner operations and filesystem support ---'
fd -i -t f 'run_scenario.py|scenario.*(runner|README)|README.*scenario' . | sed -n '1,120p'
rg -n -C 5 'add_module|set_control|write_file|read_file|filesystem|/moonlive|fixture|operation|op' . --glob '*.py' --glob '*.json' --glob '*.md' --glob '!docs/assets/**' | sed -n '1,420p'

printf '%s\n' '--- scenarios that use script controls or MoonLive files ---'
rg -l '"key"[[:space:]]*:[[:space:]]*"script"|"key"[[:space:]]*:[[:space:]]*"source"|/moonlive/' test/scenarios | sort | sed -n '1,200p'

Repository: MoonModules/projectMM

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- MoonLive-related files ---'
fd -i -t f 'moonlive|MoonLive|scenario' src test moondeck | sed -n '1,240p'

printf '%s\n' '--- script/source references outside compiler comments ---'
rg -n 'source|script|setScript|scriptName|filename|moonlive' src/light src/core/moonlive src/core/Control* src/core/*Module* test/scenario_runner.cpp moondeck/scenario/run_scenario.py --glob '*.{h,hpp,cpp,py}' | sed -n '1,300p'

printf '%s\n' '--- scenario runner implementation ---'
wc -l moondeck/scenario/run_scenario.py test/scenario_runner.cpp
sed -n '1,260p' moondeck/scenario/run_scenario.py
sed -n '1,300p' test/scenario_runner.cpp

printf '%s\n' '--- migration contract ---'
sed -n '20,40p' docs/MIGRATING.md

Repository: MoonModules/projectMM

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- file-backed script implementation ---'
wc -l src/light/moonlive/MoonLiveScriptFile.h src/light/moonlive/MoonLiveEffect.h src/light/moonlive/MoonLiveLayout.h src/light/moonlive/MoonLiveModifier.h
cat -n src/light/moonlive/MoonLiveScriptFile.h
rg -n -C 8 'script|source|MoonLiveScriptFile|compile|read|open|file' src/light/moonlive/MoonLiveEffect.h src/light/moonlive/MoonLiveLayout.h src/light/moonlive/MoonLiveModifier.h

printf '%s\n' '--- scenario operations ---'
rg -n -C 10 'set_control|add_module|remove_module|fixture|op ==|strcmp\(.*op|write|file|filesystem' test/scenario_runner.cpp | sed -n '1,360p'

printf '%s\n' '--- exact MoonLive scenario controls ---'
for f in test/scenarios/light/scenario_MoonLiveEffect_controls.json \
         test/scenarios/light/scenario_MoonLiveEffect_livescript.json \
         test/scenarios/light/scenario_MoonLive_pipeline.json; do
  echo "--- $f ---"
  rg -n -C 3 '"key"|"value"|/moonlive|script|source|fixture' "$f" | sed -n '1,260p'
done

Repository: MoonModules/projectMM

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- repository script files ---'
git ls-files | awk 'tolower($0) ~ /(^|\/)moonlive(\/|$)|\.mlv$/' | sed -n '1,200p'

printf '%s\n' '--- scenario runner dispatch and fixture setup ---'
rg -n 'set_control|add_module|remove_module|fixture|steps|op\b|applyControl|setControl|fs' test/scenario_runner.cpp | sed -n '1,240p'
sed -n '500,760p' test/scenario_runner.cpp
sed -n '760,940p' test/scenario_runner.cpp

printf '%s\n' '--- script-file test setup and platform filesystem API ---'
rg -n -C 6 'compileScriptFile|fsMkdir|fsRead|fsWrite|/moonlive|setScript|script_' test/unit test/scenario_runner.cpp src/platform/platform.h src/platform/desktop/platform_desktop.cpp | sed -n '1,360p'

Repository: MoonModules/projectMM

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- scenario runner filesystem initialization ---'
rg -n -C 8 'fsSetRoot|fsMount|fsMkdir|fsWrite|fsRead|working|temp|filesystem' test/scenario_runner.cpp test/unit/light/MoonLiveScriptFixture.h CMakeLists.txt test/CMakeLists.txt 2>/dev/null || true
sed -n '1,220p' test/unit/light/MoonLiveScriptFixture.h

printf '%s\n' '--- desktop filesystem path mapping ---'
rg -n -C 8 'toFsPath|fsSetRoot|fsMount|fsMkdir|fsRead\(' src/platform/desktop/platform_desktop.cpp src/platform/platform.h | sed -n '1,280p'

printf '%s\n' '--- build/package handling for moonlive scripts ---'
rg -n -C 5 'moonlive/|\.mlv|random-pixel|grid\.mlv|copy.*moon|file.*moonlive' CMakeLists.txt cmake test moondeck moonlive docs --glob '!**/*.png' --glob '!**/*.gif' | sed -n '1,300p'

printf '%s\n' '--- current pipeline scenario complete control steps ---'
sed -n '80,270p' test/scenarios/light/scenario_MoonLive_pipeline.json

Repository: MoonModules/projectMM

Length of output: 50379


Make the pipeline scenario use file-backed scripts.

source is ignored. Add isolated /moonlive/*.mlv fixture setup, then set each module’s script control to its filename before recording the baseline. The in-process runner currently has no filesystem fixture operation, so add equivalent runner support. Do not test source compatibility; it is intentionally removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/scenarios/light/scenario_MoonLive_pipeline.json` at line 61, Update the
MoonLive pipeline scenario to create isolated /moonlive/*.mlv file fixtures and
set every module’s script control to the corresponding filename before recording
the baseline. Add equivalent filesystem-fixture support to the in-process runner
so the scenario executes consistently there. Remove any source-based setup or
compatibility coverage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant