Contributing
July 22, 2026 ยท View on GitHub
This file is for contributor-facing notes. The top-level README should stay focused on installation, usage, and the language from a user's point of view.
For embedded-specific architecture notes, see embedded/readme.md.
JS core principles
The JavaScript side of Krill is organized as a pipeline:
- user input is parsed into a normalized model
- that model is turned into a rendering tree
- the rendering tree is rendered over time into pattern events
- playback code emits those events to a device
The important boundary is between the parsed model and the runtime tree. Syntax work belongs in the parser and evaluator. Musical behavior belongs in operators, patterns, and playback structures. Try not to blur those layers.
Main flow
The main server entry point is main.js. It creates the web server and forwards /command input to js/application.js.
js/application.js is the integration point for the JS runtime:
- js/input-evaluator.js parses source text using grammar.txt
- js/renderer/render-tree.js turns the parsed model into render nodes and pattern nodes
- js/playback/engine.js schedules playback in cycle time
- js/playback/rendering-tree-player.js advances the current tree and returns the next event to emit
- js/playback/playback-device.js converts event values into MIDI output
If you are changing behavior, find the layer that actually owns that behavior before editing. Avoid putting feature logic into js/application.js unless the change is truly about application wiring.
Parser and evaluator principles
The parser side is intentionally small:
- grammar.txt defines the accepted language
- js/input-evaluator.js builds the PEG parser and removes empty parse fields
When adding syntax, keep these rules in mind:
- If the language accepts a new form, start in grammar.txt
- Keep the output model explicit and stable so the renderer can consume it without parser-specific assumptions
- Prefer extending existing node shapes when the concept already exists
- Avoid pushing execution details into the parse model if they belong in operator behavior
The JS parser output is also the bridge toward the embedded implementation, so model changes should be made deliberately.
Rendering tree and operator principles
js/renderer/render-tree.js is the bridge between parsed data and runtime behavior. It recursively walks parsed nodes and dispatches them to render-node constructors.
The render-node framework lives in js/renderer/nodes/render-nodes.js. Render nodes follow a simple contract:
- they receive arguments that can be rendered
tick()advances stateful childrenrender()returns a pattern-like result for the current cycle
Most feature work on the JS side should happen here:
- new transformations usually belong in a new file under js/renderer/nodes/
- new pattern composition behavior should reuse the pattern utilities in js/patterns/
- render-node dispatch should be wired in js/renderer/render-tree.js
Representative files:
- js/renderer/nodes/add-render-node.js shows a binary render node using pattern weaving
- js/renderer/nodes/weighted-pattern-render-node.js shows how weighted steps and sequence rendering are built
- js/patterns/pattern.js defines the core pattern data structure and timing helpers
- js/patterns/weaving.js is the place to look for pattern-combination behavior
Playback principles
Playback should stay generic.
- js/playback/engine.js owns scheduling and tempo
- js/playback/rendering-tree-player.js owns cycle-to-cycle rendering and event lookup
- js/playback/playback-device.js owns device output
If a feature changes musical meaning, it usually belongs in the parser or operator layer, not in the playback loop. Try not to add feature-specific branches to the engine or tree player unless the feature is fundamentally about scheduling.
Guidelines for new features
Use this rule of thumb:
- New syntax or notation: update grammar.txt and verify the parsed model through js/input-evaluator.js
- New render node: add a render-node file under js/renderer/nodes/, wire it in js/renderer/render-tree.js, and add tests
- New pattern behavior: extend js/patterns/pattern.js or related pattern utilities rather than special-casing callers
- New playback or timing behavior: start in js/playback/rendering-tree-player.js or js/playback/engine.js, but only if the feature is truly about scheduling
Typical render-node work usually touches four places:
- grammar.txt if the syntax is new
- a new or updated file in js/renderer/nodes/
- render-node dispatch in js/renderer/render-tree.js
- tests in tests/
Tests
There is no useful npm test script at the moment. Use the test files directly.
Useful entry points:
- tests/base.js provides shared evaluator helpers
- tests/test-evaluator.js covers parser and model behavior
- tests/test-render-node.js covers render-node contracts
- tests/test-pattern.js covers pattern-level behavior
- tests/test-weaving.js covers pattern combination behavior
- tests/test-sequence-player.js covers playback timing
- tests/test-run-cases.js runs the shared musical cases from tests/test-cases.json
For changes that affect user-visible musical behavior, prefer adding or updating an end-to-end case in tests/test-cases.json in addition to narrower unit coverage.
Where to look first
- main.js for server and request flow
- js/application.js for top-level JS wiring
- grammar.txt for syntax ownership
- js/input-evaluator.js for parse entry
- js/renderer/render-tree.js for parsed-model to runtime dispatch
- js/renderer/nodes/render-nodes.js for render-node contract
- js/patterns/pattern.js for core pattern behavior
- js/playback/rendering-tree-player.js for event scheduling within a cycle
- tests/test-run-cases.js for end-to-end JS behavior
If a change spans multiple layers, the usual order is: syntax, model shape, operator or pattern behavior, playback implications, tests.