Adapt parser and tests to new sompyler AST slot names
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
This commit is contained in:
parent
7fd29ef9b4
commit
859e62e143
@ -439,17 +439,22 @@ function buildOffset(node) {
|
|||||||
type: 'offset',
|
type: 'offset',
|
||||||
tick: node.props.tick,
|
tick: node.props.tick,
|
||||||
stemNotes: [],
|
stemNotes: [],
|
||||||
|
motifs: [],
|
||||||
unknownProps: collectUnknownProps(node.props, new Set(['tick'])),
|
unknownProps: collectUnknownProps(node.props, new Set(['tick'])),
|
||||||
};
|
};
|
||||||
for (const child of node.children) {
|
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));
|
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;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildStemNote(node) {
|
function buildStemNote(node) {
|
||||||
const KNOWN = new Set(['pitch', 'eff_length', 'adj_stress', 'adjacent']);
|
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 {
|
return {
|
||||||
type: 'stem_note',
|
type: 'stem_note',
|
||||||
pitch: node.props.pitch,
|
pitch: node.props.pitch,
|
||||||
@ -458,8 +463,9 @@ function buildStemNote(node) {
|
|||||||
adjStress: node.props.adj_stress ?? null,
|
adjStress: node.props.adj_stress ?? null,
|
||||||
length: null,
|
length: null,
|
||||||
weight: null,
|
weight: null,
|
||||||
chainText: node.children.find(c => c.slot === 'chain')?.props._tmp_string ?? '',
|
chainText: chainNode?.props._tmp_string ?? '',
|
||||||
clauses: node.children
|
writeToName: writeToNode?.positionals[0] ?? null,
|
||||||
|
clauses: (chainNode?.children ?? [])
|
||||||
.filter(c => c.parentSlot === 'chain' && c.slot === 'clause')
|
.filter(c => c.parentSlot === 'chain' && c.slot === 'clause')
|
||||||
.map(buildClause),
|
.map(buildClause),
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
@ -472,9 +478,9 @@ function buildClause(node) {
|
|||||||
type: 'clause',
|
type: 'clause',
|
||||||
index: node.positionals[0] ?? 0,
|
index: node.positionals[0] ?? 0,
|
||||||
repeat: node.props.repeat ?? null,
|
repeat: node.props.repeat ?? null,
|
||||||
notes: node.children.filter(c => c.slot === 'note' && c.parentSlot === 'clause').map(c => ({ ...c.props })),
|
notes: node.children.filter(c => c.slot === 'note' && c.parentSlot === 'seq').map(c => ({ ...c.props })),
|
||||||
pauses: node.children.filter(c => c.slot === 'pause').map(c => ({ length: c.props.length })),
|
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').map(c => ({
|
stacks: node.children.filter(c => c.slot === 'stack' && c.parentSlot === 'seq').map(c => ({
|
||||||
length: c.props.length, netlength: c.props.netlength,
|
length: c.props.length, netlength: c.props.netlength,
|
||||||
notes: c.children.filter(cn => cn.slot === 'note').map(cn => ({ ...cn.props })),
|
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'])),
|
unknownProps: collectUnknownProps(node.props, new Set(['label'])),
|
||||||
};
|
};
|
||||||
for (const child of node.children) {
|
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.stemNotes.push(buildStemNote(child));
|
||||||
}
|
}
|
||||||
m.isStatic = m.stemNotes.length > 0 && !m.stemNotes.some(sn => Number.isInteger(sn.pitch));
|
m.isStatic = m.stemNotes.length > 0 && !m.stemNotes.some(sn => Number.isInteger(sn.pitch));
|
||||||
|
|||||||
@ -193,8 +193,11 @@ export const PaneFO = {
|
|||||||
fields: [
|
fields: [
|
||||||
{ key: 'tick', value: node.tick, editable: false },
|
{ key: 'tick', value: node.tick, editable: false },
|
||||||
{ key: 'stem notes', value: node.stemNotes.map(snLabel).join(', ') || '—', 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),
|
...unknownPropFields(node),
|
||||||
],
|
].filter(Boolean),
|
||||||
onChange: null,
|
onChange: null,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
106
test-parser.mjs
106
test-parser.mjs
@ -434,10 +434,11 @@ section('Motif parsing — dynamic motif');
|
|||||||
const DYN_MOTIF = `00 bar '001P1L1M1'
|
const DYN_MOTIF = `00 bar '001P1L1M1'
|
||||||
01 bar.voice 'pi'
|
01 bar.voice 'pi'
|
||||||
02 voice.motif label='oct'
|
02 voice.motif label='oct'
|
||||||
03 motif.stem_note pitch=0
|
03 line.stem_note pitch=0
|
||||||
04 chain.clause 0 repeat=1
|
04 stem_note.chain _tmp_string='oo'
|
||||||
05 clause.note letter='o' shift=0 length=1 netlength=1
|
05 chain.clause 0 repeat=1
|
||||||
05 clause.note letter='o' shift=12 netlength=1 length=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 dynBar = buildModel(parseAstLog(DYN_MOTIF)).bars[0];
|
||||||
const dynVoice = dynBar.voices['pi'];
|
const dynVoice = dynBar.voices['pi'];
|
||||||
@ -452,9 +453,10 @@ section('Motif parsing — static motif');
|
|||||||
const STAT_MOTIF = `00 bar '001P1L1M1'
|
const STAT_MOTIF = `00 bar '001P1L1M1'
|
||||||
01 bar.voice 'pi'
|
01 bar.voice 'pi'
|
||||||
02 voice.motif label='cadence'
|
02 voice.motif label='cadence'
|
||||||
03 motif.stem_note pitch='C4'
|
03 line.stem_note pitch='C4'
|
||||||
04 chain.clause 0
|
04 stem_note.chain _tmp_string='o'
|
||||||
05 clause.note letter='o' shift=0 length=1 netlength=1
|
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'];
|
const statVoice = buildModel(parseAstLog(STAT_MOTIF)).bars[0].voices['pi'];
|
||||||
ok('static motif isStatic=true', statVoice.motifs[0].isStatic === true);
|
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'
|
const PS_FIXTURE = `00 bar '001P1L1M1'
|
||||||
01 bar.voice 'pi'
|
01 bar.voice 'pi'
|
||||||
02 voice.offset tick=0
|
02 voice.offset tick=0
|
||||||
03 offset.stem_note pitch='C4'
|
03 line.stem_note pitch='C4'
|
||||||
04 chain.clause 0
|
04 stem_note.chain _tmp_string='o.oe'
|
||||||
05 clause.note letter='o' shift=0 length=1 netlength=1
|
05 chain.clause 0
|
||||||
05 clause.pause length=1
|
06 seq.note letter='o' shift=0 length=1 netlength=1
|
||||||
04 chain.clause 1
|
06 seq.pause length=1
|
||||||
05 clause.stack length=2 netlength=2
|
05 chain.clause 1
|
||||||
06 stack.note letter='o' shift=0
|
06 seq.stack length=2 netlength=2
|
||||||
06 stack.note letter='e' shift=0
|
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 psVoice = buildModel(parseAstLog(PS_FIXTURE)).bars[0].voices['pi'];
|
||||||
const psSn = psVoice.offsets[0].stemNotes[0];
|
const psSn = psVoice.offsets[0].stemNotes[0];
|
||||||
@ -484,13 +487,14 @@ section('Motif parsing — nested chain ignored gracefully');
|
|||||||
const NC_FIXTURE = `00 bar '001P1L1M1'
|
const NC_FIXTURE = `00 bar '001P1L1M1'
|
||||||
01 bar.voice 'pi'
|
01 bar.voice 'pi'
|
||||||
02 voice.offset tick=0
|
02 voice.offset tick=0
|
||||||
03 offset.stem_note pitch='C4'
|
03 line.stem_note pitch='C4'
|
||||||
04 chain.clause 0
|
04 stem_note.chain _tmp_string='o(...)'
|
||||||
05 clause.note letter='o' shift=0 length=1 netlength=1
|
05 chain.clause 0
|
||||||
05 clause.chain length=2
|
06 seq.note letter='o' shift=0 length=1 netlength=1
|
||||||
06 chain.clause 0
|
06 seq.chain length=2
|
||||||
07 clause.stack length=1 netlength=1
|
07 chain.clause 0
|
||||||
08 stack.note letter='o' shift=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];
|
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);
|
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'
|
const DJ_FIXTURE = `00 bar '001P1L1M1'
|
||||||
01 bar.voice 'pi'
|
01 bar.voice 'pi'
|
||||||
02 voice.motif label='oct'
|
02 voice.motif label='oct'
|
||||||
03 motif.stem_note pitch='C4'
|
03 line.stem_note pitch='C4'
|
||||||
06 chain.clause 0
|
07 chain.clause 0
|
||||||
`;
|
`;
|
||||||
let djThrew = false;
|
let djThrew = false;
|
||||||
try { parseAstLog(DJ_FIXTURE); } catch (e) { djThrew = true; }
|
try { parseAstLog(DJ_FIXTURE); } catch (e) { djThrew = true; }
|
||||||
@ -516,6 +520,56 @@ let nsThrew = false;
|
|||||||
try { parseAstLog(NS_FIXTURE); } catch (e) { nsThrew = true; }
|
try { parseAstLog(NS_FIXTURE); } catch (e) { nsThrew = true; }
|
||||||
ok('missing slot throws', nsThrew);
|
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');
|
section('Full fixture integration');
|
||||||
ok('fixture parses without error (checked above)', raw && raw.slot === 'root');
|
ok('fixture parses without error (checked above)', raw && raw.slot === 'root');
|
||||||
ok('fixture has 432 bars', model.bars.length === 432);
|
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)));
|
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);
|
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)));
|
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 ────────────────────────────────────────────────────────────────
|
// ── Summary ────────────────────────────────────────────────────────────────
|
||||||
console.log(`\n══ ${pass} passed, ${fail} failed ══\n`);
|
console.log(`\n══ ${pass} passed, ${fail} failed ══\n`);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user