feat: adopt wave into the language, with a generative harmonics form - #16
Conversation
`wave <name> <32 hex nibbles>` was never part of the grammar. It was a host-registered top-level statement, absent from DECK_GRAMMAR.md and from Highlight's TOP_KEYS, and every host that needed it registered its own copy — tish-gba (which it came from), and deck-player. Parser.tish already named the consequence: the hook exists because, without it, "the GBA implementation ended up a separate grammar rather than a subset". A statement every host implements is a language feature living in the wrong place. The corpus that exists to stop that drift never saw a `wave` line, since conformance parses with no host registrations. So `wave` is core now, and profiles.json no longer carries it as a GBA extension. On top of that, the hex is a poor way to say what a table is: timbre is harmonic content, and the digits had to be computed somewhere else and pasted in. `wave <name> harmonics <a1> …` takes amplitudes — fundamental first — and sums them into the same 32 levels, normalized so only the ratios matter. Expanded at parse time, exactly as `steps euclid` is, and for the same reason: a host reads `levels` and never learns which spelling produced it. The source form is kept alongside so an emitter can write `harmonics` back rather than flattening it to hex. 013-wave pins that the two spellings resolve to identical levels, in JS, in the Tish VM, and in the generated Rust crate. The clamps in the additive path are gone rather than covered: normalizing by the peak lands every sample in 0..15 by construction. What is guarded instead is the case that was actually reachable — a non-finite amplitude, which would have put NaN in the table.
The examples page was eight blocks, every one of them a Game Boy chip song, and not one of them used a `wave` table — so the page never showed the feature this branch just adopted, and read as if the language were GBA-only. Five sections added: wavetables (both spellings, side by side, so you can hear they are one sound), designing a timbre from harmonics, instrument voices, and `basicOsc` for when you want the language without the console. Plus mix and fx, which no example demonstrated at all despite the channel strip applying to every generator. Everything stays inside the three generators this player synthesizes faithfully. Deckard's other thirty parse fine and then get substituted for a plain oscillator, which on a page where every block has a Play button would be a demo that lies about what you are hearing. The intro now says so. test/examples.mjs is what keeps that true: it parses every block on the page and fails on a substitution or an ignored feature, not just on a parse error.
|
Pushed two follow-ups: Fixed the conformance case. Expanded the examples page ( Everything stays within
|
…#17) The Rust crate job has been posting four `##[error]` annotations on green runs: ENOENT: opendir '…/crate/target/package/deckfile-0.1.0/tests/trybuild' ENOENT: opendir '…/crate/target/package/deckfile-0.1.0/tests/target' They come from `Post Cache cargo`, not from any build step. `cargo publish --dry-run` leaves an extracted copy of the crate under `target/package/`, and that copy has a `tests/` directory (holding `conformance.rs`). rust-cache prunes `target/` before saving it, sees `tests/`, and goes looking for the trybuild layout — `tests/target` and `tests/trybuild` — which this crate has never had. It only surfaces on a cache MISS, because rust-cache skips the prune entirely on a hit. That is why it looks sporadic: it appeared on #14, not on #15, and again on #16, each time a change to the emitted crate moved the cache key. Nothing should be caching the output of a dry-run publish, so remove it once the check has served its purpose. The check itself is unchanged. Worth fixing rather than muting: a green run that reports four errors teaches everyone to skim past annotations, which is when a real one gets missed.
* feat: read, play and emit named wavetables Deckard could not read a `wave` line. The statement originated in tish-gba and deck-player added its own copy, but the reference host never had one — so a `.deck` with a named table loaded with "unexpected top-level: wave", and the DMG wave channel offered three hardcoded shapes and nothing else. `wave` is core grammar now (spacedevin/deck#16), which resolves both the hex and `harmonics` spellings to 32 levels before Deckard sees them. What was missing here is the rest of the path: - the project model holds the tables, keyed by name - Apply collects them and points each channel at the table its `wave_shape` names; a UI knob turn arrives as a one-track `.deck` fragment through the same path, so the binding stays current when the shape is changed by hand - the DMG wave channel plays a named table in preference to a built-in shape, which is the order the GBA bake uses - the Wave selector lists the song's tables instead of only Saw/Square/Sine Emit writes the tables back, and writes a `harmonics` line AS harmonics rather than flattening it to 32 digits. That is the whole reason the parser keeps the source spelling: a save should not throw away what the author wrote. Only tables a channel actually names are emitted, or a project would grow a dead `wave` line every time it loaded a file. * chore(deps): @spacedevin/deck 1.5.3 The released language package, where `wave` is core grammar — which is what makes this branch work against a real install rather than a linked build. All 12 wavetable tests pass against the published package, and the existing suites are unchanged.
…20) * feat: read `wave` from the language, and accept the `harmonics` form `wave <name> <32 hex nibbles>` originated here, as a fork of the parser, and survived as a registered dialect extension after the fork was undone. It was never GBA-specific though — deck-player registered its own copy too — so it has been adopted into the language proper (spacedevin/deck#16). That deletes the hex validation and nibble unpacking from this crate. The parser resolves both spellings to 32 levels, and all that is left here is packing two levels per byte for wave RAM. The additive sum lands at bake time, which is the only place it can: the device is no_std on ARM7TDMI with no FPU and no libm, so there is no sin() on the far side. Nothing about the ROM changes — still 16 bytes per table, copied verbatim into WAVE_RAM, so a harmonics table costs exactly what a hex one did. The corpus digest is unchanged across all 54 decks, and a new test pins the part that would be invisible otherwise: a harmonics table and the hex literal it resolves to pack to byte-identical wave RAM. Blocked on the deckfile 1.5.0 release; the pin is bumped ahead of it, so CI will not go green until that publishes. * chore(deps): pin deckfile 1.5.3 The released crate, in place of the version pinned ahead of it. `wave` is core grammar there, so this is what actually makes the branch build. All 11 tests pass against the published crate, and the corpus digest across all 54 decks is byte-identical to the pre-change baseline. * style: satisfy rustfmt and clippy in the wave bake CI runs `cargo fmt --check` on every crate and `cargo clippy --all-targets -D warnings` on this one; the wave changes passed neither. Formatting was mechanical. Clippy found two needless range loops and one real problem: `assert!(b >> 4 <= 15 && (b & 0x0f) <= 15)` is always true, because a nibble of a u8 cannot exceed 15. The assertion proved nothing. Replaced with the invariant actually worth pinning — that packing is lossless and high-nibble-first. Getting that order wrong still yields 16 plausible bytes, so nothing downstream would catch it; the ROM would just play a scrambled waveform. Tests still 11/11 and the corpus digest is unchanged across all 54 decks.
* feat: read, play and emit named wavetables Deckard could not read a `wave` line. The statement originated in tish-gba and deck-player added its own copy, but the reference host never had one — so a `.deck` with a named table loaded with "unexpected top-level: wave", and the DMG wave channel offered three hardcoded shapes and nothing else. `wave` is core grammar now (spacedevin/deck#16), which resolves both the hex and `harmonics` spellings to 32 levels before Deckard sees them. What was missing here is the rest of the path: - the project model holds the tables, keyed by name - Apply collects them and points each channel at the table its `wave_shape` names; a UI knob turn arrives as a one-track `.deck` fragment through the same path, so the binding stays current when the shape is changed by hand - the DMG wave channel plays a named table in preference to a built-in shape, which is the order the GBA bake uses - the Wave selector lists the song's tables instead of only Saw/Square/Sine Emit writes the tables back, and writes a `harmonics` line AS harmonics rather than flattening it to 32 digits. That is the whole reason the parser keeps the source spelling: a save should not throw away what the author wrote. Only tables a channel actually names are emitted, or a project would grow a dead `wave` line every time it loaded a file. * chore(deps): @spacedevin/deck 1.5.3 The released language package, where `wave` is core grammar — which is what makes this branch work against a real install rather than a linked build. All 12 wavetable tests pass against the published package, and the existing suites are unchanged.
… form (#27) * feat: read `wave` from the language, and accept the `harmonics` form `wave <name> <32 hex nibbles>` originated here, as a fork of the parser, and survived as a registered dialect extension after the fork was undone. It was never GBA-specific though — deck-player registered its own copy too — so it has been adopted into the language proper (spacedevin/deck#16). That deletes the hex validation and nibble unpacking from this crate. The parser resolves both spellings to 32 levels, and all that is left here is packing two levels per byte for wave RAM. The additive sum lands at bake time, which is the only place it can: the device is no_std on ARM7TDMI with no FPU and no libm, so there is no sin() on the far side. Nothing about the ROM changes — still 16 bytes per table, copied verbatim into WAVE_RAM, so a harmonics table costs exactly what a hex one did. The corpus digest is unchanged across all 54 decks, and a new test pins the part that would be invisible otherwise: a harmonics table and the hex literal it resolves to pack to byte-identical wave RAM. Blocked on the deckfile 1.5.0 release; the pin is bumped ahead of it, so CI will not go green until that publishes. * chore(deps): pin deckfile 1.5.3 The released crate, in place of the version pinned ahead of it. `wave` is core grammar there, so this is what actually makes the branch build. All 11 tests pass against the published crate, and the corpus digest across all 54 decks is byte-identical to the pre-change baseline. * style: satisfy rustfmt and clippy in the wave bake CI runs `cargo fmt --check` on every crate and `cargo clippy --all-targets -D warnings` on this one; the wave changes passed neither. Formatting was mechanical. Clippy found two needless range loops and one real problem: `assert!(b >> 4 <= 15 && (b & 0x0f) <= 15)` is always true, because a nibble of a u8 cannot exceed 15. The assertion proved nothing. Replaced with the invariant actually worth pinning — that packing is lossless and high-nibble-first. Getting that order wrong still yields 16 plausible bytes, so nothing downstream would catch it; the ROM would just play a scrambled waveform. Tests still 11/11 and the corpus digest is unchanged across all 54 decks.
wave <name> <32 hex nibbles>was never part of the grammar. It was a host-registered top-level statement — missing fromDECK_GRAMMAR.mdand fromHighlight.tish'sTOP_KEYS— so every host that wanted it registered its own copy: tish-gba, where it originated, and deck-player. Deckard never implemented it at all.src/deckfile/Parser.tishalready named the consequence:A statement every host implements is a language feature in the wrong place. And the corpus that exists to stop exactly this drift never saw a
waveline: conformance parses with no host registrations, sohostStatementsis{}in every expected file, whileprofiles.jsoncarriedwaveundergba.extensions.topLevel.What changed
waveis core. Parsed inParser.tishbefore the host dispatch, so a host registeringwaveis now shadowed rather than authoritative (there is a test for that). Removed from the GBA profile's extension list.New generative form. The hex is a poor way to say what a table is — timbre is harmonic content, so the digits had to be computed elsewhere and pasted in:
Those two lines are the same sound.
a1is the fundamental,a2the octave above it,a3the twelfth; the result is normalized to fill the 4-bit range, so only the ratios matter.Expanded at parse time, exactly as
steps euclidis — and for the same reason. A host readslevelsand never learns which spelling produced it, so no host needs changing to keep working. The source form is kept alongside (mode,harmonics,hex) so an emitter can writeharmonicsback instead of flattening it to hex, which is the trapeuclidavoids by retaininghits/len.Notes for review
"waves": []line each, nothing else moved.docs/DECK_EXTENSION.mdusedwaveas its worked example of the host-statement hook. That would now tell hosts to register something the language owns, so the example is acuestatement instead.Verification
npm test— build, 100% line/function/statement coverage, 13 conformance cases, Tish VM suite, JS smoke.npm run test:rust— the regenerated crate passes the same corpus, plus a new facade test asserting the two spellings produce identical levels.packages/player— 15 tests pass, including a new one asserting aharmonicstable reaches the audio graph identically to its hex literal.harmonics, so existing documented tables are unchanged.Downstream PRs follow in
schlopai/chuggie(bake),spacedevin/deckard(first-ever wave support), andschlopai/chuggie.dev(docs). This must merge and release first — the others depend on the publisheddeckfilecrate and npm packages.