From 859e62e1438a5973f8cd6b190a5ad750b3ab78a9 Mon Sep 17 00:00:00 2001 From: c0dev0id Date: Sat, 27 Jun 2026 18:03:14 +0200 Subject: [PATCH] Adapt parser and tests to new sompyler AST slot names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sompyler commit c3b51bc renamed slots across the board: - offset.stem_note → line.stem_note (parentSlot 'offset' → 'line') - motif.stem_note → line.stem_note (parentSlot 'motif' → 'line') - clause.note/pause/stack → seq.note/pause/stack (parentSlot 'clause' → 'seq') - stem_note.chain is now a real hierarchical node (depth 04) between line.stem_note and chain.clause; clauses are now its children, not direct children of stem_note Also adds: - offset.motifs[] for line.motif invocations at tick positions - stem_note.writeToName from stem_note.write_to positional - adjacent prop already stored; new tests verify True/False/absent PaneFO.js: show offset motif invocations inline (label + chord) test-parser.mjs: update synthetic fixtures to new depths/slot names; add tests for line.motif, stem_note.write_to, adjacent prop --- static/ast-parser.js | 20 ++++--- static/components/PaneFO.js | 5 +- test-parser.mjs | 106 ++++++++++++++++++++++++++++-------- 3 files changed, 99 insertions(+), 32 deletions(-) diff --git a/static/ast-parser.js b/static/ast-parser.js index 274ff8b..d5365c0 100644 --- a/static/ast-parser.js +++ b/static/ast-parser.js @@ -439,17 +439,22 @@ function buildOffset(node) { type: 'offset', tick: node.props.tick, stemNotes: [], + motifs: [], unknownProps: collectUnknownProps(node.props, new Set(['tick'])), }; for (const child of node.children) { - if (child.parentSlot === 'offset' && child.slot === 'stem_note') + if (child.parentSlot === 'line' && child.slot === 'stem_note') offset.stemNotes.push(buildStemNote(child)); + else if (child.parentSlot === 'line' && child.slot === 'motif') + offset.motifs.push({ label: child.positionals[0], chord: child.props.chord ?? null }); } return offset; } function buildStemNote(node) { const KNOWN = new Set(['pitch', 'eff_length', 'adj_stress', 'adjacent']); + const chainNode = node.children.find(c => c.parentSlot === 'stem_note' && c.slot === 'chain'); + const writeToNode = node.children.find(c => c.parentSlot === 'stem_note' && c.slot === 'write_to'); return { type: 'stem_note', pitch: node.props.pitch, @@ -458,8 +463,9 @@ function buildStemNote(node) { adjStress: node.props.adj_stress ?? null, length: null, weight: null, - chainText: node.children.find(c => c.slot === 'chain')?.props._tmp_string ?? '', - clauses: node.children + chainText: chainNode?.props._tmp_string ?? '', + writeToName: writeToNode?.positionals[0] ?? null, + clauses: (chainNode?.children ?? []) .filter(c => c.parentSlot === 'chain' && c.slot === 'clause') .map(buildClause), isDirty: false, @@ -472,9 +478,9 @@ function buildClause(node) { type: 'clause', index: node.positionals[0] ?? 0, repeat: node.props.repeat ?? null, - notes: node.children.filter(c => c.slot === 'note' && c.parentSlot === 'clause').map(c => ({ ...c.props })), - pauses: node.children.filter(c => c.slot === 'pause').map(c => ({ length: c.props.length })), - stacks: node.children.filter(c => c.slot === 'stack').map(c => ({ + notes: node.children.filter(c => c.slot === 'note' && c.parentSlot === 'seq').map(c => ({ ...c.props })), + pauses: node.children.filter(c => c.slot === 'pause' && c.parentSlot === 'seq').map(c => ({ length: c.props.length })), + stacks: node.children.filter(c => c.slot === 'stack' && c.parentSlot === 'seq').map(c => ({ length: c.props.length, netlength: c.props.netlength, notes: c.children.filter(cn => cn.slot === 'note').map(cn => ({ ...cn.props })), })), @@ -489,7 +495,7 @@ function buildMotif(node) { unknownProps: collectUnknownProps(node.props, new Set(['label'])), }; for (const child of node.children) { - if (child.parentSlot === 'motif' && child.slot === 'stem_note') + if (child.parentSlot === 'line' && child.slot === 'stem_note') m.stemNotes.push(buildStemNote(child)); } m.isStatic = m.stemNotes.length > 0 && !m.stemNotes.some(sn => Number.isInteger(sn.pitch)); diff --git a/static/components/PaneFO.js b/static/components/PaneFO.js index 4031703..6263c40 100644 --- a/static/components/PaneFO.js +++ b/static/components/PaneFO.js @@ -193,8 +193,11 @@ export const PaneFO = { fields: [ { key: 'tick', value: node.tick, editable: false }, { key: 'stem notes', value: node.stemNotes.map(snLabel).join(', ') || '—', editable: false }, + node.motifs?.length + ? { key: 'motifs', value: node.motifs.map(m => m.chord ? `${m.label}(${m.chord})` : m.label).join(', '), editable: false } + : null, ...unknownPropFields(node), - ], + ].filter(Boolean), onChange: null, }), ); diff --git a/test-parser.mjs b/test-parser.mjs index a74cb49..bf2b32b 100644 --- a/test-parser.mjs +++ b/test-parser.mjs @@ -434,10 +434,11 @@ section('Motif parsing — dynamic motif'); const DYN_MOTIF = `00 bar '001P1L1M1' 01 bar.voice 'pi' 02 voice.motif label='oct' -03 motif.stem_note pitch=0 -04 chain.clause 0 repeat=1 -05 clause.note letter='o' shift=0 length=1 netlength=1 -05 clause.note letter='o' shift=12 netlength=1 length=1 +03 line.stem_note pitch=0 +04 stem_note.chain _tmp_string='oo' +05 chain.clause 0 repeat=1 +06 seq.note letter='o' shift=0 length=1 netlength=1 +06 seq.note letter='o' shift=12 netlength=1 length=1 `; const dynBar = buildModel(parseAstLog(DYN_MOTIF)).bars[0]; const dynVoice = dynBar.voices['pi']; @@ -452,9 +453,10 @@ section('Motif parsing — static motif'); const STAT_MOTIF = `00 bar '001P1L1M1' 01 bar.voice 'pi' 02 voice.motif label='cadence' -03 motif.stem_note pitch='C4' -04 chain.clause 0 -05 clause.note letter='o' shift=0 length=1 netlength=1 +03 line.stem_note pitch='C4' +04 stem_note.chain _tmp_string='o' +05 chain.clause 0 +06 seq.note letter='o' shift=0 length=1 netlength=1 `; const statVoice = buildModel(parseAstLog(STAT_MOTIF)).bars[0].voices['pi']; ok('static motif isStatic=true', statVoice.motifs[0].isStatic === true); @@ -464,14 +466,15 @@ section('Motif parsing — pause and stack in clause'); const PS_FIXTURE = `00 bar '001P1L1M1' 01 bar.voice 'pi' 02 voice.offset tick=0 -03 offset.stem_note pitch='C4' -04 chain.clause 0 -05 clause.note letter='o' shift=0 length=1 netlength=1 -05 clause.pause length=1 -04 chain.clause 1 -05 clause.stack length=2 netlength=2 -06 stack.note letter='o' shift=0 -06 stack.note letter='e' shift=0 +03 line.stem_note pitch='C4' +04 stem_note.chain _tmp_string='o.oe' +05 chain.clause 0 +06 seq.note letter='o' shift=0 length=1 netlength=1 +06 seq.pause length=1 +05 chain.clause 1 +06 seq.stack length=2 netlength=2 +07 stack.note letter='o' shift=0 +07 stack.note letter='e' shift=0 `; const psVoice = buildModel(parseAstLog(PS_FIXTURE)).bars[0].voices['pi']; const psSn = psVoice.offsets[0].stemNotes[0]; @@ -484,13 +487,14 @@ section('Motif parsing — nested chain ignored gracefully'); const NC_FIXTURE = `00 bar '001P1L1M1' 01 bar.voice 'pi' 02 voice.offset tick=0 -03 offset.stem_note pitch='C4' -04 chain.clause 0 -05 clause.note letter='o' shift=0 length=1 netlength=1 -05 clause.chain length=2 -06 chain.clause 0 -07 clause.stack length=1 netlength=1 -08 stack.note letter='o' shift=0 +03 line.stem_note pitch='C4' +04 stem_note.chain _tmp_string='o(...)' +05 chain.clause 0 +06 seq.note letter='o' shift=0 length=1 netlength=1 +06 seq.chain length=2 +07 chain.clause 0 +08 seq.stack length=1 netlength=1 +09 stack.note letter='o' shift=0 `; const ncSn = buildModel(parseAstLog(NC_FIXTURE)).bars[0].voices['pi'].offsets[0].stemNotes[0]; ok('nested chain: only 1 top-level clause', ncSn.clauses.length === 1); @@ -501,8 +505,8 @@ section('Depth-jump guard (abort)'); const DJ_FIXTURE = `00 bar '001P1L1M1' 01 bar.voice 'pi' 02 voice.motif label='oct' -03 motif.stem_note pitch='C4' -06 chain.clause 0 +03 line.stem_note pitch='C4' +07 chain.clause 0 `; let djThrew = false; try { parseAstLog(DJ_FIXTURE); } catch (e) { djThrew = true; } @@ -516,6 +520,56 @@ let nsThrew = false; try { parseAstLog(NS_FIXTURE); } catch (e) { nsThrew = true; } ok('missing slot throws', nsThrew); +section('line.motif invocation at offset'); +const LM_FIXTURE = `00 bar '001P1L1M1' +01 bar.voice 'pi' +02 voice.offset tick=0 +03 line.stem_note pitch='C4' +04 stem_note.chain _tmp_string='o' +05 chain.clause 0 +06 seq.note letter='o' shift=0 length=1 netlength=1 +03 line.motif 'coct' +03 line.motif 'oct' chord='C2' +`; +const lmOffset = buildModel(parseAstLog(LM_FIXTURE)).bars[0].voices['pi'].offsets[0]; +ok('line.motif: offset has 1 stem note', lmOffset.stemNotes.length === 1); +ok('line.motif: offset.motifs has 2 entries', lmOffset.motifs.length === 2); +ok('line.motif: first motif label', lmOffset.motifs[0].label === 'coct'); +ok('line.motif: second motif label', lmOffset.motifs[1].label === 'oct'); +ok('line.motif: second motif chord', lmOffset.motifs[1].chord === 'C2'); +ok('line.motif: first motif chord null', lmOffset.motifs[0].chord === null); + +section('stem_note.write_to and chainText'); +const WT_FIXTURE = `00 bar '001P1L1M1' +01 bar.voice 'pi' +02 voice.offset tick=0 +03 line.stem_note pitch='C2' +04 stem_note.write_to 'coct' +04 stem_note.chain _tmp_string='o=o+3*4' +05 chain.clause 0 repeat=3 +06 seq.note letter='o' shift=0 netlength=1 length=1 +06 seq.note letter='o' shift=3 netlength=1 length=1 +`; +const wtSn = buildModel(parseAstLog(WT_FIXTURE)).bars[0].voices['pi'].offsets[0].stemNotes[0]; +ok('write_to: writeToName', wtSn.writeToName === 'coct'); +ok('write_to: chainText from _tmp_string', wtSn.chainText === 'o=o+3*4'); +ok('write_to: clause parsed', wtSn.clauses.length === 1); +ok('write_to: clause repeat', wtSn.clauses[0].repeat === 3); +ok('write_to: clause notes', wtSn.clauses[0].notes.length === 2); + +section('adjacent prop on stem_note'); +const ADJ_FIXTURE = `00 bar '001P1L1M1' +01 bar.voice 'pi' +02 voice.offset tick=0 +03 line.stem_note pitch='C4' +03 line.stem_note pitch='D4' adjacent=False +03 line.stem_note pitch='E4' adjacent=True +`; +const adjOffset = buildModel(parseAstLog(ADJ_FIXTURE)).bars[0].voices['pi'].offsets[0]; +ok('adjacent: null when absent', adjOffset.stemNotes[0].adjacent === null); +ok('adjacent: false when False', adjOffset.stemNotes[1].adjacent === false); +ok('adjacent: true when True', adjOffset.stemNotes[2].adjacent === true); + section('Full fixture integration'); ok('fixture parses without error (checked above)', raw && raw.slot === 'root'); ok('fixture has 432 bars', model.bars.length === 432); @@ -523,6 +577,10 @@ const voicesWithMotifs = model.bars.flatMap(b => Object.values(b.voices)).filter ok('fixture voices have motif objects', voicesWithMotifs.every(v => v.motifs.every(m => typeof m === 'object' && 'label' in m))); const offsetsWithStemNotes = model.bars.flatMap(b => Object.values(b.voices)).flatMap(v => v.offsets).filter(o => o.stemNotes.length > 0); ok('fixture offsets have stem note objects', offsetsWithStemNotes.every(o => o.stemNotes.every(sn => 'pitch' in sn && 'clauses' in sn))); +const offsetsWithMotifInvocations = model.bars.flatMap(b => Object.values(b.voices)).flatMap(v => v.offsets).filter(o => o.motifs?.length > 0); +ok('fixture offset motif invocations are objects', offsetsWithMotifInvocations.every(o => o.motifs.every(m => 'label' in m))); +const stemNotesWithChainText = offsetsWithStemNotes.flatMap(o => o.stemNotes).filter(sn => sn.chainText); +ok('fixture stem notes with chain text have clauses', stemNotesWithChainText.every(sn => sn.clauses.length > 0)); // ── Summary ──────────────────────────────────────────────────────────────── console.log(`\n══ ${pass} passed, ${fail} failed ══\n`);