const LINE_RE = /^(\d{2}) (\S+(?:\.\S+)*) ?(.*)/; const DEBUG_RE = /^\d{2} # DEBUG/; function coerce(s) { if (s === 'True' || s === 'Y' || s === 'on' || s === 'true') return true; if (s === 'False' || s === 'N' || s === 'off' || s === 'false') return false; if (s === '') return s; const n = Number(s); if (!isNaN(n) && s.trim() !== '') return n; return s; } export function parseAstLog(text) { const root = { slot: 'root', parentSlot: null, depth: -1, positionals: [], props: {}, children: [] }; const stack = [root]; for (const line of text.split('\n')) { if (!line.trim()) continue; if (DEBUG_RE.test(line)) continue; const m = LINE_RE.exec(line); if (!m) continue; const depth = parseInt(m[1], 10); const slotFull = m[2]; const rest = m[3] || ''; const dotIdx = slotFull.indexOf('.'); let parentSlot = null, slot = slotFull; if (dotIdx !== -1) { parentSlot = slotFull.slice(0, dotIdx); slot = slotFull.slice(dotIdx + 1); } const { positionals, props } = parseRest(rest); const node = { slot, parentSlot, depth, positionals, props, children: [] }; // Pop stack to find correct parent (parent must have depth < current) while (stack.length > 1 && stack[stack.length - 1].depth >= depth) { stack.pop(); } stack[stack.length - 1].children.push(node); stack.push(node); } return root; } function parseRest(rest) { const positionals = []; const props = {}; let i = 0; const n = rest.length; let inProps = false; while (i < n) { // skip spaces while (i < n && rest[i] === ' ') i++; if (i >= n) break; if (rest[i] === "'") { // single-quoted value (positional string) const j = rest.indexOf("'", i + 1); const val = rest.slice(i + 1, j === -1 ? n : j); i = j === -1 ? n : j + 1; if (!inProps) positionals.push(val); } else { // scan to next space let j = i; while (j < n && rest[j] !== ' ') j++; const tok = rest.slice(i, j); i = j; const eqIdx = tok.indexOf('='); if (eqIdx !== -1) { inProps = true; const key = tok.slice(0, eqIdx); let rawVal = tok.slice(eqIdx + 1); if (rawVal.startsWith("'")) { // value continues until next single quote const valStart = i - (tok.length - eqIdx - 1); // find closing quote: search from after the opening quote const openPos = rest.indexOf("'", rest.lastIndexOf(key + '=', i) + key.length + 1); const closePos = rest.indexOf("'", openPos + 1); rawVal = closePos === -1 ? rest.slice(openPos + 1) : rest.slice(openPos + 1, closePos); i = closePos === -1 ? n : closePos + 1; } props[key] = coerce(rawVal); } else if (!inProps) { positionals.push(coerce(tok)); } // bare token after props start is ignored (shouldn't happen per spec) } } return { positionals, props }; } // ── Second pass: build typed model ────────────────────────────────────────── export function buildModel(rawTree) { const score = { type: 'score', info: null, tuning: null, articles: [], stageVoices: [], instruments: [], bars: [], }; for (const node of rawTree.children) { switch (node.slot) { case 'info': score.info = { ...node.props }; break; case 'tuning': score.tuning = buildTuning(node); break; case 'article': score.articles.push(buildArticle(node)); break; case 'stage_voice': score.stageVoices.push({ name: node.positionals[0], direction: node.props.direction, distance: node.props.distance, }); break; case 'instrument': score.instruments.push(buildInstrument(node)); break; case 'bar': score.bars.push(buildBar(node)); break; default: score[node.slot] = buildGeneric(node); } } return score; } function buildTuning(node) { const t = { base: node.props.base, scales: {}, chords: {} }; for (const child of node.children) { if (child.slot === 'scales') { t.scales[child.positionals[0]] = child.positionals.slice(1); } else if (child.slot === 'chords') { t.chords[child.positionals[0]] = child.positionals.slice(1); } } return t; } function buildArticle(node) { return { type: 'article', name: node.positionals[0], props: { ...node.props }, properties: node.children .filter(c => c.slot === 'property') .map(c => ({ name: c.positionals[0], ...c.props })), }; } function buildInstrument(node) { const instr = { type: 'instrument', name: node.positionals[0], notChangedSince: node.props.NOT_CHANGED_SINCE ?? null, isLinked: (node.positionals[0] ?? '').includes('/'), isDirty: false, variations: [], basicProperties: null, railsbackCurve: null, volumes: null, timbre: null, fmModulations: [], rawChildren: [], }; for (const child of node.children) { switch (child.parentSlot + '.' + child.slot) { case 'character.variation': instr.variations.push(buildVariation(child)); break; case 'character.basic_properties': instr.basicProperties = buildBasicProperties(child); break; case 'RAILSBACK_CURVE.shape': instr.railsbackCurve = buildShape(child); break; case 'VOLUMES.shape': instr.volumes = buildShape(child); break; case 'TIMBRE.shape': instr.timbre = buildShape(child); break; case 'FM.modulation': instr.fmModulations.push({ ...child.props }); break; default: instr.rawChildren.push(buildGeneric(child)); } } return instr; } function buildVariation(node) { const v = { type: 'variation', dependsOn: node.props.depends_on ?? null, basicProperties: null, labelSpecs: [], subvariations: [], spread: null, rawChildren: [], }; for (const child of node.children) { const key = (child.parentSlot ?? child.slot) + '.' + child.slot; switch (key) { case 'variation.basic_properties': v.basicProperties = buildBasicProperties(child); break; case 'variation.label_spec': v.labelSpecs.push(buildLabelSpec(child)); break; case 'variation.subvariation': v.subvariations.push(buildVariation(child)); break; case 'variation.SPREAD': v.spread = child.positionals; break; default: v.rawChildren.push(buildGeneric(child)); } } return v; } function buildBasicProperties(node) { const bp = { type: 'basic_properties', A: null, S: null, R: null, oscillator: null, fmModulations: [], rawChildren: [], }; for (const child of node.children) { const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot; if (child.parentSlot === 'A' && child.slot === 'shape') { bp.A = buildShape(child); } else if (child.parentSlot === 'S' && child.slot === 'shape') { bp.S = buildShape(child); } else if (child.parentSlot === 'R' && child.slot === 'shape') { bp.R = buildShape(child); } else if (child.parentSlot === 'variation' && child.slot === 'O') { bp.oscillator = child.props.ref ?? child.positionals[0]; } else if (child.parentSlot === 'FM' && child.slot === 'modulation') { bp.fmModulations.push({ ...child.props }); } else { bp.rawChildren.push(buildGeneric(child)); } } return bp; } function buildLabelSpec(node) { const ls = { type: 'label_spec', label: node.positionals[0], basicProperties: null, rawChildren: [], }; for (const child of node.children) { if (child.parentSlot === 'variation' && child.slot === 'basic_properties') { ls.basicProperties = buildBasicProperties(child); } else { ls.rawChildren.push(buildGeneric(child)); } } return ls; } function buildShape(node) { return { type: 'shape', length: node.props.length, start: node.props.start, z: node.props.z ?? 1, coords: node.children .filter(c => c.slot === 'coords') .map(c => ({ x: c.props.x, y: c.props.y, z: c.props.z ?? 1, isSharp: c.props.is_sharp ?? false, })), }; } function buildBar(node) { const id = node.positionals[0] ?? ''; const idMatch = id.match(/^(\w?)(\d+)P(\d+)L(\d+)M(\d+)$/); const bar = { type: 'bar', id, movement: idMatch ? idMatch[1] : '', part: idMatch ? parseInt(idMatch[2]) : 0, line: idMatch ? parseInt(idMatch[3]) : 0, measure: idMatch ? parseInt(idMatch[4]) : 0, stressor: null, tempoShape: null, tempoLevels: null, lowerStressBound: null, upperStressBound: null, tunings: [], voices: {}, rawChildren: [], }; for (const child of node.children) { const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot; switch (fqSlot) { case 'stress_pattern.stressor': bar.stressor = buildStressor(child); break; case 'tempo.shape': bar.tempoShape = buildShape(child); break; case 'tempo.levels': bar.tempoLevels = child.positionals[0]; break; case 'lower_stress_bound.shape': bar.lowerStressBound = buildShape(child); break; case 'upper_stress_bound.shape': bar.upperStressBound = buildShape(child); break; case 'bar.tuning': bar.tunings.push({ ...child.props }); break; case 'bar.voice': bar.voices[child.positionals[0]] = buildVoice(child); break; default: bar.rawChildren.push(buildGeneric(child)); } } return bar; } function buildStressor(node) { const levels = []; let currentGroup = []; for (const child of node.children) { if (child.slot === 'level') { currentGroup.push(parseInt(child.positionals[0])); } else if (child.slot === 'subdivision') { levels.push(currentGroup); currentGroup = []; } } if (currentGroup.length) levels.push(currentGroup); return { type: 'stressor', groups: levels }; } function buildVoice(node) { const voice = { type: 'voice', name: node.positionals[0], offsets: [], articles: [], motifs: [], }; for (const child of node.children) { const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot; switch (fqSlot) { case 'voice.offset': voice.offsets.push(buildOffset(child)); break; case 'voice.article': voice.articles.push(child.positionals[0]); break; case 'voice.motif': voice.motifs.push(child.props.label); break; default: // ignore } } return voice; } function buildOffset(node) { const offset = { type: 'offset', tick: node.props.tick, stemNotes: [], clusters: [], chains: [], }; for (const child of node.children) { const fqSlot = (child.parentSlot ?? '') + (child.parentSlot ? '.' : '') + child.slot; switch (fqSlot) { case 'offset.stem_note': offset.stemNotes.push({ pitch: child.props.pitch, effLength: child.props.eff_length }); break; case 'offset.cluster': offset.clusters.push(buildCluster(child)); break; case 'offset.chain': offset.chains.push({ index: child.positionals[0], children: child.children.map(buildGeneric) }); break; default: // ignore } } return offset; } function buildCluster(node) { const cluster = { type: 'cluster', index: node.positionals[0], repeat: node.props.repeat ?? null, notes: [], pauses: [], groups: [], subchains: [], }; for (const child of node.children) { switch (child.slot) { case 'note': cluster.notes.push({ ...child.props }); break; case 'pause': cluster.pauses.push({ length: child.props.length }); break; case 'group': cluster.groups.push({ length: child.props.length, netlength: child.props.netlength }); break; case 'subchain': cluster.subchains.push({ length: child.props.length, children: child.children.map(buildGeneric) }); break; default: // ignore } } return cluster; } function buildGeneric(node) { return { type: node.slot, parentSlot: node.parentSlot, depth: node.depth, positionals: node.positionals, props: node.props, children: node.children.map(buildGeneric), }; }