diff --git a/conformance/012-gba-subset.deck b/conformance/012-gba-subset.deck new file mode 100644 index 0000000..241d759 --- /dev/null +++ b/conformance/012-gba-subset.deck @@ -0,0 +1,20 @@ +# Squarely inside the GBA bake's subset: two supported generators, no session/automation/scale. +deck 1 +bpm 148 + +track Lead id lead gen gameBoyDmg * 4 + gen type pulse duty 25 env_mode constant vol 13 + gen vib_rate 4 vib_amt 6 + note 60 0 0.25 v 120 + note 64 0.5 0.25 v 110 + note 67 1 0.5 v 100 + +track Bass id bass gen gameBoyDmg + gen type wave wave_shape saw vol 15 + note 36 0 2 v 110 + +track Kick id kick gen gbaDirectSound * 4 + gen waveform pulse duty 50 vol 15 bitcrush true pitch_drop -14 pitch_dec 0.05 + adsr a 0 d 0.1 s 0 r 0.03 + step_pitch 36 + steps x . . . x . . . x . . . x . . . diff --git a/conformance/012-gba-subset.expected.json b/conformance/012-gba-subset.expected.json new file mode 100644 index 0000000..374b9aa --- /dev/null +++ b/conformance/012-gba-subset.expected.json @@ -0,0 +1,363 @@ +{ + "program": { + "tplVersion": 1, + "bpm": 148, + "tracks": [ + { + "name": "Lead", + "id": "lead", + "generatorId": "gameBoyDmg", + "rawGenId": "gameBoyDmg", + "genParams": {}, + "loopBars": 4, + "body": [ + { + "lineNo": 6, + "tokens": [ + "gen", + "type", + "pulse", + "duty", + "25", + "env_mode", + "constant", + "vol", + "13" + ], + "raw": "gen type pulse duty 25 env_mode constant vol 13" + }, + { + "lineNo": 7, + "tokens": [ + "gen", + "vib_rate", + "4", + "vib_amt", + "6" + ], + "raw": "gen vib_rate 4 vib_amt 6" + }, + { + "lineNo": 8, + "tokens": [ + "note", + "60", + "0", + "0.25", + "v", + "120" + ], + "raw": "note 60 0 0.25 v 120" + }, + { + "lineNo": 9, + "tokens": [ + "note", + "64", + "0.5", + "0.25", + "v", + "110" + ], + "raw": "note 64 0.5 0.25 v 110" + }, + { + "lineNo": 10, + "tokens": [ + "note", + "67", + "1", + "0.5", + "v", + "100" + ], + "raw": "note 67 1 0.5 v 100" + } + ], + "genBlocks": [] + }, + { + "name": "Bass", + "id": "bass", + "generatorId": "gameBoyDmg", + "rawGenId": "gameBoyDmg", + "genParams": {}, + "loopBars": null, + "body": [ + { + "lineNo": 13, + "tokens": [ + "gen", + "type", + "wave", + "wave_shape", + "saw", + "vol", + "15" + ], + "raw": "gen type wave wave_shape saw vol 15" + }, + { + "lineNo": 14, + "tokens": [ + "note", + "36", + "0", + "2", + "v", + "110" + ], + "raw": "note 36 0 2 v 110" + } + ], + "genBlocks": [] + }, + { + "name": "Kick", + "id": "kick", + "generatorId": "gbaDirectSound", + "rawGenId": "gbaDirectSound", + "genParams": {}, + "loopBars": 4, + "body": [ + { + "lineNo": 17, + "tokens": [ + "gen", + "waveform", + "pulse", + "duty", + "50", + "vol", + "15", + "bitcrush", + "true", + "pitch_drop", + "-14", + "pitch_dec", + "0.05" + ], + "raw": "gen waveform pulse duty 50 vol 15 bitcrush true pitch_drop -14 pitch_dec 0.05" + }, + { + "lineNo": 18, + "tokens": [ + "adsr", + "a", + "0", + "d", + "0.1", + "s", + "0", + "r", + "0.03" + ], + "raw": "adsr a 0 d 0.1 s 0 r 0.03" + }, + { + "lineNo": 19, + "tokens": [ + "step_pitch", + "36" + ], + "raw": "step_pitch 36" + }, + { + "lineNo": 20, + "tokens": [ + "steps", + "x", + ".", + ".", + ".", + "x", + ".", + ".", + ".", + "x", + ".", + ".", + ".", + "x", + ".", + ".", + "." + ], + "raw": "steps x . . . x . . . x . . . x . . ." + } + ], + "genBlocks": [] + } + ], + "removeTrackIds": [], + "macros": {}, + "autos": [], + "masterMixTokens": null, + "actorMixRows": [], + "clipBlocks": [], + "sessionSceneCount": null, + "sessionSlots": [], + "song": null, + "follow": null, + "launchQuant": null, + "songSeed": null, + "xfade": null, + "mainDeck": null, + "swing": null, + "scaleRoot": null, + "scaleMode": null, + "deckMix": null, + "directives": [], + "hostStatements": {}, + "errors": [] + }, + "trackBodies": [ + { + "id": "lead", + "rows": [ + { + "kind": "gen", + "params": { + "type": "pulse", + "duty": 25, + "envMode": "constant", + "vol": 13 + }, + "lineNo": 6 + }, + { + "kind": "gen", + "params": { + "vibRate": 4, + "vibAmt": 6 + }, + "lineNo": 7 + }, + { + "kind": "note", + "midi": 60, + "startBeat": 0, + "durBeats": 0.25, + "vel": 120, + "prob": null, + "ratchet": null, + "nudge": null, + "bar": null, + "lyric": null, + "lineNo": 8 + }, + { + "kind": "note", + "midi": 64, + "startBeat": 0.5, + "durBeats": 0.25, + "vel": 110, + "prob": null, + "ratchet": null, + "nudge": null, + "bar": null, + "lyric": null, + "lineNo": 9 + }, + { + "kind": "note", + "midi": 67, + "startBeat": 1, + "durBeats": 0.5, + "vel": 100, + "prob": null, + "ratchet": null, + "nudge": null, + "bar": null, + "lyric": null, + "lineNo": 10 + } + ], + "errors": [] + }, + { + "id": "bass", + "rows": [ + { + "kind": "gen", + "params": { + "type": "wave", + "waveShape": "saw", + "vol": 15 + }, + "lineNo": 13 + }, + { + "kind": "note", + "midi": 36, + "startBeat": 0, + "durBeats": 2, + "vel": 110, + "prob": null, + "ratchet": null, + "nudge": null, + "bar": null, + "lyric": null, + "lineNo": 14 + } + ], + "errors": [] + }, + { + "id": "kick", + "rows": [ + { + "kind": "gen", + "params": { + "waveform": "pulse", + "duty": 50, + "vol": 15, + "bitcrush": "true", + "pitchDrop": -14, + "pitchDec": 0.05 + }, + "lineNo": 17 + }, + { + "kind": "adsr", + "a": 0, + "d": 0.1, + "s": 0, + "r": 0.03, + "lineNo": 18 + }, + { + "kind": "stepPitch", + "midi": 36, + "bar": null, + "lineNo": 19 + }, + { + "kind": "steps", + "mode": "literal", + "on": [ + true, + false, + false, + false, + true, + false, + false, + false, + true, + false, + false, + false, + true, + false, + false, + false + ], + "lineNo": 20 + } + ], + "errors": [] + } + ], + "clipBodies": [] +} diff --git a/conformance/profiles.json b/conformance/profiles.json index 9a356fc..3b636a0 100644 --- a/conformance/profiles.json +++ b/conformance/profiles.json @@ -16,16 +16,23 @@ "008-macros-genblock": "macros and gen_block are not baked", "009-directives": "@ directives are live-performance stream lines", "010-errors": "error-message text is implementation-specific", - "011-golden": "uses generators and sections outside the GBA subset" + "011-golden": "uses generators and sections outside the GBA subset", + "001-comments-and-sharps": "uses `scale`, which has no GBA meaning (the sharp-name parsing it pins is still exercised by 012)", + "002-track-header": "uses `remove_track`, an incremental host edit fragment", + "004-notes": "uses `transpose`, applied host-side" }, "mustAccept": [ - "001-comments-and-sharps", - "002-track-header", - "004-notes" + "012-gba-subset" ], "extensions": { - "topLevel": ["wave"], - "body": ["layer", "intensity", "min_intensity"] + "topLevel": [ + "wave" + ], + "body": [ + "layer", + "intensity", + "min_intensity" + ] } } } diff --git a/scripts/build-rust.mjs b/scripts/build-rust.mjs index 0fdc575..dda8b54 100644 --- a/scripts/build-rust.mjs +++ b/scripts/build-rust.mjs @@ -135,6 +135,62 @@ fs.mkdirSync(path.join(out, "tests"), { recursive: true }) fs.copyFileSync(path.join(root, "rust/conformance.rs"), path.join(out, "tests/conformance.rs")) fs.cpSync(path.join(root, "conformance"), path.join(out, "conformance"), { recursive: true }) +// Expose the corpus as embedded data, not just files on disk. +// +// A restricted implementation — tish-gba bakes a subset of the language — has to be able to run the +// same corpus and prove its `profiles.json` entry is honest: that it accepts everything it does not +// declare it rejects. It cannot read this crate's files (a dependency's data is not reachable from a +// consumer), so `include_str!` them and hand them over. +{ + const dir = path.join(root, "conformance") + const names = fs + .readdirSync(dir) + .filter((f) => f.endsWith(".deck")) + .map((f) => f.replace(/\.deck$/, "")) + .sort() + const rows = names + .map( + (n) => + ` Case { name: ${JSON.stringify(n)}, ` + + `source: include_str!("../conformance/${n}.deck"), ` + + `expected: include_str!("../conformance/${n}.expected.json") },` + ) + .join("\n") + fs.writeFileSync( + path.join(out, "src/corpus.rs"), + `//! The conformance corpus, embedded. +//! +//! GENERATED — see the deck repo's scripts/build-rust.mjs. +//! +//! Every implementation of the \`.deck\` language runs these same inputs. A consumer that supports a +//! SUBSET (tish-gba bakes to two sound chips) uses [\`profiles\`] to declare which cases it may reject +//! and why; anything it does not declare, it must accept and parse identically. + +/// One corpus case: the source, and the expected parse as JSON. +pub struct Case { + pub name: &'static str, + pub source: &'static str, + pub expected: &'static str, +} + +/// Every case, sorted by name. +pub fn cases() -> &'static [Case] { + &CASES +} + +/// \`profiles.json\` verbatim — which cases a restricted profile may reject, and why. +pub fn profiles() -> &'static str { + include_str!("../conformance/profiles.json") +} + +static CASES: [Case; ${names.length}] = [ +${rows} +]; +` + ) + fs.appendFileSync(libPath, "pub mod corpus;\n") +} + // Rewrite the generated manifest: real crate name, version tracking the npm package, and the // metadata crates.io needs. //