MoonLive: scripted effects, modifiers and layouts, compiled on the device - #63
Conversation
The three top-level light containers are now Layouts, Effects and Drivers — L.E.D. The old name sat one character from its own child (Layers holding Layers) and read as a near-twin of Layouts, which is the pair a newcomer actually has to tell apart. The tree keeps its shape: Effects -> Layers -> effects and modifiers. desktop 140us/7,142fps | esp32 4,164us/240fps | flash unchanged Light domain - Layers -> Effects: the class, its header, the registered type name, the card image, and Drivers::setLayers -> setEffects. - Layer, the child, is untouched. The two names differ by one character, so every replacement is word-boundary anchored rather than a token sweep — the distinction this rename draws is exactly what a blind replace would collapse. - src/light/layers/ keeps its name: it also holds Layer.h, Buffer.h, BlendMap.h and MappingLUT.h, which are all still layer things. Core - ControlModule's capture-role index is kEffectsRole, and its static_assert re-pinned to spell "Effects". It checked the old name character by character ([0]=='L', [1]=='a', [5]=='s'), so it would have failed the build. Tests - unit_Layers_container -> unit_Effects_container, scenario_Layers_composition -> scenario_Effects_composition, and the scenario key that binds Drivers to the container is "effects" on both sides. - No golden moved, which is the evidence this is a rename and nothing else. Docs/CI - MIGRATING.md carries the break. The type name IS the persisted filename and the preset capture key, so a device boots with an empty light tree and presets stop applying their effects. Per ADR-0013 that is documented, not migrated. Both halves were verified on a running desktop: the tree does come up empty, and renaming /.config/Layers.json to Effects.json restores it intact — so the shortcut in that entry is tested rather than assumed. - docs/history/ is left alone: it records what was true when written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe pull request renames the light-pipeline container from ChangesEffects rename and runtime integration
MoonLive compiler and execution
MoonLive modules and validation
Light safety and diagnostics
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 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/architecture.md`:
- Line 33: Update both table-of-contents links for the “Effects and Layer”
section to use the correct Markdown anchor `#effects-and-layer` instead of
`#layers-and-layer`, including the reference near line 475.
In `@docs/backlog/system-modules.md`:
- Line 65: Rewrite the sentence around the “Services is to System” comparison so
it describes Services as the existing top-level container for user-added Audio
and IR modules, analogous to Effects containing user-added Layer children.
Remove the implication that Effects contains effect children directly or that
MQTT is a user-added Service; preserve MQTT as code-wired under Network.
In `@docs/gettingstarted.md`:
- Around line 272-279: Propagate the Layers-to-Effects terminology across all
specified documentation: in docs/gettingstarted.md lines 257-261 and 290-299 and
README.md line 23, use the pipeline name “Layouts → Effects → Drivers”; in
docs/moonmodules/core/control.md lines 51 and 53, change preset-role references
from “layer” to “Effects” or “effects”; in docs/moonmodules/light/supporting.md
lines 21-27, add and use the effects anchor, update the test link accordingly,
and retain layers only as an intentional compatibility alias.
In `@docs/moonmodules/core/services.md`:
- Line 7: Update the Services heading in the core services documentation from a
third-level heading to ## Services so it is correctly nested beneath # Core
services and provides the proper parent level for Audio and IR.
In `@docs/usecases/build-your-own-moonmodules.md`:
- Line 322: Update docs/usecases/build-your-own-moonmodules.md at line 322 to
describe the hierarchy as Layouts → Effects → Layer → Effect/Modifier → Drivers,
with Layouts, Effects, and Drivers as top-level sibling containers and Layer
nested under Effects. Update docs/usecases/home-automation.md at line 111 to
direct users to add the Hue driver in the top-level Drivers container, not under
a Layer.
- Line 322: Update the architecture document link description in the “The
architecture doc” bullet to reflect the hierarchy `Layouts → Effects → Layer →
Effect/Modifier → Drivers`, distinguishing the top-level Effects container from
its child Effect/Modifier stage. Keep the existing hot-path rules reference
unchanged.
In `@src/core/Scheduler.cpp`:
- Line 157: Update the nearby explanatory text to say “two Layer instances”
rather than “two Effects,” matching ModuleFactory::create("Layer") and the
duplicate Layer children under the Effects container; leave the uniqueness logic
unchanged.
In `@src/light/effects/EffectBase.h`:
- Line 69: Restore the MoonLight prior-art URL path in the comments by replacing
Effects/VirtualLayer.h with Layers/VirtualLayer.h in
src/light/effects/EffectBase.h lines 69-69, src/light/layers/Buffer.h lines
28-28, and src/light/layers/Layer.h lines 33-33; no other changes are needed.
In `@src/light/layers/Effects.h`:
- Around line 45-48: Update the comments around the role-filtered loop
propagation and the corresponding comment near line 103 to describe child
modules as Layer instances or having ModuleRole::Layer, not Effects. Keep
Effects reserved for the container terminology and align both comments with the
implementation.
- Line 16: Replace the broken MoonLight prior-art URL in the comment near the
Effects container in src/light/layers/Effects.h:16-16 with one verified
canonical reference, or remove the link. Apply the same consistent update to the
Drivers prior-art comment in src/light/drivers/Drivers.h:71-71.
In `@src/main.cpp`:
- Line 158: Add a one-time migration alongside
mm::ModuleFactory::registerType<mm::Effects> that rewrites persisted “Layers.*”
keys to “Effects.*” and “captures”:“Layers” values to “Effects” across settings
and preset payloads before loading them. Ensure existing data remains
applicable, and add coverage or documentation for the complete migration.
- Line 158: Update the documentation link in
mm::ModuleFactory::registerType<mm::Effects> from the stale `#layers` anchor to
light/supporting.md#effects, unless an explicit compatibility anchor is
intentionally retained at the Effects heading.
In `@test/scenario_runner.cpp`:
- Around line 316-323: Keep Effects/Layer terminology consistent across the
affected tests: in test/scenario_runner.cpp lines 316-323, describe child Layer
modules; update the specified descriptions in
test/unit/light/unit_Effects_container.cpp lines 29-32, 90-91, 117-119, and
141-145 to refer to child Layers, enabled Layer buffers, and Drivers compositing
Layers. In test/unit/light/unit_Effects_container.cpp lines 258-265, 280-287,
302-308, 322-329, 349-358, 404-408, 424-433, and 451, rename the local
mm::Effects variable from layers to effects without changing behavior.
In `@test/unit/light/unit_Drivers_container.cpp`:
- Line 11: Update the comment describing Layer::tick() and Effects::tick() to
replace “child Effects” with “child Layer modules,” accurately identifying
Effects as the top-level container and Layer as its child type.
🪄 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: ba437a56-c961-4f03-931a-3d9a52d331d3
⛔ Files ignored due to path filters (1)
docs/assets/light/Effects.pngis excluded by!**/*.png
📒 Files selected for processing (63)
docs/MIGRATING.mddocs/architecture.mddocs/backlog/power-functions-analysis-top-down.mddocs/backlog/system-modules.mddocs/coding-standards.mddocs/gettingstarted.mddocs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/core/control.mddocs/moonmodules/core/services.mddocs/moonmodules/light/supporting.mddocs/usecases/build-your-own-moonmodules.mddocs/usecases/home-automation.mdsrc/core/ControlModule.hsrc/core/FilesystemModule.hsrc/core/HttpServerModule.cppsrc/core/ModuleFactory.hsrc/core/MoonModule.hsrc/core/Scheduler.cppsrc/core/Services.hsrc/light/drivers/Drivers.hsrc/light/effects/EffectBase.hsrc/light/layers/Buffer.hsrc/light/layers/Effects.hsrc/light/layers/Layer.hsrc/light/layers/MappingLUT.hsrc/light/layouts/Layouts.hsrc/main.cppsrc/ui/app.jssrc/ui/style.csstest/CMakeLists.txttest/scenario_runner.cpptest/scenarios/core/scenario_MoonModule_control_change.jsontest/scenarios/light/scenario_Audio_mutation.jsontest/scenarios/light/scenario_Effects_composition.jsontest/scenarios/light/scenario_GridBlacks_blackpixel.jsontest/scenarios/light/scenario_GridLayout_resize.jsontest/scenarios/light/scenario_MoonLiveEffect_controls.jsontest/scenarios/light/scenario_MoonLiveEffect_livescript.jsontest/scenarios/light/scenario_modifier_chain.jsontest/scenarios/light/scenario_modifier_swap.jsontest/scenarios/light/scenario_perf_full.jsontest/scenarios/light/scenario_perf_light.jsontest/scenarios/light/scenario_peripheral_grid_sweep.jsontest/scenarios/light/scenario_peripheral_switch.jsontest/unit/core/unit_ControlModule.cpptest/unit/core/unit_FilesystemModule_subtree.cpptest/unit/core/unit_MoonModule_lifecycle.cpptest/unit/core/unit_MqttModule.cpptest/unit/core/unit_Scheduler_unique_names.cpptest/unit/core/unit_Services.cpptest/unit/light/golden_frame.htest/unit/light/unit_Canvas.cpptest/unit/light/unit_Drivers_container.cpptest/unit/light/unit_Drivers_rendersplit.cpptest/unit/light/unit_Effects_container.cpptest/unit/light/unit_Layer_live_modifier.cpptest/unit/light/unit_Layer_persistence.cpptest/unit/light/unit_Layer_phase_animation.cpptest/unit/light/unit_Layouts_container.cpptest/unit/light/unit_Layouts_toggle_cycle.cpptest/unit/light/unit_PreviewDriver.cpptest/unit/light/unit_RandomMapModifier.cpp
| **Effects** — what plays on the lights. Add an **effect** (a moving pattern), stack | ||
| several to blend them, and reshape them with **modifiers** (mirror, rotate, and | ||
| more). Each effect has its own controls — speed, color mode, and so on — that you | ||
| tweak live. | ||
|
|
||
|  | ||
|  | ||
|
|
||
| > [Layers](moonmodules/light/supporting.md) · [Layer](moonmodules/light/supporting.md) | ||
| > [Effects](moonmodules/light/supporting.md) · [Layer](moonmodules/light/supporting.md) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Propagate the Layers → Effects rename through public documentation.
The visible rename is applied, but old container names remain in pipeline prose, preset-role prose, and anchors. Use Effects for the top-level container and Layer only for child modules.
docs/gettingstarted.md#L272-L279: update the pipeline references at Lines 257-261 and 290-299, andREADME.mdLine 23, to useLayouts → Effects → Drivers.docs/moonmodules/core/control.md#L38-L45: change the preset-role prose at Lines 51 and 53 fromlayertoEffectsoreffects.docs/moonmodules/light/supporting.md#L21-L27: add and use theeffectsanchor, update the test link, and retainlayersonly as an intentional compatibility alias.
📍 Affects 3 files
docs/gettingstarted.md#L272-L279(this comment)docs/moonmodules/core/control.md#L38-L45docs/moonmodules/light/supporting.md#L21-L27
🤖 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/gettingstarted.md` around lines 272 - 279, Propagate the
Layers-to-Effects terminology across all specified documentation: in
docs/gettingstarted.md lines 257-261 and 290-299 and README.md line 23, use the
pipeline name “Layouts → Effects → Drivers”; in docs/moonmodules/core/control.md
lines 51 and 53, change preset-role references from “layer” to “Effects” or
“effects”; in docs/moonmodules/light/supporting.md lines 21-27, add and use the
effects anchor, update the test link accordingly, and retain layers only as an
intentional compatibility alias.
| /// **Prior art:** MoonLight's `PhysicalLayer` runs N `VirtualLayer`s and composites their buffers into the display channel — same idea, different shape: Drivers (not Layers) does the compositing here (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight). | ||
| /// @card Layers.png | ||
| class Layers : public MoonModule { | ||
| /// **Prior art:** MoonLight's `PhysicalLayer` runs N `VirtualLayer`s and composites their buffers into the display channel — same idea, different shape: Drivers (not Effects) does the compositing here (https://github.com/ewowi/MoonLight/blob/main/src/MoonLight). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace the duplicated broken prior-art links.
The same prior-art URL appears in both changed comments and returns 404. Replace both links with one verified canonical MoonLight reference, or remove both links. ()
- src/light/layers/Effects.h#L16-L16: update the
Effectscontainer prior-art link. - src/light/drivers/Drivers.h#L71-L71: update the
Driversprior-art link.
📍 Affects 2 files
src/light/layers/Effects.h#L16-L16(this comment)src/light/drivers/Drivers.h#L71-L71
🤖 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/layers/Effects.h` at line 16, Replace the broken MoonLight
prior-art URL in the comment near the Effects container in
src/light/layers/Effects.h:16-16 with one verified canonical reference, or
remove the link. Apply the same consistent update to the Drivers prior-art
comment in src/light/drivers/Drivers.h:71-71.
Source: MCP tools
| // Containers | ||
| mm::ModuleFactory::registerType<mm::Layouts>("Layouts", "light/supporting.md#layouts"); | ||
| mm::ModuleFactory::registerType<mm::Layers>("Layers", "light/supporting.md#layers"); | ||
| mm::ModuleFactory::registerType<mm::Effects>("Effects", "light/supporting.md#layers"); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Migrate persisted keys as well as the filename.
Changing the registered type to "Effects" changes more than /.config/Layers.json to Effects.json. Existing persistence and preset payloads also contain old "Layers.*" keys and "captures":"Layers" values, while the updated fixtures require "Effects.*" and "captures":"Effects". Renaming only the file leaves settings unapplied and makes old presets non-applicable.
Add a one-time migration, or document and test a complete rewrite of the old keys and capture values before shipping.
🤖 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/main.cpp` at line 158, Add a one-time migration alongside
mm::ModuleFactory::registerType<mm::Effects> that rewrites persisted “Layers.*”
keys to “Effects.*” and “captures”:“Layers” values to “Effects” across settings
and preset payloads before loading them. Ensure existing data remains
applicable, and add coverage or documentation for the complete migration.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n '^#+ .*(Effects|Layers)' docs/moonmodules/light/supporting.mdRepository: MoonModules/projectMM
Length of output: 174
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "### File context"
sed -n '1,50p' docs/moonmodules/light/supporting.md
echo
echo "### Anchor usage"
rg -n 'supporting\.md#[A-Za-z0-9-]+' .Repository: MoonModules/projectMM
Length of output: 3263
Fix the Effects documentation anchor.
light/supporting.md has an old #layers anchor before the renamed Effects section and no matching #layers heading. registerType<mm::Effects>("Effects", "light/supporting.md#layers") points to the wrong section. Update it to light/supporting.md#effects, or keep an explicit compatibility anchor at the Effects heading.
🤖 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/main.cpp` at line 158, Update the documentation link in
mm::ModuleFactory::registerType<mm::Effects> from the stale `#layers` anchor to
light/supporting.md#effects, unless an explicit compatibility anchor is
intentionally retained at the Effects heading.
| if (std::strcmp(type, "Effects") == 0) { | ||
| // Wire the container's Layouts (mirrors main.cpp's | ||
| // layersContainer->setLayouts). Layers re-propagates this to its | ||
| // child Layers at every prepareTree, so a Layer added later picks | ||
| // effectsContainer->setLayouts). Effects re-propagates this to its | ||
| // child Effects at every prepareTree, so a Layer added later picks | ||
| // it up — the self-healing path the device relies on. | ||
| if (props.has("layouts")) { | ||
| auto* layoutsModule = static_cast<mm::Layouts*>(modules[props["layouts"].str]); | ||
| if (layoutsModule) static_cast<mm::Layers*>(mod)->setLayouts(layoutsModule); | ||
| if (layoutsModule) static_cast<mm::Effects*>(mod)->setLayouts(layoutsModule); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Keep the Effects/Layer boundary consistent in test comments and names.
The runtime contract uses Effects as the grouping container and Layer as its child role. The changed test text and local names use Effects for child Layers or layers for an mm::Effects object.
test/scenario_runner.cpp#L316-L323: change “child Effects” to “child Layer modules”.test/unit/light/unit_Effects_container.cpp#L29-L32: describe composition across child Layers.test/unit/light/unit_Effects_container.cpp#L90-L91: rename “two child Effects” to “two child Layers”.test/unit/light/unit_Effects_container.cpp#L117-L119: refer to enabled Layer buffers.test/unit/light/unit_Effects_container.cpp#L141-L145: describe Drivers compositing Layers from the Effects container.test/unit/light/unit_Effects_container.cpp#L258-L265: rename the locallayersvariable toeffects.test/unit/light/unit_Effects_container.cpp#L280-L287: rename the locallayersvariable toeffects.test/unit/light/unit_Effects_container.cpp#L302-L308: rename the locallayersvariable toeffects.test/unit/light/unit_Effects_container.cpp#L322-L329: rename the locallayersvariable toeffects.test/unit/light/unit_Effects_container.cpp#L349-L358: rename the locallayersvariable toeffects.test/unit/light/unit_Effects_container.cpp#L404-L408: rename the locallayersvariable toeffects.test/unit/light/unit_Effects_container.cpp#L424-L433: rename the locallayersvariable toeffects.test/unit/light/unit_Effects_container.cpp#L451-L451: rename the locallayersvariable toeffects.
As per coding guidelines, use standard, recognizable names and make test descriptions state user-understandable behavior.
📍 Affects 2 files
test/scenario_runner.cpp#L316-L323(this comment)test/unit/light/unit_Effects_container.cpp#L29-L32test/unit/light/unit_Effects_container.cpp#L90-L91test/unit/light/unit_Effects_container.cpp#L117-L119test/unit/light/unit_Effects_container.cpp#L141-L145test/unit/light/unit_Effects_container.cpp#L258-L265test/unit/light/unit_Effects_container.cpp#L280-L287test/unit/light/unit_Effects_container.cpp#L302-L308test/unit/light/unit_Effects_container.cpp#L322-L329test/unit/light/unit_Effects_container.cpp#L349-L358test/unit/light/unit_Effects_container.cpp#L404-L408test/unit/light/unit_Effects_container.cpp#L424-L433test/unit/light/unit_Effects_container.cpp#L451-L451
🤖 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/scenario_runner.cpp` around lines 316 - 323, Keep Effects/Layer
terminology consistent across the affected tests: in test/scenario_runner.cpp
lines 316-323, describe child Layer modules; update the specified descriptions
in test/unit/light/unit_Effects_container.cpp lines 29-32, 90-91, 117-119, and
141-145 to refer to child Layers, enabled Layer buffers, and Drivers compositing
Layers. In test/unit/light/unit_Effects_container.cpp lines 258-265, 280-287,
302-308, 322-329, 349-358, 404-408, 424-433, and 451, rename the local
mm::Effects variable from layers to effects without changing behavior.
Source: Coding guidelines
Scripts can now compute — `+`, `-`, `*` with parentheses and the usual precedence — and print a value to the serial log. On top of that, a modifier can be written as a script: the coordinate transform that decides where each light sits in the pattern, edited on a running device instead of compiled in and reflashed. desktop 133us/7,518fps | esp32 4,164us/240fps | flash: desktop 1,076KB (+2KB), esp32s3-n16r8 1,704KB (+4KB) Core - MoonLive gains arithmetic: precedence climbing over the existing Const/Add/Mul IR, so all three backends lower it unchanged. Subtraction is emitted as a + (b * -1): no supported ISA has a subtract, and Xtensa's add-immediate masks its operand to four bits, so negating the immediate there would silently produce a different constant. - Division and modulo are deliberately absent: both need a two-argument host call and Call is unary today. A script using them gets "unexpected character", which is the honest answer rather than a misleading parse error. Light domain - print(v) logs a value and returns it, so it wraps any sub-expression without changing the result. It is the only view inside a running script — one that compiles cleanly and renders wrong gives no other clue, which is exactly the case that cost this session its longest debugging detour. Capped at a short burst: the script runs once per light, and an uncapped print on a 16k wall would be 16k blocking serial writes per rebuild. - setXYZ(index, x, y, z) writes a POSITION through the same element store setRGB writes a colour through — three values at index * stride. The engine needed no change to host a second binding, which is the evidence its domain-neutrality is real rather than asserted. - MoonLiveModifier: scripts read x/y/z and width/height/depth as injected controls. A script needs the EXTENT to write a mirror at all; against a fixed 255 every light of a 16-wide grid lands out of bounds, the Layer drops it, and the fixture goes black with no error anywhere. The bug that made it render nothing, since it is the interesting one: prepare() asked the Layer for a mapping rebuild, the Layer's rebuild IS applyState(), and applyState() calls prepare(). The two called each other forever, the mapping was permanently mid-rebuild, and nothing ever drew. Every part was correct in isolation — the script compiled, the fold returned the right coordinates, every light was accepted — so only a probe inside the fold showed it: 782,296 rebuilds in six seconds where there should have been one. A rebuild is now requested only when the compiled source actually changed. Tests - The rebuild cycle: re-preparing an unchanged script must not ask again. - The black-screen case: the default script stays inside the grid it is given, at every coordinate. - print() transparency, arithmetic precedence, and the per-light cost (16,384 transforms in 31us, so the native call per light is free). Docs/CI - MoonLiveModifier.md, including the two limits worth knowing: a coordinate is a byte, and a script cannot resize the logical box. - Rename fallout from the external review: four upstream MoonLight URLs my sweep had rewritten to a path that does not exist, two broken doc anchors, the registerType help link, the README pipeline name, a heading level, and four comments saying Effects where they meant Layer. - The preset capture role is "effects" rather than "layer": it names the container a preset captures, not a module inside it. A breaking change, documented in MIGRATING.md. - system-modules.md no longer justifies a core-domain split by naming light-domain containers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 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 33-38: Update the migration documentation in docs/MIGRATING.md
(lines 33-38) to represent preset captures as the single string value the loader
reads, using "Layers" only for the file-edit migration path; update the
corresponding guidance in docs/moonmodules/core/control.md (lines 35-43)
consistently. Do not document captures as an array.
In `@docs/moonmodules/light/MoonLiveModifier.md`:
- Around line 41-47: Update the Limits section of MoonLiveModifier documentation
to state that values written by setXYZ are stored as bytes and therefore
truncate or wrap to the 0–255 range, including transformed outputs beyond that
range. Place this behavior alongside the existing input-coordinate limit
description and keep the documented input behavior unchanged.
In `@README.md`:
- Line 23: Update the pipeline description near the `layouts → layers with
effects + modifiers → drivers` text to name the top-level stage `Effects` and
describe its child layers separately, matching the terminology used in the
pluggable pipeline description while preserving the existing pipeline order.
In `@src/light/moonlive/MoonLiveBuiltins_light.h`:
- Around line 34-47: The print burst cap is process-lifetime rather than per-run
because mm_light_print uses a never-reset function-local static counter. In
src/light/moonlive/MoonLiveBuiltins_light.h:34-47, either reset the counter at
the start of each script run sequence or revise the surrounding comment to
accurately describe per-boot behavior; in
docs/moonmodules/light/MoonLiveModifier.md:39, update the documentation to match
the shipped behavior, explicitly replacing the per-run burst claim.
In `@src/light/moonlive/MoonLiveModifier.h`:
- Around line 138-151: Remove the unused public test accessors sourceForTest()
and boxForTest() from MoonLiveModifier; do not alter the release() or
setSource() behavior.
- Around line 165-175: Replace the redundant compiled_ buffer in the MoonLive
modifier state with a compact checksum of full_. Update the rebuild-detection
logic to compute and compare the checksum, refreshing it whenever the mapping is
rebuilt, while preserving the existing source-change behavior and using the
repository’s existing core/crc helper.
In `@test/unit/light/unit_MoonLiveModifier.cpp`:
- Around line 167-209: Remove the first duplicate rebuild-signal test case and
its preceding explanatory comment. Keep the second test case, “editing a script
asks the layer to rebuild its mapping,” including its rebuild-consumption checks
and final modifyLogical assertion.
- Around line 240-265: Update the test case “a script that computes a position
outside the grid does not black out the fixture” so it observes the modifier’s
mapped output rather than the prefilled layer buffer. Remove the direct buffer
fill/count assertion and inspect the mapping fold’s counts or destinations after
applying the layer, or render through a driver and count emitted lights; ensure
the assertion fails when no light is mapped.
- Around line 147-165: Update the test case “transforming a wall's worth of
lights stays within a rebuild's budget” to remove the timing-dependent CHECK on
elapsed microseconds and the associated wall-clock assertion. Preserve the loop
and MESSAGE measurement for reporting, and replace the timing-based validation
with a deterministic assertion that directly verifies the script is not compiled
once per modifyLogical call, using the existing observable API or state.
🪄 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: 6f4521ac-651a-4bef-9cee-475ff9d31d5e
📒 Files selected for processing (25)
README.mddocs/MIGRATING.mddocs/architecture.mddocs/backlog/system-modules.mddocs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/core/control.mddocs/moonmodules/core/services.mddocs/moonmodules/light/MoonLiveModifier.mddocs/usecases/build-your-own-moonmodules.mddocs/usecases/home-automation.mdsrc/core/ControlModule.hsrc/core/moonlive/MoonLiveCompiler.cppsrc/light/drivers/Drivers.hsrc/light/layers/Effects.hsrc/light/layers/Layer.hsrc/light/moonlive/MoonLiveBuiltins_light.hsrc/light/moonlive/MoonLiveModifier.hsrc/main.cppsrc/ui/app.jstest/CMakeLists.txttest/unit/core/unit_ControlModule.cpptest/unit/light/unit_Drivers_container.cpptest/unit/light/unit_Effects_container.cpptest/unit/light/unit_MoonLiveModifier.cpp
| | The saved light tree | The device looks for `/.config/Effects.json` and the old file is `Layers.json`, so the light tree boots empty | Re-add your Layer, effect and modifiers, then let it save | | ||
| | Presets that capture the look | A preset file records `"captures": ["Layers"]`, a key no module now answers to | Re-save each preset once the tree is rebuilt | | ||
|
|
||
| A preset also records the ROLE it covers, and that role is now named after the container rather than after a module inside it: `"layer"` becomes `"effects"`. A preset carrying the old role still loads, but shows no tint or emoji on its pad until it is re-saved — the UI has no `layer` role to colour it by. | ||
|
|
||
| Renaming the file on the device works if you would rather not rebuild by hand: `Layers.json` → `Effects.json`, and `"Layers"` → `"Effects"` inside each `/.config/presets/*.json`. Nothing else in either file changes. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Expect one persisted captures shape across implementation and documentation.
rg -n -C 8 \
'saveSubtreeTo|applySubtree|captures' \
--glob '*.{cpp,h,hpp,md,json}'Repository: MoonModules/projectMM
Length of output: 159
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Files matching target names:"
git ls-files | rg '(^docs/MIGRATING\.md$|^docs/moonmodules/core/control\.md$|FilesystemModule\.md$|\.cpp$|\.h$|\.hpp$|\.json$)' | sed -n '1,200p'
echo
echo "Search presets/captures/persistence names without filtering by extension first:"
rg -n -C 5 'captures|saveSubtreeTo|applySubtree|Layers|Effects' . --glob '!**/.git/**' | sed -n '1,300p'Repository: MoonModules/projectMM
Length of output: 36383
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Target docs lines:"
sed -n '1,80p' docs/MIGRATING.md
echo
sed -n '1,70p' docs/moonmodules/core/control.md
echo
echo "Source files containing LightPresetsModule/preset/captures/saveSubtreeTo/applySubtree:"
rg -n -C 6 'LightPresetsModule|LightPresets|preset|presets|captures|saveSubtreeTo|applySubtree|Layers|Effects' \
src moondeck library.json espidf component.mk CMakeLists.txt -g '!**/build/**' -g '!**/.git/**' | sed -n '1,400p'
echo
echo "List LightPresetsModule candidate files:"
git ls-files | rg '(^src/light/drivers/LightPresetsModule\.(h|cpp)$|.*ControlModule.*|.*FilesystemModule.*)'Repository: MoonModules/projectMM
Length of output: 49643
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 12 'class ControlModule|struct P|presets|captures|preset|apply\(|save\(' \
src/core/Control*.h src/core/Control*.cpp src/core/ControlModule.cpp || true
python3 - <<'PY'
import subprocess, re
files = subprocess.check_output(['git','ls-files'], text=True).splitlines()
for f in files:
if f.endswith(('.h','.hpp','.cpp','.md')):
text = open(f, encoding='utf-8', errors='surrogateescape').read()
for name in ['ControlModule','presetsRevision','presetCount','presetName','activePreset','applyPreset','capture','captures']:
if name in text:
lines = text.splitlines()
for i,l in enumerate(lines,1):
if name in l:
print(f"{f}:{i}: {l.strip()}")
PYRepository: MoonModules/projectMM
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "ControlModule implementation excerpt:"
sed -n '1,240p' src/core/ControlModule.cpp
echo
sed -n '240,420p' src/core/ControlModule.cpp
echo
echo "Read-only semantic probe: parseString ignores array value."
python3 - <<'PY'
# Mirror the minimal parseString path from JsonUtil/ControlModule behavior without running repo code:
# parseString(head, "captures", p.captures) expects "captures":"Value"; when captures is ["Layers"]
# the value token is [, so the string parse does not produce "Layers".
import json
for value in ["Effects", '["Layers"]']:
data = {"captures": value}
print(value, "== parseString target ==")
try:
print(json.loads(data))
except Exception as e:
print("exception", *type(e).__name__, str(e))
PYRepository: MoonModules/projectMM
Length of output: 267
Keep the migration capture key as a string.
ControlModule persists preset captures as a single subtree string (for example "Effects"); "captures": ["Layers"] does not match the single-value capture the loader reads. The migration instruction should use "Layers" in the migrate-to example only if a file edit is the chosen path.
📍 Affects 2 files
docs/MIGRATING.md#L33-L38(this comment)docs/moonmodules/core/control.md#L35-L43
🤖 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/MIGRATING.md` around lines 33 - 38, Update the migration documentation
in docs/MIGRATING.md (lines 33-38) to represent preset captures as the single
string value the loader reads, using "Layers" only for the file-edit migration
path; update the corresponding guidance in docs/moonmodules/core/control.md
(lines 35-43) consistently. Do not document captures as an array.
Source: Coding guidelines
| 🧊 **Native 3D from the ground up**: 2D and 1D are just the cases where a dimension is size 1. Effects never pick a mode. | ||
|
|
||
| 🎛️ **Pluggable pipeline**: Layouts → Layers (effects + modifiers) → Drivers. Build it visually in the browser, and every change applies live (settings also persist to flash across power cycles). | ||
| 🎛️ **Pluggable pipeline**: Layouts → Effects (layers of effects + modifiers) → Drivers. Build it visually in the browser, and every change applies live (settings also persist to flash across power cycles). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Keep the README pipeline terminology consistent.
Line 23 now uses Effects, but Line 106 still describes layouts → layers with effects + modifiers → drivers. The README presents two names for the same top-level stage. Update Line 106 to use Effects and distinguish its Layer children.
🤖 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 `@README.md` at line 23, Update the pipeline description near the `layouts →
layers with effects + modifiers → drivers` text to name the top-level stage
`Effects` and describe its child layers separately, matching the terminology
used in the pluggable pipeline description while preserving the existing
pipeline order.
Source: Coding guidelines
A script's subtraction was correct only modulo 256 on two of the three targets. `a - b` compiles to `a + (b * -1)`, and both assemblers built that -1 through a 16-bit immediate, so the register held 65535. A stored colour byte came out right either way, which is why every test passed; anything reading the full value did not. An index computed by subtraction became ~65k, the element store's bounds guard rejected it, and the light silently never lit. desktop 133us/7,518fps | esp32 4,164us/240fps | flash unchanged Platform - arm64 emits `movn wD, #~imm16` for a negative constant; movz can only build a zero-extended one. - Xtensa uses the wide `movi` 12-bit SIGNED field (-2048..2047), the only encoding here that holds a negative. Verified byte for byte: -1 emits 0x32 0xaf 0xff, which the CPU reads back as -1. - RISC-V already sign-extended and is unchanged. Confirmed on the bench: the scripted mirror renders on the S3, which it could not have done with a wrapped -1 — every light would have folded outside the grid and the panel would be black. Light domain - A disabled-then-re-enabled scripted modifier was permanently inert: release() frees the compiled program but kept the source it was compiled from, so the next prepare saw "unchanged" and never asked for the rebuild that would apply it. release() now forgets it, which is the state a first compile starts from. - print()'s burst resets on every compile. It was a process-lifetime counter, so the tool that exists to explain a misbehaving script went silent after one burst — including in the middle of diagnosing this commit's own bug. Tests - Subtraction is pinned THROUGH print(), which returns the full 32-bit value. A byte comparison cannot see this class of bug at all: mod-256 congruence makes the stored byte correct whether or not the arithmetic is. - The wall-budget test asserts the compiled program is unchanged across 16,384 folds rather than a wall-clock bound, so it states the property (no per-call compile) instead of how busy the machine is. - The blackout test observes the folded COORDINATES. It filled the buffer through draw::fill and counted lit bytes, which writes every byte itself and so could not fail whatever the mapping did. Docs - MIGRATING: a preset records `"captures": "Layers"`, a single string; it was written as an array, which would send a file edit hunting for brackets that are not there. - MoonLiveModifier: a failed compile falls back to passing coordinates through (the doc claimed the previous mapping survives), and a position a script computes past 255 keeps its low byte. Reviews - Reviewer (Fable): H1 (this commit's subject), M1, M2 and L1-L5 addressed. - CodeRabbit: 5 fixed, 4 already done, 1 skipped — replacing the compared source with a checksum trades a collision-shaped false negative for nothing on a cold path that compares once per prepare. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
docs/moonmodules/light/MoonLiveModifier.md (1)
49-55: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winDocument script-declared controls.
The table lists only
source, butMoonLiveModifier::defineControls()also exposes each user-declared MoonLive control after compilation. Document how a script declares these controls and state that changing one rebuilds the mapping.As per coding guidelines: “Documentation must describe the system as it currently exists.”
🤖 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/moonmodules/light/MoonLiveModifier.md` around lines 49 - 55, Update the Controls section of MoonLiveModifier documentation to include the user-declared controls exposed by MoonLiveModifier::defineControls(). Describe how scripts declare these controls and state that changing a declared control rebuilds the mapping, alongside the existing source control behavior.Source: Coding guidelines
test/unit/light/unit_MoonLiveModifier.cpp (1)
151-174: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy liftUse an observable compile count.
dynamicBytes()isengine_.codeCap(). Recompiling the same source can produce the same executable capacity, so Line 174 still passes ifmodifyLogical()compiles on every call. Add a compile-count test seam or an executable-allocation counter and assert that the count remains one across the fold.🤖 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/unit/light/unit_MoonLiveModifier.cpp` around lines 151 - 174, Replace the dynamicBytes() comparison in the MoonLiveModifier test with an observable compilation count, exposed through an appropriate test seam or executable-allocation counter. Capture the count after m.prepare(), assert it reflects one compilation, and verify it remains unchanged after all modifyLogical() calls; do not rely on executable capacity as the recompilation indicator.Source: Path instructions
🤖 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 `@src/light/moonlive/MoonLiveBuiltins_light.h`:
- Around line 47-52: Update mm_light_print and its printBudget diagnostic flow
so effect ticks perform only bounded, nonallocating queue writes and remain
noexcept, removing both std::printf calls from the tick path. Store the
formatted diagnostic records in a preallocated queue, then drain that queue from
a housekeeping path through the existing platform output seam, preserving the
print budget and burst-spent message behavior.
In `@src/platform/desktop/moonlive_asm_host.cpp`:
- Around line 48-59: Complete signed-immediate lowering in
HostAssembler::mulImm() so negative multipliers, including -1, are materialized
with signed semantics; update src/platform/desktop/moonlive_asm_host.cpp lines
48-59, while HostAssembler::movImm() is only context. In
src/platform/esp32/moonlive_asm_xtensa.cpp lines 58-71, prevent values below
-2048 from falling through to unsigned construction by materializing the full
signed value or rejecting the literal during compilation. Add backend tests
covering subtraction and a literal below -2048.
In `@test/unit/light/unit_MoonLiveModifier.cpp`:
- Around line 224-259: Update the test case name to distinguish the intentional
out-of-grid mapping from the default-script assertion, then add CHECK(inside ==
0) after the loop in “a script that computes a position outside the grid leaves
lights mapped” to assert every transformed coordinate is unmapped while
preserving the existing defInside check.
- Around line 290-308: Update the subtraction regression test around “a
subtraction produces the whole value, not just its low byte” to run against a
multi-light buffer and use the subtraction result as a bounds-checked element
index. Assert that the expected destination element is written, ensuring values
such as 99 and an incorrectly widened result cannot both pass through setXYZ’s
byte truncation.
---
Outside diff comments:
In `@docs/moonmodules/light/MoonLiveModifier.md`:
- Around line 49-55: Update the Controls section of MoonLiveModifier
documentation to include the user-declared controls exposed by
MoonLiveModifier::defineControls(). Describe how scripts declare these controls
and state that changing a declared control rebuilds the mapping, alongside the
existing source control behavior.
In `@test/unit/light/unit_MoonLiveModifier.cpp`:
- Around line 151-174: Replace the dynamicBytes() comparison in the
MoonLiveModifier test with an observable compilation count, exposed through an
appropriate test seam or executable-allocation counter. Capture the count after
m.prepare(), assert it reflects one compilation, and verify it remains unchanged
after all modifyLogical() calls; do not rely on executable capacity as the
recompilation indicator.
🪄 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: e555b367-e5cf-45b3-b0b5-f02211e7051e
📒 Files selected for processing (11)
docs/MIGRATING.mddocs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/light/MoonLiveModifier.mdsrc/light/moonlive/MoonLiveBuiltins_light.hsrc/light/moonlive/MoonLiveEffect.hsrc/light/moonlive/MoonLiveModifier.hsrc/platform/desktop/moonlive_asm_host.cppsrc/platform/esp32/moonlive_asm_xtensa.cpptest/unit/light/unit_Effects_container.cpptest/unit/light/unit_MoonLiveModifier.cpp
| extern "C" inline uint32_t mm_light_print(uint32_t v) { | ||
| uint32_t& left = printBudget(); | ||
| if (left > 0) { | ||
| std::printf("[script] %u\n", static_cast<unsigned>(v)); | ||
| if (--left == 0) std::printf("[script] (burst spent; edit the script for a fresh one)\n"); | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Remove blocking output from the effect tick path.
MoonLiveEffect::tick() can invoke mm_light_print() through engine_.run(). Line 50 calls std::printf, which can block the render tick. Store bounded diagnostic records in a preallocated queue and drain them from a housekeeping path through a platform seam.
As per path instructions: “render/tick code is nonblocking and noexcept; avoid allocation, blocking I/O, delays, or network calls there.”
🤖 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/MoonLiveBuiltins_light.h` around lines 47 - 52, Update
mm_light_print and its printBudget diagnostic flow so effect ticks perform only
bounded, nonallocating queue writes and remain noexcept, removing both
std::printf calls from the tick path. Store the formatted diagnostic records in
a preallocated queue, then drain that queue from a housekeeping path through the
existing platform output seam, preserving the print budget and burst-spent
message behavior.
Source: Path instructions
| // Subtraction is emitted as `a + (b * -1)`, and -1 has to survive into the register. The assemblers | ||
| // materialise a constant with a 16-bit immediate, so a naive -1 becomes 65535 and every subtraction | ||
| // is right only MODULO 256 — invisible in a stored byte, and wrong everywhere the full value is | ||
| // used: a bounds-guarded index silently drops the light, and a value handed to a host call is | ||
| // nonsense. Byte-comparison tests cannot see this, so it is checked through print(), which returns | ||
| // the full 32-bit value. | ||
| TEST_CASE("a subtraction produces the whole value, not just its low byte") { | ||
| // `a - b` compiles to `a + (b * -1)`, so -1 has to reach the register intact. The assemblers | ||
| // build a constant from a 16-bit immediate, and a naive -1 lands as 65535 — which leaves every | ||
| // subtraction correct only MODULO 256. A stored colour byte cannot show that (the low byte is | ||
| // right either way), so this checks the value THROUGH print(), which returns the full 32 bits | ||
| // and is therefore the only observer that can fail. | ||
| // | ||
| // The consequences the byte hides: an index computed by subtraction becomes ~65k, the element | ||
| // store's bounds guard rejects it, and the light silently never lights; a subtraction handed to | ||
| // a host call (random16, print) gets a wrong argument. | ||
| CHECK(transform("setXYZ(0, print(width - 1 - x), y, z);", 0, 0, 0, 16, 16, 1).x == 15); | ||
| CHECK(transform("setXYZ(0, print(100 - 1), y, z);", 0, 0, 0, 255, 255, 1).x == 99); | ||
| CHECK(transform("setXYZ(0, print(5 - 5), y, z);", 0, 0, 0, 255, 255, 1).x == 0); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Test subtraction through an untruncated observer.
Each print() result is immediately stored through setXYZ(), which keeps only its low byte. Both 99 and 65635 store as 99, so these checks pass when -1 is materialized as 65535. Execute the script against a multi-light buffer and use the subtraction result as a bounds-checked element index, then assert that the expected destination was written.
🤖 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/unit/light/unit_MoonLiveModifier.cpp` around lines 290 - 308, Update the
subtraction regression test around “a subtraction produces the whole value, not
just its low byte” to run against a multi-light buffer and use the subtraction
result as a bounds-checked element index. Assert that the expected destination
element is written, ensuring values such as 99 and an incorrectly widened result
cannot both pass through setXYZ’s byte truncation.
A layout can now be written as text on a running device instead of compiled in as a C++ class.
`for (i = 0; i < width; i = i + 1) { addLight(i, 0, 0); }` describes a strand; the nested form
describes a panel. The MoonLive language grows the loops, arithmetic and print() this needs, and a
`moonlive/` folder ships the scripts as editable files.
Performance: desktop tick 164 -> 149us (16384 lights); flash S3 +4.9KB, P4 +97KB, desktop +18KB.
The stored KPI read 133us, so this looks like a regression in the metrics diff; it is not. That
number came from a quieter machine. Re-measuring the pre-branch tree on THIS machine gives 164us,
against 149us here.
**Core**
- MoonLive language: `for` loops (bottom-tested, entry guard + back edge), script locals, and
precedence-climbing arithmetic. Subtraction emits `a + (b * -1)`.
- IR gains `Mov`, `Label`, `BranchGe`, `BranchNe`; 16 labels.
- `HostCallFn` widened to three arguments so a built-in can take a coordinate triple.
**Light domain**
- `MoonLiveLayout`: a scripted layout that **allocates nothing**. `lightCount()` runs the script with
a counting sink, `forEachCoord` runs it into the caller's, so the two answers come from the same
code and cannot drift. Staging coordinates would cost 48KB on a 16k-light fixture.
- `MoonLiveModifier`: a scripted coordinate fold, with the x/y/z preamble its script reads.
- Built-ins: `addLight`, `setXYZ`, `print`.
**Platform**
- Xtensa `Mov` lowers to a real register move. It was add-immediate-zero, which the ISA cannot
encode (that slot means -1), so a loop counter started at -1, the unsigned guard saw 0xffffffff,
and every loop exited immediately. Found by disassembling, after five wrong hypotheses.
- Negative immediates: Xtensa via 12-bit signed `movi`, arm64 via `movn`. `Const(-1)` was becoming
65535 on both, making subtraction correct only modulo 256. Out-of-range now fails the compile
rather than silently taking the unsigned path.
- RISC-V vreg map widened 12 -> 18, call frame 64 -> 112 bytes.
**Scripts/MoonDeck**
- `moonlive/` with layouts, effects and modifiers as `.mlv` files; each module ships one as default.
- `moondeck/moonlive/disasm.py` disassembles what a script emits, on the host, without a device.
**Tests**
- `unit_MoonLiveLayout`, `unit_MoonLiveScripts` (compiles every file in `moonlive/`),
`scenario_MoonLive_pipeline` (layout + effect + modifier all scripted, in one wired pipeline).
- Pinned: a scripted control keeps its live value across a source edit. The arena matches slots by
OFFSET, so a different script whose first control is also `width` inherits the value rather than
its own initialiser. Deliberate (the slider you moved survives an edit) and easy to be surprised
by, so it is a test rather than a comment.
**Docs**
- `print` documented in one place, `moonlive/README.md`, with the bindings pointing at it: it is for
debugging, it blocks, and it comes back out again before a script ships.
- Backlog: drain `print()` through a queue so nothing of it remains on the tick.
**Reviews**
- 🐇 `mulImm` negative handling: skipped, never called by the compiler.
- 🐇 Xtensa/arm64 `movImm` below the encodable range: fixed, both now fail the compile.
- 🐇 Subtraction test proved nothing (99 and 65635 are the same byte): fixed, asserts on a loop
bound instead, which is not truncated.
- 🐇 Out-of-grid modifier test passed on half the script: fixed with `CHECK(inside == 0)`.
- 🐇 `MoonLiveModifier.md` missing script-declared controls: fixed.
- 🐇 `print()` blocks a MM_NONBLOCKING tick: valid. Bounded by the burst cap and documented; the
queue-drain redesign is backlogged, not built here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 14
🤖 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/moonmodules/light/MoonLiveLayout.md`:
- Around line 52-62: Update the two-pass explanation near the claim that
identical arithmetic prevents drift so the guarantee explicitly applies only to
deterministic scripts. Acknowledge that supported stateful or random built-ins
such as random16 can produce different count and placement results, consistent
with the existing Limits warning.
In `@moondeck/moonlive/disasm.py`:
- Around line 58-69: Resolve Ruff E741 in the disassembly parsing by renaming
the ambiguous l variables to line in both comprehensions, including the hexbytes
construction and the objdump line search, without changing their behavior.
- Around line 42-46: Update the emitter build flow around subprocess.run to use
the project’s MoonDeck build entry point instead of invoking c++ directly.
Ensure the temporary emitter is built through the MoonDeck script or command so
project-configured compiler flags and dependencies are applied, while preserving
the existing emitter output behavior.
In `@moondeck/moonlive/emit_xtensa.cpp`:
- Around line 5-15: Remove the direct Xtensa implementation includes and
__XTENSA__ definition from the translation unit containing lowerToBytes. Expose
the required backend functionality through the platform-layer library/API under
src/platform/**, then have the MoonDeck utility depend on and link against that
API without compiling platform implementation files directly; preserve a single
backend definition.
In `@moonlive/effects/gradient.mlv`:
- Around line 3-5: Update InlineOp::StoreElem in moonlive_lower_riscv.cpp to
copy the index register into scratch before multiplying it for the byte offset,
preserving the original loop variable register for subsequent uses and the
for-step. Keep MoonLiveCompiler.cpp’s loop-variable register behavior and the
gradient example unchanged.
In `@moonlive/layouts/two-rows.mlv`:
- Around line 4-6: Update the light-emission loop in the two-row layout to
output all y=0 lights with ascending x coordinates first, then output all y=1
lights with descending x coordinates. Replace the current paired addLight calls
so the return row forms a reverse traversal.
In `@moonlive/README.md`:
- Around line 26-29: Remove the synchronous print path from render-executed
MoonLive scripts: update mm_light_print and its supporting code so rendering
only enqueues into a bounded, nonallocating noexcept queue drained outside the
render path, or disable the diagnostic in render builds. Ensure no std::printf
call remains reachable from render/tick code, and preserve the existing
burst-limit behavior where applicable.
In `@src/core/moonlive/MoonLiveCompiler.cpp`:
- Around line 417-433: Validate the condition and step identifiers in the
for-loop parsing flow against the active loop variable varName, rejecting
mismatches instead of silently using the loop local. Update the locals lookup
used by this flow to search from localCount - 1 down to zero so nested loop
variables shadow outer locals correctly; alternatively reject duplicate local
names.
- Around line 467-470: Update the loop-generation logic around the BranchGe and
BranchNe emissions to reject or safely bound steps that cannot advance toward
the limit, including a zero step such as i = i. Prefer validating that the
supported step is provably positive before emitting the back edge; otherwise
emit a bounded iteration guard so counter cannot loop indefinitely.
In `@src/platform/desktop/moonlive_asm_host.cpp`:
- Around line 112-120: Update the host-call frame and argument save/restore
logic in the surrounding assembly emission to preserve x3 alongside x0-x2, since
R3 carries elapsed time and may be clobbered by built-ins. Increase the stack
allocation and matching deallocation by a 16-byte-aligned amount, save x3 before
the call, and restore it afterward while keeping existing argument setup
unchanged.
In `@src/platform/esp32/moonlive_asm_riscv.cpp`:
- Around line 16-20: Remove the kScratchFn-to-x16 overlap by reserving a
register that is not included in kRvReg, and update call() to use that register
for the host-result stash. Ensure the selected scratch register does not
conflict with address calculation via kScratchAddr, and keep the result copy to
d outside the registers restored by call().
In `@src/platform/esp32/moonlive_lower_riscv.cpp`:
- Around line 64-69: Update the RISC-V call path used by the IrOp::Call case in
moonlive_lower_riscv.cpp and RiscvAssembler::call so the host-call return value
is saved in an unused stack slot before restoring saved[]/kScratchFn, then
reloaded into the destination afterward. Preserve existing argument passing and
restoration behavior while ensuring random16() and print() receive the actual a0
result.
In `@test/scenarios/light/scenario_MoonLive_pipeline.json`:
- Around line 13-52: Update the fixture to add an Effects module configured with
the existing Layouts module, parent Layer under Effects, and replace Drivers’
direct layer property with the effects reference. Preserve the existing module
IDs and driver setup while routing the scenario through Effects::setLayouts and
Drivers::setEffects.
In `@test/unit/light/unit_MoonLiveScripts.cpp`:
- Around line 70-73: Gate the native-code success assertions in the MoonLive
compile tests around the `engine.compile` checks, including the additional
affected cases, using the project’s supported-host/backend availability
condition. Preserve parser or script-validity coverage on unsupported hosts,
while only requiring `ok` and reporting compile failures when the native backend
is available.
🪄 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: eb712dde-aa4e-452c-a474-21717ddbee0c
📒 Files selected for processing (40)
docs/backlog/backlog-light.mddocs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/light/MoonLiveLayout.mddocs/moonmodules/light/MoonLiveModifier.mdmoondeck/moonlive/disasm.pymoondeck/moonlive/emit_xtensa.cppmoonlive/README.mdmoonlive/effects/gradient.mlvmoonlive/effects/random-pixel.mlvmoonlive/layouts/diagonal.mlvmoonlive/layouts/grid.mlvmoonlive/layouts/reversed-row.mlvmoonlive/layouts/two-rows.mlvmoonlive/modifiers/mirror.mlvmoonlive/modifiers/shift.mlvmoonlive/modifiers/transpose.mlvsrc/core/moonlive/MoonLiveBuiltins.hsrc/core/moonlive/MoonLiveCompiler.cppsrc/core/moonlive/MoonLiveIr.hsrc/light/moonlive/MoonLiveBuiltins_light.hsrc/light/moonlive/MoonLiveEffect.hsrc/light/moonlive/MoonLiveLayout.hsrc/main.cppsrc/platform/desktop/moonlive_asm_host.cppsrc/platform/desktop/moonlive_asm_host.hsrc/platform/desktop/moonlive_lower_host.cppsrc/platform/esp32/moonlive_asm_riscv.cppsrc/platform/esp32/moonlive_asm_riscv.hsrc/platform/esp32/moonlive_asm_xtensa.cppsrc/platform/esp32/moonlive_asm_xtensa.hsrc/platform/esp32/moonlive_lower_riscv.cppsrc/platform/esp32/moonlive_lower_xtensa.cpptest/CMakeLists.txttest/scenario_runner.cpptest/scenarios/light/scenario_MoonLive_pipeline.jsontest/scenarios/light/scenario_peripheral_grid_sweep.jsontest/unit/light/unit_MoonLiveLayout.cpptest/unit/light/unit_MoonLiveModifier.cpptest/unit/light/unit_MoonLiveScripts.cpp
| build = subprocess.run( | ||
| ["c++", "-std=c++20", "-O0", "-I", os.path.join(ROOT, "src"), | ||
| "-I", os.path.join(ROOT, "src", "platform", "desktop"), | ||
| TOOL_SRC, os.path.join(ROOT, "src", "core", "moonlive", "MoonLiveCompiler.cpp"), | ||
| "-o", emitter], |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use the MoonDeck build entry point.
This direct c++ invocation bypasses the project build configuration. Build the temporary emitter through the MoonDeck build path so its compiler flags and dependencies match the project.
As per coding guidelines, “Use the project's MoonDeck scripts for build, flash, monitor, test, and check tasks rather than invoking alternate tooling directly.”
🧰 Tools
🪛 ast-grep (0.45.0)
[error] 41-46: Command coming from incoming request
Context: subprocess.run(
["c++", "-std=c++20", "-O0", "-I", os.path.join(ROOT, "src"),
"-I", os.path.join(ROOT, "src", "platform", "desktop"),
TOOL_SRC, os.path.join(ROOT, "src", "core", "moonlive", "MoonLiveCompiler.cpp"),
"-o", emitter],
capture_output=True, text=True)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🪛 Ruff (0.16.1)
[error] 42-42: subprocess call: check for execution of untrusted input
(S603)
[warning] 42-42: subprocess.run without explicit check argument
Add explicit check=False
(PLW1510)
[error] 43-46: Starting a process with a partial executable path
(S607)
🤖 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/moonlive/disasm.py` around lines 42 - 46, Update the emitter build
flow around subprocess.run to use the project’s MoonDeck build entry point
instead of invoking c++ directly. Ensure the temporary emitter is built through
the MoonDeck script or command so project-configured compiler flags and
dependencies are applied, while preserving the existing emitter output behavior.
Source: Coding guidelines
| #define __XTENSA__ 1 | ||
| #include "platform/esp32/moonlive_asm_xtensa.h" | ||
| #include "platform/esp32/moonlive_asm_xtensa.cpp" | ||
| // The lowerer body, with the emit seam it expects. | ||
| #include "core/moonlive/MoonLiveIr.h" | ||
| #include "core/moonlive/MoonLiveBuiltins.h" | ||
| namespace mm::moonlive { | ||
| size_t lowerToBytes(const IrProgram& ir, uint8_t* out, size_t cap); | ||
| } | ||
| #include "platform/esp32/moonlive_lower_xtensa.cpp" | ||
| #undef __XTENSA__ |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Keep the Xtensa backend behind the platform layer.
moondeck/moonlive/emit_xtensa.cpp defines __XTENSA__ and directly includes platform implementation files. This moves platform-specific implementation ownership into a MoonDeck utility and can create duplicate definitions when normal backend sources are also linked. Expose a platform-layer library or API and link the utility to it.
As per path instructions, src/platform/** is the only place where platform-specific includes and hardware APIs are allowed.
🤖 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/moonlive/emit_xtensa.cpp` around lines 5 - 15, Remove the direct
Xtensa implementation includes and __XTENSA__ definition from the translation
unit containing lowerToBytes. Expose the required backend functionality through
the platform-layer library/API under src/platform/**, then have the MoonDeck
utility depend on and link against that API without compiling platform
implementation files directly; preserve a single backend definition.
Source: Path instructions
| "fixture": [ | ||
| { | ||
| "name": "fix-layouts", | ||
| "op": "add_module", | ||
| "id": "Layouts", | ||
| "type": "Layouts" | ||
| }, | ||
| { | ||
| "name": "fix-scripted-layout", | ||
| "op": "add_module", | ||
| "id": "MLL", | ||
| "type": "MoonLiveLayout", | ||
| "parent_id": "Layouts" | ||
| }, | ||
| { | ||
| "name": "fix-layer", | ||
| "op": "add_module", | ||
| "id": "Layer", | ||
| "type": "Layer", | ||
| "props": { | ||
| "layouts": "Layouts", | ||
| "channelsPerLight": 3 | ||
| } | ||
| }, | ||
| { | ||
| "name": "fix-drivers", | ||
| "op": "add_module", | ||
| "id": "Drivers", | ||
| "type": "Drivers", | ||
| "props": { | ||
| "layer": "Layer" | ||
| } | ||
| }, | ||
| { | ||
| "name": "fix-artnet", | ||
| "op": "add_module", | ||
| "id": "ArtNet", | ||
| "type": "NetworkSendDriver", | ||
| "parent_id": "Drivers" | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Exercise the Effects container in this end-to-end scenario.
The fixture creates a standalone Layer, and Drivers uses its direct layer fallback. This bypasses Effects::setLayouts and Drivers::setEffects, which are part of this rename.
Add an Effects fixture with Layouts configured. Parent Layer under Effects. Configure Drivers with effects.
🤖 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` around lines 13 - 52,
Update the fixture to add an Effects module configured with the existing Layouts
module, parent Layer under Effects, and replace Drivers’ direct layer property
with the effects reference. Preserve the existing module IDs and driver setup
while routing the scenario through Effects::setLayouts and Drivers::setEffects.
A `for` counter was destroyed by any setRGB or addLight in its own body on both device backends, so a loop ran the wrong number of times. Nested loops were refused outright on Xtensa, which meant the shipped default layout script could not compile on the smallest target. Both fixed, and confirmed on an S3 and a P4. Performance: desktop tick unchanged; flash S3 +80 B, P4 +144 B (the scratch registers and the x3 save). **Light domain** - `two-rows.mlv` is now the serpentine its own comment describes: out along y=0, back along y=1 with x descending, so the turn joins two lights that are neighbours on the wire. It emitted (0,0),(0,1),(1,0),(1,1)… — a zig-zag no physical strand can follow. **Platform** - `StoreElem` builds a light's byte address in SCRATCH instead of folding it into the caller's index register. Folding assumed the index is dead after the store — true for a throwaway temp, false for a `for` counter, which the step and the loop test read again. `mull a8, a8, a4` overwrote the counter and `bne a8, a7` then compared the corrupted value. Xtensa AND RISC-V; the desktop host was already correct because it used a scratch register, which is exactly why the suite stayed green. - Inline scratch is reserved only for the ops a program contains. The unconditional reservation cost every script a register, and that one register is what a nested loop was short of on Xtensa (12 registers, and the program needs 11). Nested loops now compile there. - arm64 `call()` saves x3 (kArg3, elapsed time), frame 112 -> 128 bytes. Unreachable today — the grammar has no `t` and no lowering reads it — but a built-in may clobber x3 under the AAPCS, so exposing `t` would have made it a silent wrong-value bug. The new encodings were checked by re-deriving the existing instructions with the same encoder first. - RISC-V `kScratchFn` was x16/a6 = vreg R12, so `call()` stashed its result there and the restore loop reloaded it from the frame, returning R12's stale value. Now t6, the only caller-saved register outside the map. A constexpr check on all three backends now rejects a scratch that is also a vreg; restoring the old value makes it fire, so it catches the bug rather than passing vacuously. **Tests** - A loop counter survives the body that uses it; a nested loop lays out a full grid on every target's register budget. - `unit_MoonLiveScripts` requires native-code success only where a backend exists for the host ISA (`MM_MOONLIVE_HAS_HOST_JIT` is 0 on x86_64, which is what CI runs) — it would have failed every script there for a reason unrelated to the script. The codegen diagnostic is now a named constant so the test distinguishes "no JIT here" from "this script is wrong" without matching on prose. **Docs/Scripts** - The two-pass count/emit guarantee states its condition: it holds for a deterministic script, and `random16` breaks it (the Limits section already said so, three paragraphs down). - A serpentine over arbitrary rows still needs `%` or `if`; a fixed few rows can be written as one loop per direction, which is what two-rows.mlv now does. - `disasm.py`: rename the ambiguous `l` bindings (Ruff E741). `emit_xtensa.cpp` records why it includes the backend sources directly: it is `#if defined(__XTENSA__)`, so on this host it compiles to nothing and there is no library to link — running the real emitter without a board is the point. **Reviews** - 🐇 RISC-V kScratchFn aliasing, and the call return destroyed by the restore: already fixed earlier on this branch; verified against current code and skipped. - 🐇 RISC-V StoreElem clobbers the loop variable: fixed, and Xtensa had it too. - 🐇 two-rows.mlv is not a serpentine: fixed. - 🐇 arm64 call() does not preserve x3: fixed. - 🐇 Ruff E741 in disasm.py; layout doc's unqualified drift claim; script tests need a host-JIT gate: all fixed. - 🐇 Scenario fixture should route through Effects: skipped, stale — it describes the fixture already replaced on this branch, which wires Layer.layouts and Drivers.layer like every working scenario. - 🐇 emit_xtensa.cpp should link a platform library instead of including .cpp files, and disasm.py should build through MoonDeck: skipped, same premise — the backend compiles to nothing off-target, so no such library exists. Reason recorded in the file. - 🐇 print() must not write from the render path: bounded by the burst cap, documented, and backlogged by name last commit; the queue redesign is its own change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/platform/esp32/moonlive_asm_xtensa.cpp (2)
174-180: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winStage argument A before overwriting
a11.
a11is a valid source register inkXtReg. Ifamaps toa11, Line 179 overwrites it before Line 180 reads it. A three-argument built-in then receives argument B as argument A.Use a non-vreg scratch such as
a13to stage argument A before assigninga11.Proposed fix
- emit2(uint16_t((uint32_t(ar(c)) << 8) | (12 << 4) | 0xd)); // mov a12, argC - emit2(uint16_t((uint32_t(ar(b)) << 8) | (11 << 4) | 0xd)); // mov a11, argB - emit2(uint16_t((uint32_t(ar(a)) << 8) | (10 << 4) | 0xd)); // mov a10, argA + emit2(uint16_t((uint32_t(ar(a)) << 8) | (13 << 4) | 0xd)); // mov a13, argA + emit2(uint16_t((uint32_t(ar(c)) << 8) | (12 << 4) | 0xd)); // mov a12, argC + emit2(uint16_t((uint32_t(ar(b)) << 8) | (11 << 4) | 0xd)); // mov a11, argB + emit2(uint16_t((13u << 8) | (10 << 4) | 0xd)); // mov a10, a13🤖 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/platform/esp32/moonlive_asm_xtensa.cpp` around lines 174 - 180, Update the three-argument emission sequence around the mov instructions to stage argument A in the non-vreg scratch register a13 before writing a11, then assign a10 from that staged value. Preserve the existing high-first ordering for arguments C and B and ensure ar(a) is read before a11 can be overwritten.
167-172: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPreserve
a14anda15across the window rotation.
kXtRegmaps virtual registersR10andR11toa14anda15.callx8rotates the window by 8, so these live values can be overwritten. Save both registers to the entry frame aroundcallx8and restore them with the other rotate-out values.🤖 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/platform/esp32/moonlive_asm_xtensa.cpp` around lines 167 - 172, Update the callx8 save/restore sequence around the rotate-out scratch handling to preserve a14 and a15, which hold virtual registers R10 and R11 via kXtReg. Store both registers in unused entry-frame slots before callx8 and restore them alongside a8, a9, and a11 afterward, without changing the existing a10 argument/result handling.Source: Path instructions
src/core/moonlive/MoonLiveCompiler.cpp (1)
435-440: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winReject lexer errors while scanning the deferred step expression.
If the step contains an unsupported character, such as
i = i / 1,Lexer::advance()leaves the lexer at the same character. This loop then never reachesTok::EndorTok::RParen. Compilation can block the caller indefinitely.Proposed fix
int depth = 0; while (!failed && lex.kind != Tok::End) { + if (lex.kind == Tok::Error) { + fail(lex.err); + return false; + } if (lex.kind == Tok::LParen) depth++; else if (lex.kind == Tok::RParen) { if (depth == 0) break; depth--; } lex.advance(); }🤖 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/MoonLiveCompiler.cpp` around lines 435 - 440, Update the deferred step-expression scanning loop in MoonLiveCompiler to detect lexer errors or lack of advancement after lex.advance(). When the lexer cannot advance, reject the expression and propagate the existing compilation failure path instead of continuing to loop; preserve normal stopping at Tok::End and the matching Tok::RParen.
🤖 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/moonmodules/light/MoonLiveLayout.md`:
- Line 54: Update the random16 explanation near the two-pass addLight
description: state that counts differ only when random output changes loop
bounds or the number of addLight calls, while random coordinate arguments can
produce different placements between passes without changing the count.
In `@src/platform/esp32/moonlive_asm_riscv.cpp`:
- Around line 16-17: Update the register mapping around kRvReg so host
callee-saved registers x18–x21 are preserved across JIT entry and exit:
implement matching save/restore logic in the registered-function prologue() and
epilogue(), including stack handling, or remove the corresponding R14–R17
mappings from kRvReg. Ensure the existing lowerToBytes() call preservation
remains consistent.
In `@test/unit/light/unit_MoonLiveLayout.cpp`:
- Around line 270-284: Extend the regression test for loop-counter preservation
to exercise StoreElem lowering through an effect or engine buffer, using a
six-iteration loop that calls setRGB with the loop index and asserting all six
indexed writes occur. Keep the existing addLight call-based test separately to
retain Call register-preservation coverage, and align the buffer setup and
assertions with the relevant docs/moonmodules specification.
---
Outside diff comments:
In `@src/core/moonlive/MoonLiveCompiler.cpp`:
- Around line 435-440: Update the deferred step-expression scanning loop in
MoonLiveCompiler to detect lexer errors or lack of advancement after
lex.advance(). When the lexer cannot advance, reject the expression and
propagate the existing compilation failure path instead of continuing to loop;
preserve normal stopping at Tok::End and the matching Tok::RParen.
In `@src/platform/esp32/moonlive_asm_xtensa.cpp`:
- Around line 174-180: Update the three-argument emission sequence around the
mov instructions to stage argument A in the non-vreg scratch register a13 before
writing a11, then assign a10 from that staged value. Preserve the existing
high-first ordering for arguments C and B and ensure ar(a) is read before a11
can be overwritten.
- Around line 167-172: Update the callx8 save/restore sequence around the
rotate-out scratch handling to preserve a14 and a15, which hold virtual
registers R10 and R11 via kXtReg. Store both registers in unused entry-frame
slots before callx8 and restore them alongside a8, a9, and a11 afterward,
without changing the existing a10 argument/result handling.
🪄 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: d264f38f-11a3-4be8-9bfb-470edcf04c29
📒 Files selected for processing (18)
docs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/light/MoonLiveLayout.mdmoondeck/moonlive/disasm.pymoondeck/moonlive/emit_xtensa.cppmoonlive/layouts/two-rows.mlvsrc/core/moonlive/MoonLiveCompiler.cppsrc/core/moonlive/MoonLiveCompiler.hsrc/core/moonlive/MoonLiveIr.hsrc/platform/desktop/moonlive_asm_host.cppsrc/platform/desktop/moonlive_lower_host.cppsrc/platform/esp32/moonlive_asm_riscv.cppsrc/platform/esp32/moonlive_asm_xtensa.cppsrc/platform/esp32/moonlive_lower_riscv.cppsrc/platform/esp32/moonlive_lower_xtensa.cpptest/scenarios/light/scenario_MoonLive_pipeline.jsontest/unit/light/unit_MoonLiveLayout.cpptest/unit/light/unit_MoonLiveScripts.cpp
A MoonLive script hit a wall at six statements, whichever kind it was: the compiler's working buffers were fixed-size members chosen when a script was one statement. They are now sized to the source, so a script is bounded by the device's memory instead. A scripted module also cost far more RAM than it should, which bootlooped an S3. Performance: desktop tick 149 -> 148us; flash S3 +1.3KB, P4 +640B, desktop +16KB. **Core** - `IrProgram` allocates its op array to fit, sized by a token pre-pass before parsing. It was `IrInst ops[64]` — 2 KB of stack for a one-statement script and a hard ceiling for a large one. `kMaxIrOps` survives only as a sanity bound so a runaway source fails with a diagnostic. - `MoonLive::compile` stages emitted code in a right-sized buffer instead of `uint8_t staging[768]`. - A `sourceHash` (FNV-1a, the idiom already used for schema and WS diffing) answers "has this source changed since I compiled it" in 4 bytes. - The `for` header's step-expression scan now stops on a lexer error. `Tok::Error` is not `Tok::End` and the lexer does not advance past the offending character, so a stray symbol there HUNG the compile instead of reporting it. **Light domain** - A scripted module was as large as three copies of its script. `compiled_` existed only to be compared, and the modifier's `full_` existed for the duration of one compile; the first is now the hash, the second a cold-path temporary. MoonLiveModifier 13328 -> 4632 B, Layout 8712 -> 4624 B. This is what bootlooped the S3: `ModuleFactory::registerType` constructs a `T probe` ON THE STACK, and 13 KB does not fit a 12288-byte main task. - The script buffer is 4 KB (was 512 B, which truncated a longer script silently — the ceiling a user meets first, since it is what the UI writes into). **Platform** - Xtensa saves a14/a15 across `call()`. They are vregs R10/R11 and CALL8 rotates the window out from under them, so a value live across a call was destroyed. Reachable on the SHIPPED default: `grid.mlv` is a nested loop (11 vregs, so R10 is in use) whose body calls addLight. - Xtensa stages argA through a13. a11 is vreg R9, so argA could BE a11 and the argB write clobbered it before the a10 move read it; high-first ordering alone did not cover that. - RISC-V drops x18-x21 from its vreg map. They are callee-saved and `prologue()` is empty, so the emitted routine returned to its caller with four registers clobbered. 14 vregs still exceeds Xtensa's 12; frame 112 -> 80 bytes with every offset moved to match. - The branch back-patch is bounds-checked in all three assemblers, and every label/fixup table access is guarded against a failed allocation — `buf_` is the caller's tightly-sized buffer now, where a stray patch corrupts the heap rather than landing in an oversized member. - `Scheduler::prepareTree` quiesces the render worker for the whole walk. applyState runs Layouts, then Layer, then Drivers, so a reshape leaves a window where the mapping is rebuilt for the new light count while the driver buffer is still the old size. The add/remove mutators already quiesce; a reshape did not. NOT MoonLive-specific — `LayoutBase::affectsPrepare` returns true for every layout control, so a compiled GridLayout width takes the same path. **Tests** - Resizing a scripted layout keeps the layer's mapping and buffer in step: six resizes, growing and shrinking, through a real Layouts + Layer. Green under ASan. - A loop counter survives the body that uses it, now covering the inline store (`setRGB`) as well as the call (`addLight`) — the bug was in StoreElem, which the call path never exercised. Verified by reintroducing the bug and watching the new subcase fail. - A stray character in a `for` header is rejected rather than spun on. - A script is limited by memory, not by a fixed op or code budget. - `unit_MoonLiveLayout` / `unit_MoonLiveModifier` gate on `MM_MOONLIVE_HAS_HOST_JIT`. It is 0 on x86_64, which is what CI runs, so every case there failed for a reason unrelated to the layout — this is the sanitizer failure on the last two commits, verified against a true no-backend build. **Docs/Scripts** - `disasm.py` links the platform layer (the new allocations need it) — it had stopped building. - The two-pass count/emit guarantee states when `random16` actually breaks it: the count only diverges when randomness drives a loop bound or the number of addLight calls; a random coordinate keeps the count and moves the lights. - Backlogged: shrink a scripted module below its 4 KB script buffer, and stop `registerType` building a whole module on the stack — that is the smaller fix and the one that unblocks a classic ESP32. **Reviews** - 🐇 Xtensa a14/a15 not preserved across call: fixed, confirmed in disassembly. - 🐇 Xtensa argA clobbered when it is a11: fixed, staged through a13. - 🐇 RISC-V x18-x21 callee-saved but clobbered: fixed by dropping them from the map. - 🐇 for-header scan can loop forever: fixed, with a regression test. - 🐇 Loop-counter test misses StoreElem: fixed, and the new subcase was proven to fail with the bug reintroduced. - 🐇 random16 two-pass wording imprecise: fixed. - 👾 Reviewer on the staged diff: the agent died on an API error, so no findings. I audited the part it was pointed at — every new allocation is freed on all paths (three destructors, two scope guards), and free(nullptr) is a no-op on both platforms. Known open: resizing a scripted layout still corrupts the heap on an S3 (a `block_locate_free` assert — a damaged free list, so a write past a block, not a race). The quiesce above did not fix it and the cause is not yet found; whether it predates this branch is being checked on hardware. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/core/moonlive/MoonLiveCompiler.cpp (1)
299-302: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate the stale comment above the arity check.
The comment states that the IR
Callop carries a single argument vreg and that a Call builtin must be unary. The code now passes three argument vregs and rejects onlyargc > 3. Align the comment with the three-argument contract.♻️ Proposed comment update
- // The IR Call op carries a single argument vreg, so a Call-kind builtin must be unary. - // (Today random16 is the only one.) Reject a multi-arg Call up front rather than silently - // dropping args[1..]; a future N-ary helper needs the IR Call contract widened first. + // The IR Call op carries three argument vregs, so a Call-kind builtin takes at most three + // arguments. Reject a wider Call up front rather than silently dropping args[3..]; a + // future N-ary helper needs the IR Call contract widened first. if (fn->kind == BuiltinKind::Call && fn->argc > 3) { fail("a call takes at most three arguments"); return; }🤖 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/MoonLiveCompiler.cpp` around lines 299 - 302, Update the comment above the arity check in the MoonLiveCompiler call-handling logic to describe the IR Call op’s three-argument-vreg contract and state that Call-kind builtins support at most three arguments. Keep the existing argc > 3 validation and failure behavior unchanged.src/platform/esp32/moonlive_asm_riscv.cpp (1)
130-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winStale frame documentation in
RiscvAssembler::call. The call frame grew from 64 to 80 bytes and gained argument staging slots, but the surrounding comments still describe the old layout. A reader checking offsets against the comments gets the wrong numbers.
src/platform/esp32/moonlive_asm_riscv.cpp#L130-L143: change "64-byte frame, 16-byte aligned" to 80-byte, and derive the saved set fromkRvRegso the list cannot drift from the register map.src/platform/esp32/moonlive_asm_riscv.cpp#L166-L170: change the trailing comments fromlw ra, 108(sp)andaddi sp, sp, 112to 76 and 80, matching the emitted encodings.🤖 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/platform/esp32/moonlive_asm_riscv.cpp` around lines 130 - 143, The RiscvAssembler::call frame documentation and saved-register handling are stale: in src/platform/esp32/moonlive_asm_riscv.cpp lines 130-143, update the header to describe the 80-byte frame and derive the saved set from kRvReg so it cannot drift; in lines 166-170, correct the trailing offset comments to ra at 76(sp) and stack restoration by 80 bytes.
🤖 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/history/plans/Plan-20260809` - MoonLive scales — right-sized IR, and the
stack as the register overflow.md:
- Around line 9-14: Update all repository-source Markdown links in this plan,
including the affected ranges, to prefix paths with ../../../ so links resolve
from docs/history/plans/ to the repository root; preserve the existing targets
and link text.
- Around line 220-232: Correct the ordered-list numbering in the verification
steps: keep the first item 6, then renumber the subsequent entries sequentially
from 7 through 10 without changing their content.
- Around line 50-55: Keep platform allocation and executable-memory placement
out of core: update the plan at docs/history/plans/Plan-20260809 - MoonLive
scales — right-sized IR, and the stack as the register overflow.md:50-55 to
specify a core-neutral compile/code-placement interface, and update
src/core/moonlive/MoonLive.cpp:61-76 to inject that interface into MoonLive.
Implement the concrete allocation and executable-code placement behind the
interface under src/platform/**, leaving src/core/** platform-independent.
In `@moonlive/README.md`:
- Around line 22-25: Update the README description of the source control’s
capacity to state that it has a fixed 4 KB limit, with the effective script
limit determined by whichever is reached first: the 4 KB source capacity or
available device memory. Remove the claim that script size is bounded only by
free memory or has no fixed budget.
In `@src/core/moonlive/MoonLive.cpp`:
- Around line 61-76: Update the staging allocation in the compile flow around
srcLen, stagingCap, and compileSource so a valid script is not rejected when the
initial estimate is too small. Base capacity on parsed IR or detect output
overflow and retry compilation with a larger buffer before reporting “codegen
failed,” while preserving cleanup on every allocation and retry path.
In `@src/core/moonlive/MoonLiveIr.h`:
- Line 6: Remove the platform/platform.h dependency from IrProgram in
MoonLiveIr.h and make op-array allocation core-neutral. Update
IrProgram::reserve and its callers to receive an injected allocator interface or
caller-owned span, with the light/platform layer supplying allocation and
release; preserve the existing sizing and storage behavior without platform
references in src/core.
- Around line 122-128: Update IrProgram::reserve to reset vregsUsed alongside
count before allocating the new instruction buffer, ensuring reused programs
start with no stale virtual-register usage mark.
In `@src/light/moonlive/MoonLiveEffect.h`:
- Around line 105-110: Update the comment above MoonLiveEffect::source_ to
remove obsolete references to the 512-byte limit, truncation, and any backlog
for increasing capacity; document only the current 4096-byte buffer rationale
and stable-address requirement.
In `@src/platform/esp32/moonlive_asm_riscv.cpp`:
- Around line 166-170: Update the inline comments on the restore instructions in
the surrounding `emit32` sequence to match their actual immediates: document
`encLw(1, 2, 76)` as loading from 76(sp) and `encAddi(2, 2, 80)` as adding 80 to
sp. Do not change the instruction values or surrounding restore logic.
In `@src/platform/esp32/moonlive_asm_xtensa.h`:
- Around line 27-39: Remove the first duplicated constructor documentation
paragraph in the ESP32 Xtensa assembler declaration, preserving the second block
that also documents branches. Apply the same cleanup to the corresponding
assembler documentation in the desktop host and ESP32 RISC-V declarations.
In `@test/unit/light/probe_w.cpp`:
- Around line 14-26: Add doctest assertions to the light-count test around the
existing controls loop and repeated lightCount() calls: verify the initial
count, assert each result equals the expected width multiplied by height, and
assert a, b, and c are equal for every width. Retain the diagnostic output while
making incorrect or inconsistent counts fail the test.
- Line 4: Add the missing <initializer_list> standard header alongside the
existing includes in probe_w.cpp so the range-based initializer-list loop
compiles in sanitizer builds.
In `@test/unit/light/unit_MoonLiveLayout.cpp`:
- Around line 388-418: Gate the code-generation TEST_CASE named “a script is
limited by memory, not by a fixed op or code budget” with
MM_MOONLIVE_HAS_HOST_JIT, since both subcases require a host backend. Move the
existing `#endif` below this TEST_CASE or apply the same conditional pattern used
in unit_MoonLiveScripts.cpp, while preserving both subcases unchanged when JIT
support is available.
In `@test/unit/light/unit_MoonLiveScripts.cpp`:
- Around line 111-115: Update the valid-script assertion in the MoonLive test
around the case variable c so kCodegenFailed is accepted only when
MM_MOONLIVE_HAS_HOST_JIT is unavailable, matching the existing logic around
lines 77-81. Keep the expected-failure assertion unchanged and preserve
acceptance of successful valid scripts on all hosts.
---
Outside diff comments:
In `@src/core/moonlive/MoonLiveCompiler.cpp`:
- Around line 299-302: Update the comment above the arity check in the
MoonLiveCompiler call-handling logic to describe the IR Call op’s
three-argument-vreg contract and state that Call-kind builtins support at most
three arguments. Keep the existing argc > 3 validation and failure behavior
unchanged.
In `@src/platform/esp32/moonlive_asm_riscv.cpp`:
- Around line 130-143: The RiscvAssembler::call frame documentation and
saved-register handling are stale: in src/platform/esp32/moonlive_asm_riscv.cpp
lines 130-143, update the header to describe the 80-byte frame and derive the
saved set from kRvReg so it cannot drift; in lines 166-170, correct the trailing
offset comments to ra at 76(sp) and stack restoration by 80 bytes.
🪄 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: a98fcdee-f1ae-4cd6-a4ac-71d1425795d7
📒 Files selected for processing (33)
docs/backlog/backlog-light.mddocs/history/plans/Plan-20260809 - MoonLive scales — right-sized IR, and the stack as the register overflow.mddocs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/light/MoonLiveLayout.mdmoondeck/moonlive/disasm.pymoondeck/moonlive/emit_xtensa.cppmoonlive/README.mdmoonlive/layouts/two-rows.mlvsrc/core/Scheduler.cppsrc/core/moonlive/MoonLive.cppsrc/core/moonlive/MoonLiveCompiler.cppsrc/core/moonlive/MoonLiveCompiler.hsrc/core/moonlive/MoonLiveIr.hsrc/light/moonlive/MoonLiveEffect.hsrc/light/moonlive/MoonLiveLayout.hsrc/light/moonlive/MoonLiveModifier.hsrc/platform/desktop/moonlive_asm_host.cppsrc/platform/desktop/moonlive_asm_host.hsrc/platform/desktop/moonlive_lower_host.cppsrc/platform/esp32/moonlive_asm_riscv.cppsrc/platform/esp32/moonlive_asm_riscv.hsrc/platform/esp32/moonlive_asm_xtensa.cppsrc/platform/esp32/moonlive_asm_xtensa.hsrc/platform/esp32/moonlive_lower_riscv.cppsrc/platform/esp32/moonlive_lower_xtensa.cpptest/CMakeLists.txttest/scenarios/light/scenario_MoonLiveEffect_livescript.jsontest/scenarios/light/scenario_MoonLive_pipeline.jsontest/unit/light/probe_w.cpptest/unit/light/unit_MoonLiveLayout.cpptest/unit/light/unit_MoonLiveModifier.cpptest/unit/light/unit_MoonLiveScripts.cpp
| - **Seven `addLight` statements in a row fail.** `kMaxIrOps = 64` ([MoonLiveIr.h:33](src/core/moonlive/MoonLiveIr.h#L33)) | ||
| and a call costs ~9 IR ops. No nesting, no register pressure — this is the wall a user meets first. | ||
| - **Nested `for` loops are refused on Xtensa.** Measured: `LOWER BAIL: vregsUsed=11 +2 > kRegCount=12` | ||
| ([moonlive_lower_xtensa.cpp:24](src/platform/esp32/moonlive_lower_xtensa.cpp#L24)). They work on | ||
| desktop and RISC-V, which have larger register maps. The shipped default layout script `grid.mlv` | ||
| is a nested loop, so the module's own default cannot compile on the smallest target. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the source-file links.
These relative paths resolve below docs/history/plans/, not at the repository root. For example, src/core/moonlive/MoonLiveIr.h resolves to docs/history/plans/src/core/moonlive/MoonLiveIr.h.
Prefix repository-source links with ../../../, and update all affected links in this plan.
Also applies to: 31-46, 50-52, 67-70, 83-103, 114-126, 171-191
🤖 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/history/plans/Plan-20260809` - MoonLive scales — right-sized IR, and the
stack as the register overflow.md around lines 9 - 14, Update all
repository-source Markdown links in this plan, including the affected ranges, to
prefix paths with ../../../ so links resolve from docs/history/plans/ to the
repository root; preserve the existing targets and link text.
| - **Allocate to fit the script, not to `kMax`.** The op array and the code buffer move to a | ||
| right-sized `platform::alloc` ([platform.h:55](src/platform/platform.h#L55)), sized from a cheap | ||
| pre-pass over the source and freed when compilation ends. A one-statement script pays for one | ||
| statement instead of 3.4 KB, so this *reduces* peak memory for the common case while removing the | ||
| ceiling for the rare one. Compilation is cold-path, so an allocation there costs nothing that | ||
| matters. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Keep platform allocation and executable placement outside src/core.
The plan assigns platform::alloc to core code, and MoonLive::compile implements that design. This breaks the required core/platform boundary. It also leaves MoonLive.cpp coupled to platform allocation and executable-memory behavior.
docs/history/plans/Plan-20260809 - MoonLive scales — right-sized IR, and the stack as the register overflow.md#L50-L55: change the design to use a core-neutral compile and code-placement interface.src/core/moonlive/MoonLive.cpp#L61-L76: inject that interface intoMoonLive, and move allocation and executable-code placement tosrc/platform/**.
Based on learnings: src/core must remain platform-independent and executable-memory handling must move behind a core/platform boundary. As per path instructions: src/core/** must be platform-independent, and only src/platform/** may contain platform-specific APIs.
📍 Affects 2 files
docs/history/plans/Plan-20260809 - MoonLive scales — right-sized IR, and the stack as the register overflow.md#L50-L55(this comment)src/core/moonlive/MoonLive.cpp#L61-L76
🤖 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/history/plans/Plan-20260809` - MoonLive scales — right-sized IR, and the
stack as the register overflow.md around lines 50 - 55, Keep platform allocation
and executable-memory placement out of core: update the plan at
docs/history/plans/Plan-20260809 - MoonLive scales — right-sized IR, and the
stack as the register overflow.md:50-55 to specify a core-neutral
compile/code-placement interface, and update
src/core/moonlive/MoonLive.cpp:61-76 to inject that interface into MoonLive.
Implement the concrete allocation and executable-code placement behind the
interface under src/platform/**, leaving src/core/** platform-independent.
Sources: Path instructions, Learnings
| 6. **Degrade**: a deliberately absurd script fails with a clear diagnostic and no crash; an alloc | ||
| failure in `reserve()` fails the compile cleanly rather than writing through a null pointer. | ||
| 6. **Unchanged behaviour**: `unit_moonlive_ir` / `unit_moonlive_fill` (the `fill` behavioural golden, | ||
| and kArg4 surviving a call) stay green — they pin that a `FillElems` program still gets its | ||
| scratch. `unit_MoonLiveScripts.cpp:118` (bare vs commented produce equal length) is the canary for | ||
| the pass accidentally becoming source-dependent. | ||
| 7. **Encodings on device backends without flashing**: `uv run moondeck/moonlive/disasm.py` on a | ||
| spilling script, reading the actual `s32i`/`l32i` offsets against the frame layout. This is the | ||
| tool that found the `Mov`→`addi 0` bug. Extend it to RISC-V, which has no equivalent today. | ||
| 8. **Memory + hot path**: `collect_kpi.py --commit`. The IR allocation is cold-path, but a modifier | ||
| script runs once per light, so measure a mapping rebuild on a large grid. Confirm a non-spilling | ||
| script emits no prologue and costs nothing. | ||
| 9. **Hardware, the final gate (PO)**: flash `grid.mlv` on an S3 (Xtensa — the target that fails |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix the ordered-list numbering.
Line 222 repeats item 6. Renumber the remaining verification steps from 7 through 10.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 222-222: Ordered list item prefix
Expected: 7; Actual: 6; Style: 1/2/3
(MD029, ol-prefix)
[warning] 226-226: Ordered list item prefix
Expected: 8; Actual: 7; Style: 1/2/3
(MD029, ol-prefix)
[warning] 229-229: Ordered list item prefix
Expected: 9; Actual: 8; Style: 1/2/3
(MD029, ol-prefix)
[warning] 232-232: Ordered list item prefix
Expected: 10; Actual: 9; Style: 1/2/3
(MD029, ol-prefix)
🤖 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/history/plans/Plan-20260809` - MoonLive scales — right-sized IR, and the
stack as the register overflow.md around lines 220 - 232, Correct the
ordered-list numbering in the verification steps: keep the first item 6, then
renumber the subsequent entries sequentially from 7 through 10 without changing
their content.
Source: Linters/SAST tools
| A script's size is bounded by the device's free memory, not by a fixed budget: the compiler sizes | ||
| its working buffers to the source it is given. The `source` control holds 4 KB of text, which is a | ||
| few hundred statements — well past what a layout or effect needs. A script that outgrows the device | ||
| fails to compile with a diagnostic; it never truncates silently. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Describe the fixed source limit.
The source control has a fixed 4096-byte buffer. State that the effective limit is the 4 KB source capacity or available device memory, whichever is reached first. Do not state that script size has no fixed budget.
As per coding guidelines, “Documentation must describe the system as it currently exists.”
🤖 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 `@moonlive/README.md` around lines 22 - 25, Update the README description of
the source control’s capacity to state that it has a fixed 4 KB limit, with the
effective script limit determined by whichever is reached first: the 4 KB source
capacity or available device memory. Remove the claim that script size is
bounded only by free memory or has no fixed budget.
Source: Coding guidelines
| // Staging sized to the SOURCE, not to a fixed worst case. It was `uint8_t staging[768]`, which | ||
| // capped a script's emitted code at 768 bytes — about six statements — and put 768 bytes on the | ||
| // stack of a 12 KB task for every compile, however small the script. | ||
| // | ||
| // Eight code bytes per source character is the upper bound: the densest measured construct is a | ||
| // three-argument call at ~128 emitted bytes from ~16 characters. The assembler's own overflow | ||
| // check still catches a miss, so a wrong factor fails the compile rather than corrupting memory. | ||
| // It matters that this is not wildly generous — a 6 KB script on an ESP32 cannot afford an | ||
| // allocation sized by guesswork. Freed before returning, on every path. | ||
| const size_t srcLen = source ? std::strlen(source) : 0; | ||
| const size_t stagingCap = 256 + srcLen * 8; | ||
| auto* staging = static_cast<uint8_t*>(platform::alloc(stagingCap)); | ||
| if (!staging) { freeCode(); error_ = "no code memory"; return false; } | ||
| struct Free { uint8_t* p; ~Free() { platform::free(p); } } freeStaging{staging}; | ||
|
|
||
| CompileResult cr = compileSource(source, table, staging, stagingCap); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Replace the fixed source-to-code allocation multiplier.
Line 71 allocates 256 + srcLen * 8 bytes. A call also emits argument materialization and the backend call-save sequence. Repeated short valid calls can exceed this capacity. The lowerer then returns zero and reports codegen failed.
Estimate code capacity from parsed IR, or distinguish output overflow and retry with a larger buffer. Do not reject a valid script because the initial staging estimate is too small.
🤖 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 61 - 76, Update the staging
allocation in the compile flow around srcLen, stagingCap, and compileSource so a
valid script is not rejected when the initial estimate is too small. Base
capacity on parsed IR or detect output overflow and retry compilation with a
larger buffer before reporting “codegen failed,” while preserving cleanup on
every allocation and retry path.
| // @module MoonLiveLayout | ||
| #include "doctest.h" | ||
| #include "light/moonlive/MoonLiveLayout.h" | ||
| #include <cstring> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Add the required standard header.
The range-based initializer-list loop requires <initializer_list>. The sanitizer builds fail before tests run.
Proposed fix
`#include` <cstring>
+#include <initializer_list>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #include <cstring> | |
| `#include` <cstring> | |
| `#include` <initializer_list> |
🤖 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/unit/light/probe_w.cpp` at line 4, Add the missing <initializer_list>
standard header alongside the existing includes in probe_w.cpp so the
range-based initializer-list loop compiles in sanitizer builds.
Source: Pipeline failures
| std::printf(" initial lights=%u\n", (unsigned)l.lightCount()); | ||
| // Does an unchanged source recompile? codeCap changes address each time if so. | ||
|
|
||
| const auto& cs = l.controls(); | ||
| for (uint8_t v : {10, 20, 32, 64, 1, 16}) { | ||
| for (uint8_t i = 0; i < cs.count(); i++) | ||
| if (cs[i].name && std::strcmp(cs[i].name, "width") == 0) | ||
| *static_cast<uint8_t*>(cs[i].ptr) = v; | ||
| l.prepare(); | ||
| // Ask several times, as the pipeline does (count, then walk, then count). | ||
| auto a = l.lightCount(); auto b = l.lightCount(); auto c = l.lightCount(); | ||
| std::printf(" width=%3u -> %u/%u/%u dynamicBytes=%u\n", v, | ||
| (unsigned)a,(unsigned)b,(unsigned)c,(unsigned)l.dynamicBytes()); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add assertions for the repeated width changes.
This test only writes diagnostic output. It passes when lightCount() returns incorrect or inconsistent values. Assert the initial count, each expected width * height count, and equality across the repeated queries.
As per path instructions, “Unit and integration tests using doctest. Verify tests cover edge cases and match the specifications.”
🧰 Tools
🪛 Cppcheck (2.21.0)
[error] 23-23: failed to evaluate #if condition, undefined function-like macro invocation
(syntaxError)
🪛 GitHub Actions: Test / 0_sanitizers (address).txt
[error] 18-18: GCC compilation failed: deducing from a brace-enclosed initializer list requires '#include <initializer_list>'. Add the missing header. The 'mm_tests' build failed with exit code 2.
🪛 GitHub Actions: Test / 2_sanitizers (realtime).txt
[error] 18-18: C++ compilation failed: cannot deduce the initializer-list type because <initializer_list> was not included. Add #include <initializer_list>.
🪛 GitHub Actions: Test / 3_sanitizers (thread).txt
[error] 18-18: Compilation failed: deducing from a brace-enclosed initializer list requires '#include <initializer_list>'. Add the missing include before using the initializer-list range in the for loop. Command failed during 'cmake --build build/san --target mm_tests'.
🪛 GitHub Actions: Test / sanitizers (address)
[error] 18-18: GCC compilation failed in the mm_tests build: deducing from a brace-enclosed initializer list requires '#include <initializer_list>' for the range-based loop. Failed command: cmake --build build/san --target mm_tests.
🪛 GitHub Actions: Test / sanitizers (realtime)
[error] 18-18: Build of mm_tests failed: cannot deduce the initializer-list type because <initializer_list> was not included. Add #include <initializer_list> to probe_w.cpp.
🪛 GitHub Actions: Test / sanitizers (thread)
[error] 18-18: Compilation failed: deducing from a brace-enclosed initializer list requires '#include <initializer_list>' for the range-based loop. Add the missing include.
🤖 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/unit/light/probe_w.cpp` around lines 14 - 26, Add doctest assertions to
the light-count test around the existing controls loop and repeated lightCount()
calls: verify the initial count, assert each result equals the expected width
multiplied by height, and assert a, b, and c are equal for every width. Retain
the diagnostic output while making incorrect or inconsistent counts fail the
test.
Source: Path instructions
This reverts commit 32026eb.
Resizing a scripted layout corrupted the heap and crashed an S3 roughly once every twelve changes. Two separate unbounded writes, both found by evidence: 320 resizes on a production build now run clean, plus malformed scripts, a 60,000-iteration loop, and 40s of concurrent edits from two threads. Performance: desktop tick 149 -> 150us; S3 tick ~2150us unchanged; flash S3 +272 B, P4 -16 B. (The stored esp32 KPI of 4164us was recorded on a different board state — this session measured ~2150us both before and after, so the metric's 2013us drop is a baseline change, not a speedup.) **Light domain** - `blendMap`'s mapped path is bounded by the destination it was handed. The identity path beside it has always clamped to min(src, dst); the LUT path did not, so a mapping built for a NEW light count wrote past a driver buffer still sized for the old one. ASan reproduces the overflow deterministically with the guard removed. - `Layer::buildFoldedLUT`'s pass-B scatter is bounded by what it allocated. The two-pass CSR build sizes `dests` from pass A, then scatters into it — safe only while both passes see the same coordinates. `forEachCoord` triggers a scripted layout's lazy compile, so a control edited between the passes makes pass B emit more lights than pass A counted and the write runs off the end. This was the main culprit: the corruption surfaced later inside an unrelated allocation, which is why the crash pointed everywhere except the writer. - The `addLight` sink is thread_local. It was one process-wide pair set and cleared around each run, so the HTTP task cleared it mid-run of the render task and the built-in called a live function pointer with a null context. Same shape as the WDT flag in the ESP32 worker. - A scripted module reports every heap byte it holds (`MoonLive::heapBytes`), not just the exec block — the control arena was invisible, and a failed compile reported 0 while the arena was still live. **Platform** - Xtensa saves a14/a15 across `call()`. They are vregs R10/R11 and CALL8 rotates the window out from under them, so a value live across a call was destroyed. Reachable on the SHIPPED default: grid.mlv is a nested loop (11 vregs, so R10 is in use) whose body calls addLight. - Xtensa stages argA through a13. a11 is vreg R9, so argA could BE a11 and the argB write clobbered it before the a10 move read it; high-first ordering alone did not cover that. - RISC-V drops x18-x21 from its vreg map. They are callee-saved and `prologue()` is empty, so the emitted routine returned to its caller with four registers clobbered. 14 vregs still exceeds Xtensa's 12; frame 112 -> 80 bytes with every offset moved to match. **Core** - The `for` header's step-expression scan stops on a lexer error. `Tok::Error` is not `Tok::End` and the lexer does not advance past the offending character, so a stray symbol HUNG the compile. **Scripts/MoonDeck** - `monitor_esp32` decodes panic backtraces inline, the equivalent of PlatformIO's `esp32_exception_decoder`: each address resolves to function/file/line against that build's ELF, picking the Xtensa or RISC-V toolchain from the firmware name. It also decodes the register-dump PC (useful when the frame chain is destroyed), says so when a backtrace is marked CORRUPTED, and warns when the device's firmware SHA does not match the ELF — decoding against the wrong build is confidently wrong. Without `--firmware` it behaves exactly as before; decoding must never cost the serial output. **Tests** - A mapping larger than its destination writes only what the buffer holds (ASan-verified: the guard removed reproduces heap-buffer-overflow). - A layout that changes size mid-build cannot overrun the mapping. - Two threads can run scripts at once without stealing each other's sink. - A scripted layout reports every heap byte it holds, compiled or not. - A loop counter survives the body that uses it, now covering the inline store (setRGB) as well as the call (addLight) — the bug was in StoreElem, which the call path never exercised. - A stray character in a `for` header is rejected, not spun on. - `unit_MoonLiveLayout` / `unit_MoonLiveModifier` gate on `MM_MOONLIVE_HAS_HOST_JIT`. It is 0 on x86_64, which is what CI runs, so every case there failed for a reason unrelated to the layout — this is the sanitizer failure on the last commits. **Docs** - architecture.md states the rule: a scripted module differs from a compiled one in one thing only — where its behaviour comes from. A mechanism one needs and the other does not is a finding. The one current exception is named: `applyState()` prepares parent-before-child, so a container asks children for their extent before they have prepared, which is why MoonLiveLayout compiles lazily from a const method. That lazy compile is what let the layout change size mid-build above, so the smell and the crash were the same bug. The fix is a two-phase prepare (build child-first, aggregate parent-first); it touches the base class and the six aggregating containers, so it belongs on its own branch rather than folded in here. - The two-pass count/emit guarantee states when `random16` actually breaks it: the count only diverges when randomness drives a loop bound or the number of addLight calls; a random coordinate keeps the count and moves the lights. **Reviews** - 🐇 Xtensa a14/a15 not preserved across call: fixed, confirmed in disassembly. - 🐇 Xtensa argA clobbered when it is a11: fixed, confirmed in disassembly. - 🐇 RISC-V x18-x21 callee-saved but clobbered: fixed by dropping them from the map. The P4 build caught a bad edit here that the desktop build cannot see — the array and its enum had drifted. - 🐇 for-header scan can loop forever: fixed, with a regression test. - 🐇 Loop-counter test misses StoreElem: fixed. - 🐇 random16 two-pass wording imprecise: fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/platform/esp32/moonlive_asm_riscv.cpp (1)
172-181: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick winStop branch patching after an emission overflow.
emit32()andemit()setoverflow_without advancinglen_. A later branch records a fixup at that full buffer offset.finalize()then callspatchBranches()beforelowerToBytes()checksoverflowed(). Both implementations can read or write pastbuf_during compilation of a valid script that exceeds the fixed code capacity.
src/platform/esp32/moonlive_asm_riscv.cpp#L172-L181: Return before readingbuf_ + f.atwhenoverflow_is set.src/platform/esp32/moonlive_asm_xtensa.cpp#L209-L214: Return before writingbuf_[f.at + 2]whenoverflow_is set.Proposed fix
void RiscvAssembler::patchBranches() { + if (overflow_) return; for (uint8_t i = 0; i < fixupCount_; i++) {void XtensaAssembler::patchBranches() { + if (overflow_) return; for (uint8_t i = 0; i < fixupCount_; i++) {🤖 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/platform/esp32/moonlive_asm_riscv.cpp` around lines 172 - 181, Stop branch patching immediately when overflow_ is set: in patchBranches() within src/platform/esp32/moonlive_asm_riscv.cpp at lines 172-181, return before reading buf_ + f.at; likewise, in src/platform/esp32/moonlive_asm_xtensa.cpp at lines 209-214, return before writing buf_[f.at + 2].src/platform/desktop/moonlive_asm_host.cpp (1)
166-171: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winReject invalid and unbound branch labels.
Line 166 indexes
labelPos_before it validatesf.label. An invalid label reads past the fixed table. An unbound label leaves the branch offset at zero and does not setoverflow_. A taken conditional branch then targets itself, andlowerToBytes()can return executable code that loops forever.Set
overflow_and stop patching whenf.label >= kMaxLabelsor when its target is unbound.Proposed fix
for (uint8_t i = 0; i < fixupCount_; i++) { const Fixup& f = fixups_[i]; + if (f.label >= kMaxLabels) { + overflow_ = true; + return; + } int32_t target = labelPos_[f.label]; - if (target < 0) continue; + if (target < 0) { + overflow_ = true; + return; + }🤖 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/platform/desktop/moonlive_asm_host.cpp` around lines 166 - 171, Update the branch-patching loop around labelPos_ to validate f.label against kMaxLabels before indexing the table, and set overflow_ then stop patching for invalid labels. Also set overflow_ and stop patching when the validated label’s target is unbound, rather than leaving the branch unchanged; preserve normal immediate patching for valid bound labels.
🤖 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 `@moondeck/MoonDeck.md`:
- Around line 959-964: Add the text language identifier to the fenced code block
containing the Guru Meditation monitor output in MoonDeck.md, changing the
opening fence to use text while preserving the output content unchanged.
In `@moondeck/run/monitor_esp32.py`:
- Around line 111-130: Store the firmware SHA comparison result from the SHA_RE
validation near the existing warning, and use it to prevent decoding when the
running SHA does not match elf_sha. Keep reporting the mismatch, but skip the
decode(tool, elf, addrs) call in the subsequent frame-processing loop until the
SHA is valid.
- Around line 71-80: Update the firmware setup block around find_addr2line and
elf_sha so OSError from Path(elf).read_bytes() is caught; on failure, reset
tool, elf, and elf_sha to None and print the existing raw-address fallback
message, allowing monitoring to continue to the serial-port setup.
---
Outside diff comments:
In `@src/platform/desktop/moonlive_asm_host.cpp`:
- Around line 166-171: Update the branch-patching loop around labelPos_ to
validate f.label against kMaxLabels before indexing the table, and set overflow_
then stop patching for invalid labels. Also set overflow_ and stop patching when
the validated label’s target is unbound, rather than leaving the branch
unchanged; preserve normal immediate patching for valid bound labels.
In `@src/platform/esp32/moonlive_asm_riscv.cpp`:
- Around line 172-181: Stop branch patching immediately when overflow_ is set:
in patchBranches() within src/platform/esp32/moonlive_asm_riscv.cpp at lines
172-181, return before reading buf_ + f.at; likewise, in
src/platform/esp32/moonlive_asm_xtensa.cpp at lines 209-214, return before
writing buf_[f.at + 2].
🪄 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: 0d855154-cc2c-4a70-a59b-ba2bcf2b61cd
📒 Files selected for processing (35)
docs/architecture.mddocs/backlog/backlog-light.mddocs/metrics/repo-health.jsondocs/metrics/repo-health.mdmoondeck/MoonDeck.mdmoondeck/moondeck_config.jsonmoondeck/moonlive/disasm.pymoondeck/run/monitor_esp32.pymoonlive/README.mdsrc/core/moonlive/MoonLive.hsrc/core/moonlive/MoonLiveCompiler.cppsrc/core/moonlive/MoonLiveCompiler.hsrc/core/moonlive/MoonLiveIr.hsrc/light/layers/BlendMap.hsrc/light/layers/Layer.hsrc/light/moonlive/MoonLiveBuiltins_light.hsrc/light/moonlive/MoonLiveEffect.hsrc/light/moonlive/MoonLiveLayout.hsrc/light/moonlive/MoonLiveModifier.hsrc/platform/desktop/moonlive_asm_host.cppsrc/platform/desktop/moonlive_asm_host.hsrc/platform/desktop/moonlive_lower_host.cppsrc/platform/esp32/moonlive_asm_riscv.cppsrc/platform/esp32/moonlive_asm_riscv.hsrc/platform/esp32/moonlive_asm_xtensa.cppsrc/platform/esp32/moonlive_asm_xtensa.hsrc/platform/esp32/moonlive_lower_riscv.cppsrc/platform/esp32/moonlive_lower_xtensa.cpptest/CMakeLists.txttest/scenarios/light/scenario_MoonLiveEffect_livescript.jsontest/scenarios/light/scenario_MoonLive_pipeline.jsontest/unit/light/unit_BlendMap.cpptest/unit/light/unit_MoonLiveLayout.cpptest/unit/light/unit_MoonLiveModifier.cpptest/unit/light/unit_MoonLiveScripts.cpp
💤 Files with no reviewable changes (6)
- test/CMakeLists.txt
- src/core/moonlive/MoonLiveCompiler.h
- moonlive/README.md
- docs/backlog/backlog-light.md
- moondeck/moonlive/disasm.py
- src/core/moonlive/MoonLiveCompiler.cpp
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>
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
docs/MIGRATING.md (1)
55-55: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winUpdate the manual preset migration.
The documented file edit changes
"Layers"to"Effects"but leaves the persisted"role": "layer"value unchanged. That preset still loads without a pad tint, as Line 53 states. Rename the role to"effects"in the manual migration, or instruct users to re-save the preset after the edit.As per coding guidelines, "
**/*.md: Documentation must describe the system as it currently exists."🤖 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/MIGRATING.md` at line 55, Update the manual preset migration instructions near the Layers-to-Effects rename to also change the persisted role value from "layer" to "effects" in each preset JSON, ensuring migrated presets load with the correct pad tint.Source: Coding guidelines
docs/moonmodules/light/MoonLiveModifier.md (1)
11-11: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winDocument
forloops as supported syntax.MoonLiveCompiler.cppnow parses bracedforloops. A modifier still runs once per coordinate, but its script can contain a loop.
docs/moonmodules/light/MoonLiveModifier.md#L11-L11: replace “It does not loop” with wording that distinguishes per-coordinate invocation from supported script syntax.docs/moonmodules/light/MoonLiveModifier.md#L39-L39: removeforfrom the unsupported grammar list.As per coding guidelines, “Documentation must describe the system as it currently exists.”
🤖 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/moonmodules/light/MoonLiveModifier.md` at line 11, Update docs/moonmodules/light/MoonLiveModifier.md:11 to clarify that the layer invokes the modifier once per physical light/coordinate while the modifier script itself may contain supported braced for loops. At docs/moonmodules/light/MoonLiveModifier.md:39, remove for from the unsupported grammar list; no other syntax documentation needs changing.Source: Coding guidelines
src/platform/desktop/moonlive_asm_host.h (1)
59-64: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the host-call documentation to match the three-argument API.
HostAssembler::callnow accepts three source registers, but the comment still saysd = fn(a). Documentd = fn(a, b, c)so the backend contract matches the declaration.Proposed fix
- // Call a host built-in: d = fn(a). Preserves the host-arg registers + // Call a host built-in: d = fn(a, b, c). Preserves the host-arg registers🤖 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/platform/desktop/moonlive_asm_host.h` around lines 59 - 64, Update the documentation for HostAssembler::call to describe the three-argument operation as d = fn(a, b, c), while preserving the existing register-preservation and live-vreg contract details.src/core/moonlive/MoonLiveIr.h (1)
87-92: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winCorrect the branch-label limit wording.
kIrLabels = 16allocates new label IDs as each loop is parsed; a sequential script can use more than eightforconstructs if the compiler stopped there. Reserve two labels usinglocalCountfor the actual nesting depth, or changekIrLabelsto match the intended per-program label capacity.🤖 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` around lines 87 - 92, Correct the branch-label capacity around kIrLabels and its documentation: account for two labels per for construct when determining supported nesting depth, using localCount if that is the intended limit, or set kIrLabels to the actual per-program label capacity. Ensure the documented limit matches the compiler and assembler behavior.src/light/moonlive/MoonLiveBuiltins_light.h (1)
24-27: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftRemove mutable process-global state from concurrent built-ins.
Layout scripts can run on more than one thread.
mm_light_random16()writesswithout synchronization.printBudget()is also decremented and reset without synchronization. These operations cause C++ data races and undefined behavior.
src/light/moonlive/MoonLiveBuiltins_light.h#L24-L27: make the PRNG state execution-local, such asthread_local, or pass it through a binding-owned context.src/light/moonlive/MoonLiveBuiltins_light.h#L111-L124: store diagnostic budget in binding-owned state and synchronize its reset and consumption. Do not share the counter across script executions.🤖 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/MoonLiveBuiltins_light.h` around lines 24 - 27, Remove mutable process-global state from both concurrent built-ins: update mm_light_random16() to use execution-local PRNG state, such as thread_local or binding-owned context, and update printBudget() to use binding-owned diagnostic state with synchronized reset and consumption; apply the changes at src/light/moonlive/MoonLiveBuiltins_light.h lines 24-27 and 111-124.
♻️ Duplicate comments (1)
moondeck/moonlive/emit_xtensa.cpp (1)
1-20: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftKeep the Xtensa backend in the platform layer.
This translation unit defines
__XTENSA__and includes platform assembler and lowering implementations directly. That violates the platform-layer boundary and can create duplicate backend definitions when normal platform sources are linked. Expose the required lowering API from the platform target and link this utility against it.🤖 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/moonlive/emit_xtensa.cpp` around lines 1 - 20, Remove the direct __XTENSA__ definition and included platform implementation files from the utility translation unit. Expose the required lowerToBytes API through the platform target, then link this utility against that target while retaining only the necessary public declarations and IR includes.Source: Path instructions
🤖 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/moonmodules/light/MoonLiveEffect.md`:
- Around line 37-38: Update the system-variable documentation for width, height,
depth, and x, y, z to explicitly state their valid 0..255 range, reflecting the
uint8_t storage and saturation/bypass behavior. Do not present these fields as
unrestricted logical-grid dimensions or light coordinates.
- Around line 60-61: Update the builtin range descriptions in the table for
scale(value, n), sin(angle), and cos(angle) to match their implementation:
document scale as 0..n-1 and the trigonometric results as 1..65535. Leave the
surrounding descriptions unchanged.
In `@docs/moonmodules/light/MoonLiveLayout.md`:
- Around line 14-22: Use valid triple-backtick Markdown fences for both code
examples in docs/moonmodules/light/MoonLiveLayout.md: update the opening and
closing delimiters at lines 14-22 and 33-49, preserving each example’s content.
In `@moondeck/moonlive/emit_xtensa.cpp`:
- Around line 28-29: Update the compileSource invocation in the layout sample to
pass layoutSysVars() instead of modifierSysVars(), while keeping lightBuiltins()
and the existing source, buffer, and size arguments unchanged.
In `@moondeck/run/monitor_esp32.py`:
- Around line 125-131: Update the final serial cleanup around the reconnect flow
in monitor_esp32 so it checks whether ser is not None before calling
ser.close(). Preserve the existing cleanup behavior for an active serial
connection while avoiding AttributeError when interruption occurs during the
ser-is-None reconnect loop.
In `@moonlive/modifiers/shift.mlv`:
- Around line 1-5: Update the shift modifier’s setXYZ call so x + amount cannot
truncate to an in-range uint8_t value; preserve out-of-range coordinates through
the modifier runtime or use its established invalid-coordinate representation,
allowing Layer to drop positions beyond the box instead of wrapping them.
In `@src/core/moonlive/MoonLiveBuiltins.h`:
- Around line 144-147: Update SysVar::add to validate offsets before storing a
variable: accept Arena entries only when their offset is at least kMaxCtrls and
below kArenaBytes, and accept Arg entries only when their offset corresponds to
a defined argument VReg. Return false for invalid values while preserving the
existing capacity and null-name checks.
In `@test/unit/light/unit_MoonLiveScripts.cpp`:
- Around line 116-120: Require successful code generation on supported JIT hosts
by guarding the valid-script assertions with MM_MOONLIVE_HAS_HOST_JIT: in
test/unit/light/unit_MoonLiveScripts.cpp lines 116-120, require r.ok instead of
accepting moonlive::kCodegenFailed; in test/unit/core/unit_moonlive_compiler.cpp
lines 227-233, require ok.ok; and in lines 248-257, require both ok.ok and
seq.ok. Preserve the existing fallback acceptance for hosts without JIT support.
---
Outside diff comments:
In `@docs/MIGRATING.md`:
- Line 55: Update the manual preset migration instructions near the
Layers-to-Effects rename to also change the persisted role value from "layer" to
"effects" in each preset JSON, ensuring migrated presets load with the correct
pad tint.
In `@docs/moonmodules/light/MoonLiveModifier.md`:
- Line 11: Update docs/moonmodules/light/MoonLiveModifier.md:11 to clarify that
the layer invokes the modifier once per physical light/coordinate while the
modifier script itself may contain supported braced for loops. At
docs/moonmodules/light/MoonLiveModifier.md:39, remove for from the unsupported
grammar list; no other syntax documentation needs changing.
In `@src/core/moonlive/MoonLiveIr.h`:
- Around line 87-92: Correct the branch-label capacity around kIrLabels and its
documentation: account for two labels per for construct when determining
supported nesting depth, using localCount if that is the intended limit, or set
kIrLabels to the actual per-program label capacity. Ensure the documented limit
matches the compiler and assembler behavior.
In `@src/light/moonlive/MoonLiveBuiltins_light.h`:
- Around line 24-27: Remove mutable process-global state from both concurrent
built-ins: update mm_light_random16() to use execution-local PRNG state, such as
thread_local or binding-owned context, and update printBudget() to use
binding-owned diagnostic state with synchronized reset and consumption; apply
the changes at src/light/moonlive/MoonLiveBuiltins_light.h lines 24-27 and
111-124.
In `@src/platform/desktop/moonlive_asm_host.h`:
- Around line 59-64: Update the documentation for HostAssembler::call to
describe the three-argument operation as d = fn(a, b, c), while preserving the
existing register-preservation and live-vreg contract details.
---
Duplicate comments:
In `@moondeck/moonlive/emit_xtensa.cpp`:
- Around line 1-20: Remove the direct __XTENSA__ definition and included
platform implementation files from the utility translation unit. Expose the
required lowerToBytes API through the platform target, then link this utility
against that target while retaining only the necessary public declarations and
IR includes.
🪄 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: e23e433f-42f4-4962-aea4-ee3714d935bf
⛔ Files ignored due to path filters (1)
moondeck/build/erase_flash_esp32.pyis excluded by!**/build/**
📒 Files selected for processing (39)
docs/MIGRATING.mddocs/backlog/backlog-light.mddocs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/light/MoonLiveEffect.mddocs/moonmodules/light/MoonLiveLayout.mddocs/moonmodules/light/MoonLiveModifier.mdmoondeck/MoonDeck.mdmoondeck/moonlive/disasm.pymoondeck/moonlive/emit_xtensa.cppmoondeck/run/monitor_esp32.pymoonlive/effects/lines.mlvmoonlive/layouts/grid.mlvmoonlive/layouts/reversed-row.mlvmoonlive/layouts/ring.mlvmoonlive/layouts/two-rows.mlvmoonlive/modifiers/mirror.mlvmoonlive/modifiers/shift.mlvsrc/core/math16.hsrc/core/moonlive/MoonLive.cppsrc/core/moonlive/MoonLive.hsrc/core/moonlive/MoonLiveBuiltins.hsrc/core/moonlive/MoonLiveCompiler.cppsrc/core/moonlive/MoonLiveCompiler.hsrc/core/moonlive/MoonLiveIr.hsrc/light/moonlive/MoonLiveBuiltins_light.hsrc/light/moonlive/MoonLiveEffect.hsrc/light/moonlive/MoonLiveLayout.hsrc/light/moonlive/MoonLiveModifier.hsrc/platform/desktop/moonlive_asm_host.hsrc/platform/esp32/moonlive_asm_riscv.hsrc/platform/esp32/moonlive_asm_xtensa.htest/scenarios/light/scenario_MoonLive_pipeline.jsontest/unit/core/unit_math16.cpptest/unit/core/unit_moonlive_compiler.cpptest/unit/core/unit_moonlive_fill.cpptest/unit/core/unit_moonlive_ir.cpptest/unit/light/unit_MoonLiveLayout.cpptest/unit/light/unit_MoonLiveScripts.cpp
| // Slide along x. A coordinate outside the box is dropped by the Layer, so the pattern walks off | ||
| // the edge rather than wrapping. | ||
| uint8_t amount = 4; // @control 0..64 | ||
|
|
||
| setXYZ(0, x + amount, y, z); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Preserve out-of-range coordinates instead of wrapping them.
For a 255-wide box, x + amount can exceed 255. setXYZ stores the low byte. For example, x = 254 and amount = 4 becomes 2. The Layer then receives an in-range coordinate and cannot drop it.
Preserve overflow state through the modifier runtime, or define a non-wrapping invalid-coordinate representation.
🤖 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 `@moonlive/modifiers/shift.mlv` around lines 1 - 5, Update the shift modifier’s
setXYZ call so x + amount cannot truncate to an in-range uint8_t value; preserve
out-of-range coordinates through the modifier runtime or use its established
invalid-coordinate representation, allowing Layer to drop positions beyond the
box instead of wrapping them.
Three sanitizer jobs went red on push, all at the compile step, all on one file: a test used std::string without including <string>. Apple's clang supplies it transitively and GCC does not, so the build was green on every machine here. `build_desktop.py --gcc` already existed for exactly this and no gate ran it; now one does, in about a second. Performance: desktop 126 us/tick (7936 fps), esp32 2151 us/tick (464 fps). Scripts/MoonDeck - A "GCC build (CI's toolchain)" gate, so a divergence between the two compilers is caught where it is cheap. It reproduces the pushed failure verbatim, which is how it was checked. - The monitor closed its serial port unconditionally on exit, but that handle is None while the reconnect loop waits for a rebooting board - the moment Ctrl+C is most likely. Ending the session printed an AttributeError instead of the log path. Core - SysVarTable::add now refuses an offset the arena cannot hold. Storing one succeeded and then wrote nothing every frame, with no error at any point. - kIrLabels was documented as the nesting depth a script can reach. The counter is program-wide and never reused, so it is the total number of loops (8) whatever their shape; the diagnostic said "nested too deeply" and now says what it means. Light domain - shift.mlv promised a light walks off the edge. A coordinate is a byte, so a large enough shift wraps it back to the left edge instead. The comment now says what happens; making the promise true needs a way for a script to say "out of bounds", which is backlogged. Tests - Two suites accepted "codegen failed" as a pass even on hosts that HAVE an assembler, which would have let a real codegen regression through. Strict where a backend exists, lenient only where there is none. Still green, so the JIT does emit for all of them. - <string> added to the file that broke CI, and to one more found by sweeping - green today only because some other header happened to pull it in. Docs/CI - scale() maps onto 0..n-1 and sin()/cos() return 1..65535; both were documented one wider. The scale range is what ring.mlv depends on. - The modifier page listed `for` as absent from the grammar. It has been there throughout - a modifier gets one coordinate per call, which is a reason not to need a loop, not a reason the language lacks one. - System variables are bytes: the page now says 0..255 and what saturation does. - MIGRATING said nothing else in a preset changes, two lines below documenting that the role does. Reviews - 🐇 CodeRabbit, 14 findings: 9 fixed (above), 1 partly - shift.mlv's comment corrected and the mechanism backlogged, since the proposed fix needs a runtime representation that does not exist yet. 4 skipped: random16 and printBudget hold process-global state, but a torn read of an LCG returns a different random number and a racy budget decrement moves a debug print limit by a few - neither breaks an invariant; emit_xtensa.cpp passes the WIDEST system-variable table on purpose, because it disassembles whatever script it is given, and restructuring that tool's includes is separate work. Flash: esp32 1762368, esp32s3-n16r8 1752080, esp32s31 2024656, desktop 1137800. Tests: 1322 cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
docs/backlog/backlog-light.md (1)
291-293: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftDo not defer synchronous
print()on the render path.Lines 291-293 confirm that
print(v)writes directly to serial while an effect runs intick(). The burst cap limits volume, but it does not make blocking I/O nonblocking. Queue preallocated records and drain them during housekeeping before release.As per path instructions, render/tick code is nonblocking and must not perform blocking I/O.
🤖 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 291 - 293, Update MoonLive’s synchronous print path so EFFECT execution in tick() never writes directly to serial or performs blocking I/O. Queue print records using preallocated storage, then drain the queue from the existing housekeeping/platform-output path before release while preserving the current burst budget and burst-spent behavior.Source: Path instructions
docs/moonmodules/light/MoonLiveModifier.md (1)
13-18: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winFix the Markdown fence around the
setXYZexamples.Line 13 uses ``c and Line 18 uses two backticks. Markdown does not render this example as a fenced C block. Use a three-backtick opener and closer.
Proposed fix
-``c +```c ... -`` +```🤖 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/moonmodules/light/MoonLiveModifier.md` around lines 13 - 18, Correct the Markdown code fence surrounding the setXYZ examples: update the opening and closing delimiters to use three backticks, preserving the C language annotation and all example lines unchanged.docs/moonmodules/light/MoonLiveLayout.md (1)
55-55: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftMake the documented layout clock match the runtime.
Line 55 says a layout reads elapsed milliseconds through
t. TherunScriptmethod insrc/light/moonlive/MoonLiveLayout.hcurrently callsengine_.run(..., 0), so layout scripts receivet == 0for both passes. Either pass one captured elapsed value to both count and placement passes, or documenttas fixed at zero. Otherwise, layouts that usetcannot animate as documented.As per path instructions, documentation must describe the system as it currently exists.
🤖 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/moonmodules/light/MoonLiveLayout.md` at line 55, The documentation claims MoonLive layouts receive elapsed milliseconds via t, but runScript currently supplies 0. Update the MoonLiveLayout documentation to describe t as fixed at zero, unless the runtime is changed to capture and pass one elapsed value to both runScript count and placement passes; ensure the documented behavior matches the implemented system.Source: Path instructions
src/core/moonlive/MoonLiveCompiler.cpp (2)
457-494: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReject trailing tokens in the loop step.
parseExpr()accepts a valid prefix and the parser restores the outer lexer without checking that the next token isTok::RParen. A source such asfor (i = 0; i < 2; i = i + 1 junk) { ... }compiles as ifjunkwere absent.After parsing
stepSrc, require the next token to be the closing parenthesis before emittingIrOp::Mov.🤖 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/MoonLiveCompiler.cpp` around lines 457 - 494, The re-lexed loop-step parser accepts trailing tokens because it does not verify that parsing ended at Tok::RParen. In the stepLex block of the for-loop compilation flow, validate the next token after parseExpr() is Tok::RParen before emitting IrOp::Mov; reject any other token and avoid emitting the step assignment.
479-498: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftPreserve loop values across host calls.
A loop body can call a builtin such as
addLight(i, 0, 0). Thecounterandlimitregisters remain live after that call and are used by Lines 497-498.HostAssembler::callstates that caller-saved registers other thanR0-R2must not be live across a call. The loop counter and bound therefore can be clobbered before the branch executes.Define a cross-call liveness contract for loop values. Spill and restore live loop registers, or make every backend preserve the required virtual registers across
IrOp::Call.🤖 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/MoonLiveCompiler.cpp` around lines 479 - 498, Preserve the loop’s live counter and limit values across builtin calls before the post-body branches in the for-loop compilation path. Update the loop handling around parseStatement(), IrOp::Call generation, or HostAssembler::call to establish a cross-call liveness contract, ensuring counter and limit are restored or retained before the BranchGe and BranchNe instructions.
🤖 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 `@moondeck/event/_gates.py`:
- Line 145: Update the command construction in the event gate to use iterable
unpacking for UV instead of list concatenation, preserving the existing
build_desktop.py, --gcc, and --tests arguments while resolving Ruff RUF005.
---
Outside diff comments:
In `@docs/backlog/backlog-light.md`:
- Around line 291-293: Update MoonLive’s synchronous print path so EFFECT
execution in tick() never writes directly to serial or performs blocking I/O.
Queue print records using preallocated storage, then drain the queue from the
existing housekeeping/platform-output path before release while preserving the
current burst budget and burst-spent behavior.
In `@docs/moonmodules/light/MoonLiveLayout.md`:
- Line 55: The documentation claims MoonLive layouts receive elapsed
milliseconds via t, but runScript currently supplies 0. Update the
MoonLiveLayout documentation to describe t as fixed at zero, unless the runtime
is changed to capture and pass one elapsed value to both runScript count and
placement passes; ensure the documented behavior matches the implemented system.
In `@docs/moonmodules/light/MoonLiveModifier.md`:
- Around line 13-18: Correct the Markdown code fence surrounding the setXYZ
examples: update the opening and closing delimiters to use three backticks,
preserving the C language annotation and all example lines unchanged.
In `@src/core/moonlive/MoonLiveCompiler.cpp`:
- Around line 457-494: The re-lexed loop-step parser accepts trailing tokens
because it does not verify that parsing ended at Tok::RParen. In the stepLex
block of the for-loop compilation flow, validate the next token after
parseExpr() is Tok::RParen before emitting IrOp::Mov; reject any other token and
avoid emitting the step assignment.
- Around line 479-498: Preserve the loop’s live counter and limit values across
builtin calls before the post-body branches in the for-loop compilation path.
Update the loop handling around parseStatement(), IrOp::Call generation, or
HostAssembler::call to establish a cross-call liveness contract, ensuring
counter and limit are restored or retained before the BranchGe and BranchNe
instructions.
🪄 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: f3f72725-afee-4259-b840-396454c22b2b
📒 Files selected for processing (19)
docs/MIGRATING.mddocs/backlog/backlog-light.mddocs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/light/MoonLiveEffect.mddocs/moonmodules/light/MoonLiveLayout.mddocs/moonmodules/light/MoonLiveModifier.mdmoondeck/event/_gates.pymoondeck/moonlive/emit_xtensa.cppmoondeck/run/monitor_esp32.pymoonlive/modifiers/shift.mlvsrc/core/moonlive/MoonLiveBuiltins.hsrc/core/moonlive/MoonLiveCompiler.cppsrc/core/moonlive/MoonLiveIr.hsrc/platform/desktop/moonlive_asm_host.htest/scenarios/light/scenario_GridBlacks_blackpixel.jsontest/unit/core/unit_FilesystemModule_persistence.cpptest/unit/core/unit_moonlive_compiler.cpptest/unit/light/unit_MoonLiveScripts.cpp
| # -Werror those are hard failures discovered only after a push. Compiling with the real | ||
| # thing answers it here — see build_desktop.py --gcc for the four cycles that cost once. | ||
| Gate("GCC build (CI's toolchain)", | ||
| UV + ["moondeck/build/build_desktop.py", "--gcc", "--tests"], |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Use iterable unpacking for UV.
Ruff RUF005 flags UV + [...] on Line 145. Replace it with [*UV, "moondeck/build/build_desktop.py", "--gcc", "--tests"] to avoid list concatenation and clear the lint warning.
🧰 Tools
🪛 Ruff (0.16.1)
[warning] 145-145: Consider iterable unpacking instead of concatenation
Replace with iterable unpacking
(RUF005)
🤖 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/event/_gates.py` at line 145, Update the command construction in the
event gate to use iterable unpacking for UV instead of list concatenation,
preserving the existing build_desktop.py, --gcc, and --tests arguments while
resolving Ruff RUF005.
Source: Linters/SAST tools
A for loop emitted code that tested and advanced its own counter whatever the condition and step clauses actually named, so `for (i = 0; j < 3; i = i + 1)` compiled and ran as if it said `i` - and trailing junk after the step was dropped silently. Both are the worst kind of bug for a live script: a wrong fixture with no error anywhere to explain it. Performance: desktop 122 us/tick (8196 fps), esp32 2151 us/tick (464 fps). Core - Both for clauses now verify they name the loop variable, which is what their error strings already claimed. The realistic case is a nested loop whose inner condition tests the outer variable. - The step is re-lexed from source that was skipped over, and an expression parser stops at the first token it cannot use, so anything after it vanished. It must now end at the ')'. - MoonLiveBuiltins_light.h used kArg3 without including the header that defines it. It compiled only because every consumer includes MoonLiveIr.h first; standalone it failed. Swept every non-ESP32 header for the same defect - the two that remain (Layer.h, EffectBase.h) fail from a pre-existing mutual include, left alone. Light domain - MoonLiveLayout's compiled_ was sized by a literal where the other two bindings use sizeof(source_). It matches today; raising kMaxScriptBytes would have made this the one binding whose rebuild-comparison truncates and rebuilds every frame. Scripts/MoonDeck - The Layers->Effects rename never reached MoonDeck. run_live_scenario.py's snapshot set still named "Layers", so a live scenario could not capture or restore the Effects subtree - it would leave a bench board with a destroyed config after a clear_children. Five files. - A test asserted codegen success without gating on MM_MOONLIVE_HAS_HOST_JIT, so it failed every CI sanitizer job: x86_64 has no MoonLive backend. It pins register reuse, which is front-end behaviour, but proving it needs emitted code. Swept the test tree - it was the only unguarded one of sixteen. Tests - The for-clause checks and the trailing-token rejection are pinned, including a sequential reuse case, since a name legitimately repeats once a loop closes. - A loop counter is now shown to survive a call in its body by running the loop and counting the lights, rather than by arguing about which registers are caller-saved. Docs/CI - The host assembler said only R0..R2 may be live across a call; it saves the whole vreg pool, which is why loop counters survive. The comment is what made a reviewer read a bug into working code. Its kArg3 note also still said the grammar has no `t`. - A layout's `t` is always 0: the script runs twice per rebuild and must agree with itself, so it gets a fixed clock. The page claimed it received elapsed milliseconds. - MoonLiveEffect.md described the grammar as a single call statement (`for` has been there throughout) and claimed three builtins where thirteen are registered. - moonlive/README.md documented a drivers/ folder git cannot ship - empty, so untracked. Reviews - 👾 Reviewer over the branch diff, 10 findings: 8 fixed (above), 1 already done, 1 skipped - collapsing the three bindings' defineControls into a mixin, which the Reviewer flagged as a judgment call and which coding-standards tolerates for header-only light modules. It found no fault with the system-variable construct, the arena split, the heap-overrun fixes, or the hot path, and fuzzed the compiler under ASan+UBSan without a crash. - 🐇 CodeRabbit, 7 findings: 3 fixed, 4 skipped - two were already fixed earlier this session; RUF005 is not an enabled rule here (ruff passes, and all seven gates share the idiom); the print-queue request is already backlogged with that exact design. Its claim that a loop counter is clobbered across a call is wrong - the test above demonstrates it. Flash: esp32 1762368, esp32s3-n16r8 1752384, esp32s31 2024656, desktop 1137800. Tests: 1324 cases. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
src/light/moonlive/MoonLiveLayout.h (1)
110-120: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy liftSerialize compilation and execution of the shared engine.
lightCount()andforEachCoord()both callcompile(). The runtime can invoke layout walks from the HTTP task and the render task concurrently. A first compile or source update then races onsource_,compiled_, andengine_. One task can replace generated code while another task executes it.Compile on a housekeeping path and publish an immutable program with a safe lifetime. Do not mutate
engine_from concurrent layout walks.🤖 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/MoonLiveLayout.h` around lines 110 - 120, Refactor MoonLiveLayout::compile and the lightCount/forEachCoord execution path so layout walks never concurrently mutate or execute the shared engine_. Perform compilation during a serialized housekeeping/update path, then publish an immutable compiled program with lifetime protection for readers; have concurrent walks use that published program instead of source_, compiled_, or engine_ directly. Preserve status and dynamic-byte updates during compilation while eliminating races on all shared compilation state.src/core/moonlive/MoonLiveCompiler.cpp (1)
456-458: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy liftBound loop work at runtime.
A loop bound can be dynamic. Four nested loops with controls set to
255execute more than four billion body iterations, even when every step advances. AMoonLiveEffectruns this code fromtick(), so a valid script can block rendering for an unbounded time.Add a shared generated iteration budget that exits the script when exhausted. Apply it to every loop level.
🤖 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/MoonLiveCompiler.cpp` around lines 456 - 458, In MoonLiveCompiler’s loop code generation around the limit expression, add one shared runtime iteration budget for the entire script execution, initialize it per run, and decrement/check it on every iteration of every loop level. Exit the script cleanly when the budget is exhausted, ensuring nested and dynamically bounded loops cannot run unbounded work.src/light/moonlive/MoonLiveBuiltins_light.h (2)
97-128: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftMake the print budget per execution context.
printBudget()returns one process-global mutable counter.resetPrintBudget()writes it during compilation or preparation whilemm_light_print()decrements it during execution. The suppliedsrc/light/moonlive/MoonLiveLayout.hcontext allows those paths to run on different tasks. This creates a data race and allows one module to reset another module's budget. Store the budget with the module or run context, or use synchronized thread-local state with an explicit contract.🤖 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/MoonLiveBuiltins_light.h` around lines 97 - 128, Make the print budget execution-context-local instead of process-global: update printBudget(), resetPrintBudget(), and mm_light_print() to access the owning module or MoonLiveLayout run context, or an explicitly synchronized thread-local context. Ensure each context can reset and decrement only its own budget without data races or affecting other modules.
25-29: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftDo not share RNG state across concurrent script runs.
mm_light_random16mutates one function-local staticswithout synchronization. The suppliedsrc/light/moonlive/MoonLiveLayout.hcontext allows scripted layout runs from HTTP and render tasks, so simultaneous scripts can race ons. Keep RNG state per engine or run, or use documented thread-local state if cross-thread sequence sharing is not required.🤖 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/MoonLiveBuiltins_light.h` around lines 25 - 29, The mm_light_random16 function currently shares mutable static RNG state across concurrent script runs. Replace the function-local static s with RNG state scoped to the engine or individual run, or use documented thread-local storage when sequence sharing across threads is unnecessary, while preserving the existing random16 calculation and zero-input behavior.
♻️ Duplicate comments (1)
src/light/moonlive/MoonLiveBuiltins_light.h (1)
97-128: 🩺 Stability & Availability | 🟠 MajorRemove synchronous output from script execution.
This remains unresolved from the previous review.
mm_light_printcallsstd::printf, andMoonLiveEffect::tick()can execute it throughengine_.run(). The call can block the render path and violates the nonblocking hot-path contract. Queue bounded diagnostic records and drain them from a housekeeping path.As per path instructions: render/tick code must be nonblocking and must not allocate, block, or perform slow I/O.
🤖 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/MoonLiveBuiltins_light.h` around lines 97 - 128, Replace synchronous std::printf calls in mm_light_print with bounded, nonblocking diagnostic record queuing, preserving the print budget and returned value. Add or reuse a housekeeping-path drain to perform serial output outside MoonLiveEffect::tick() and engine_.run(), ensuring the render path performs no allocation, blocking, or slow I/O.Source: Path instructions
🤖 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/moonmodules/light/MoonLiveEffect.md`:
- Line 80: Update the nearby MoonLive and MoonLiveEffect API bullets to document
compile(source, table, sysvars) and state that bindings provide effectSysVars(),
modifierSysVars(), or layoutSysVars(). Keep the new system-variable section
consistent with this current API description.
In `@test/unit/core/unit_moonlive_fill.cpp`:
- Around line 150-168: Add a host-JIT test alongside “a loop counter survives a
call in the body” that passes the supplied t value into random16, then uses t in
a later setRGB argument after the generated call. Run the program with a known t
value and assert the output contains that value, ensuring t survives across the
call.
---
Outside diff comments:
In `@src/core/moonlive/MoonLiveCompiler.cpp`:
- Around line 456-458: In MoonLiveCompiler’s loop code generation around the
limit expression, add one shared runtime iteration budget for the entire script
execution, initialize it per run, and decrement/check it on every iteration of
every loop level. Exit the script cleanly when the budget is exhausted, ensuring
nested and dynamically bounded loops cannot run unbounded work.
In `@src/light/moonlive/MoonLiveBuiltins_light.h`:
- Around line 97-128: Make the print budget execution-context-local instead of
process-global: update printBudget(), resetPrintBudget(), and mm_light_print()
to access the owning module or MoonLiveLayout run context, or an explicitly
synchronized thread-local context. Ensure each context can reset and decrement
only its own budget without data races or affecting other modules.
- Around line 25-29: The mm_light_random16 function currently shares mutable
static RNG state across concurrent script runs. Replace the function-local
static s with RNG state scoped to the engine or individual run, or use
documented thread-local storage when sequence sharing across threads is
unnecessary, while preserving the existing random16 calculation and zero-input
behavior.
In `@src/light/moonlive/MoonLiveLayout.h`:
- Around line 110-120: Refactor MoonLiveLayout::compile and the
lightCount/forEachCoord execution path so layout walks never concurrently mutate
or execute the shared engine_. Perform compilation during a serialized
housekeeping/update path, then publish an immutable compiled program with
lifetime protection for readers; have concurrent walks use that published
program instead of source_, compiled_, or engine_ directly. Preserve status and
dynamic-byte updates during compilation while eliminating races on all shared
compilation state.
---
Duplicate comments:
In `@src/light/moonlive/MoonLiveBuiltins_light.h`:
- Around line 97-128: Replace synchronous std::printf calls in mm_light_print
with bounded, nonblocking diagnostic record queuing, preserving the print budget
and returned value. Add or reuse a housekeeping-path drain to perform serial
output outside MoonLiveEffect::tick() and engine_.run(), ensuring the render
path performs no allocation, blocking, or slow I/O.
🪄 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: c87af99a-3632-4e6a-b8d0-8adea569b6b6
📒 Files selected for processing (19)
docs/metrics/repo-health.jsondocs/metrics/repo-health.mddocs/moonmodules/light/MoonLiveEffect.mddocs/moonmodules/light/MoonLiveLayout.mddocs/moonmodules/light/MoonLiveModifier.mdmoondeck/check/check_specs.pymoondeck/docs/screenshot_modules.pymoondeck/docs/update_module_docs.pymoondeck/scenario/run_live_scenario.pymoonlive/README.mdsrc/core/moonlive/MoonLiveCompiler.cppsrc/light/drivers/Drivers.hsrc/light/moonlive/MoonLiveBuiltins_light.hsrc/light/moonlive/MoonLiveLayout.hsrc/platform/desktop/moonlive_asm_host.cppsrc/platform/desktop/moonlive_asm_host.htest/unit/core/unit_moonlive_compiler.cpptest/unit/core/unit_moonlive_fill.cpptest/unit/light/unit_MoonLiveScripts.cpp
💤 Files with no reviewable changes (1)
- moonlive/README.md
| - **`MoonLiveBuiltins`** (`src/core/moonlive/MoonLiveBuiltins.h`) — the **neutral host-binding seam**: a `BuiltinTable` of `{name → descriptor}`, where a descriptor is either `Call` (a host C function pointer — a pure helper like `random16`) or `Inline` (a neutral opcode tag the backend emits inline — the hot-path buffer writers, no per-pixel call). The core owns no function names; it resolves a call against whatever the host registered. | ||
| - **`MoonLiveCompiler`** (`src/core/moonlive/MoonLiveCompiler.h/.cpp`) — the **platform-independent front-end**: a recursive-descent lexer + expression parser that lowers each statement to the typed IR (`MoonLiveIr.h`). Pure (source + table in, IR out, deterministic). Knows the *language*, never an ISA and never a domain. | ||
| - **`MoonLiveBuiltins_light`** (`src/light/moonlive/MoonLiveBuiltins_light.h`) — the **light-domain registration**: the only place the LED vocabulary lives. Registers `setRGB`/`fill` (Inline, lowering to RGB stores) and `random16` (Call). A different host (display, sensor) writes its own table; the core is unchanged. | ||
| - **`MoonLiveBuiltins_light`** (`src/light/moonlive/MoonLiveBuiltins_light.h`) — the **light-domain registration**: the only place the LED vocabulary lives. Registers the whole vocabulary above — Inline ops lowering to stores, and Calls into host helpers — plus the system variables each binding supplies. A different host (display, sensor) writes its own table; the core is unchanged. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the API bullets for system variables.
The new system-variable section is correct, but the nearby MoonLive and MoonLiveEffect bullets still describe compile(source, table) and say that MoonLiveEffect passes only the builtin table. src/core/moonlive/MoonLive.h requires compile(source, table, sysvars), and the bindings pass effectSysVars(), modifierSysVars(), or layoutSysVars(). Update those bullets so this specification matches the current API.
As per coding guidelines: Documentation must describe the system as it currently exists.
🤖 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/moonmodules/light/MoonLiveEffect.md` at line 80, Update the nearby
MoonLive and MoonLiveEffect API bullets to document compile(source, table,
sysvars) and state that bindings provide effectSysVars(), modifierSysVars(), or
layoutSysVars(). Keep the new system-variable section consistent with this
current API description.
Source: Coding guidelines
| // A loop counter and its limit are live ACROSS a call whenever the body calls anything — which is | ||
| // most real effects. The assembler's contract says it preserves what has to survive; this runs the | ||
| // loop and counts, so a backend that clobbered either would show up as a short or runaway loop | ||
| // rather than as an argument about which registers are caller-saved. | ||
| #if MM_MOONLIVE_HAS_HOST_JIT | ||
| TEST_CASE("a loop counter survives a call in the body") { | ||
| moonlive::MoonLive eng; | ||
| // random16 is a Call; `i` and the limit `w` are both live around it. | ||
| REQUIRE(eng.compile("uint8_t w = 8;\nfor (i = 0; i < w; i = i + 1) { setRGB(i, random16(200), 200, 0); }", | ||
| kCtrlTable, kSys)); | ||
| uint8_t buf[8 * 3] = {}; | ||
| eng.run(buf, 8, 3, 0); | ||
| int written = 0; | ||
| for (int i = 0; i < 8; i++) | ||
| if (buf[i * 3] || buf[i * 3 + 1] || buf[i * 3 + 2]) written++; | ||
| CHECK(written == 8); // every iteration ran: the counter was not clobbered by the call | ||
| eng.free(); | ||
| } | ||
| #endif |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win
Exercise t across the generated call.
This test keeps i and w live across random16, but it never reads t after that call. It cannot detect a regression in the x3-backed t preservation changed in src/platform/desktop/moonlive_asm_host.cpp. Add a case that passes t to random16, uses t in a later argument, and asserts that the output contains the supplied t.
🤖 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/unit/core/unit_moonlive_fill.cpp` around lines 150 - 168, Add a host-JIT
test alongside “a loop counter survives a call in the body” that passes the
supplied t value into random16, then uses t in a later setRGB argument after the
generated call. Run the program with a known t value and assert the output
contains that value, ensuring t survives across the call.
Scripted effects, modifiers and layouts you can write on a running device, and the container rename that landed alongside them.
MoonLive: write a script, see it on the wall
A
sourcecontrol holds text; the engine compiles it to native machine code and calls it each tick through a function pointer. Not an interpreter — a compiler with three backends (Xtensa, RISC-V, arm64) behind one IR, so a script runs at hand-written speed on the render path.Three bindings ship: a scripted effect (colors), modifier (where one light lands), and layout (where the lights physically are). Eleven example scripts live in
moonlive/, each compiled by the test suite so a language change that breaks one is caught.A
uint8_t x = 4; // @control 0..64line becomes a real slider, bound to a live arena byte the running code reads — moving it does not recompile.System variables, not hidden declarations
The values a script is given —
t, the layer'swidth/height/depth, and thex/y/za modifier folds — are a first-class construct: the host registers them, the compiler resolves them before locals and controls, and a script cannot declare a name that shadows one.They used to be faked. Each binding pasted
uint8_t width = 0;declarations on top of the user's script and hid them from the UI by index, which meant an effect could declare its ownwidthand silently disagree with the layer it was drawing into — set 16 on an 8×8 panel and it drew off the edge.Each binding supplies only what it writes. That is what keeps
xandyusable as ordinary loop counters in a layout while they mean "the light being folded" in a modifier, and what turns a layout readingwidthinto a named error rather than a silent zero that places no lights.Layers→EffectsThe three top-level containers are now Layouts, Effects, Drivers — L.E.D. The old name sat one character from its own child (
LayersholdingLayers) and read as a near-twin ofLayouts. The tree keeps its shape; the childLayeris untouched.Breaking
Two, both in MIGRATING.md with the action to take:
width/heightno longer compiles and its fixture goes dark until edited (the shippedgrid.mlvusescols/rows).Layerscontainer rename costs the saved light tree and preset role tints; both are one file edit on the device, or a rebuild in the UI.Bugs this branch fixed
fortypo compiled clean.for (i = 0; j < 3; i = i + 1)emitted code testingiwhatever the clauses named, and trailing junk after the step was dropped. A wrong fixture with no diagnostic — the hardest kind to trace.-1materialised as 65535 on two backends, and nested loops refused on Xtensa for scratch that a program without inline ops never needed.controlSlot()was bounded by the control count, so every system-variable write hit a null pointer and vanished.run_live_scenario.pycould not restore the Effects subtree, so a live scenario would leave a bench board with a destroyed config.Verification
#include <string>that every local clang build supplied transitively, which is why a GCC gate (CI's toolchain) now runs on every commit.Known limits
lines.mlvdoes not compile on Xtensa —fillplus two loops exceeds the S3 register budget. It runs on S31 and desktop; every other shipped script compiles on all three. The fix is spilling to the stack, which is its own branch.