The lead-sheet DSL¶
Tonalis's tonalis layer is a generic, format-agnostic lead-sheet / chord-chart
language. You write a harmony chart as plain text; a conformant implementation
reads that text and produces:
- a canonical AST (
LeadSheet), and - a list of findings (errors + warnings).
It is deliberately the pure language layer: it covers lexical structure, document/header/body semantics, the chord grammar, the canonical AST, and the findings codes — and emits no vendor file/URL format. Rendering the AST into a specific notation-app format is the job of a separate downstream codec built on top of this core.
Normative reference
This page is an orientation. The authoritative contract is the lead-sheet
SPEC at conformance/tonalis/SPEC.md in the repository, and the
machine-checkable case corpus under conformance/tonalis/cases/. Where
this prose and the spec disagree, the spec (and the blessed cases generated
from the Python reference) win.
Syntax at a glance¶
title: All The Things You Are
composer: Jerome Kern
key: Ab
time: 4/4
[A]
| F-7 | Bb7 | Eb^7 | Ab^7 |
| D-7 G7 | C^7 | C^7 | C^7 |
A document is a header followed by a body:
- Header —
key: valueper line. Required keys:title,key,time. Also recognized:composer,style. The body begins at the first line whose first character is[,|, or{. - Sections —
[A],[B],[C],[D],[Intro]/[i],[Verse]/[v], with an optional trailing integer ([A2]). Each carries a neutralkindrole; a downstream codec maps that to its own section marker. - Measures —
|-separated. Multiple chords in one measure split the bar evenly;chord:Nsets an explicit beat count. - Repeats & endings —
{ ... }is a repeat;1./2.mark first/second endings;Zor||is a final barline. - Navigation —
@segno,@coda,@tocoda,@fine;<text>annotations;@break/@newlinerender hints;[time: n/d]mid-body meter changes. - No-chord —
N.C.(orn).
Chart to AST: a worked example¶
Here is the whole round trip on a compact chart — the exact output shown is captured from the Python reference (the other ports are byte-identical by conformance). Input:
parse_dsl returns 0 findings (a clean chart) and this LeadSheet AST — shown
here as its canonical JSON projection (to_json):
{
"schema_version": 1,
"meta": {
"title": "Blue Bossa",
"composer": "Kenny Dorham",
"key": "Cm",
"time": [4, 4] // (1)!
},
"sections": [
{
"label": "A",
"kind": "a", // (2)!
"measures": [
{ "cells": [ { "chord": "C-7", "beats": null, "alt": null } ],
"ending": null, "bar_open": false, "barline": "normal",
"nav": [], "hints": [], "line": 7 },
{ "cells": [ { "chord": "F-7", "beats": null, "alt": null } ],
"ending": null, "bar_open": false, "barline": "normal",
"nav": [], "hints": [], "line": 7 },
{ "cells": [ // (3)!
{ "chord": "D-7", "beats": null, "alt": null },
{ "chord": "G7", "beats": null, "alt": null }
],
"ending": null, "bar_open": false, "barline": "normal",
"nav": [], "hints": [], "line": 7 },
{ "cells": [ { "chord": "C-7", "beats": null, "alt": null } ],
"ending": null, "bar_open": false, "barline": "normal",
"nav": [], "hints": [], "line": 7 }
]
}
]
}
- Time signature is a
[numerator, denominator]pair, not a string — so consumers never re-parse"4/4". labelis what you typed (A);kindis the neutral role a downstream codec maps to its own section marker.[Verse]and[v]both yieldkind: "verse".- A measure with two chords produces two cells, each with
beats: null. The even split across the bar is derived by the consumer — the AST only stores an explicitbeatswhen you writechord:N(e.g.C-7:3). Nothing is invented on your behalf.
This projection round-trips losslessly (from_json(to_json(ir)) == ir), and the
canonical text printer re-flows layout so parse(serialize(ir)) == ir modulo source
line numbers. Both round-trips are gated by the conformance suite on every case.
When the linter has something to say¶
The linter never throws — malformed input becomes findings, compared on
(code, severity, line). Feeding it a chart that uses spelled-out chord
qualities:
produces two error-severity findings (the chart fails to compile):
bad-chord error line 6 invalid chord token: 'Cmaj7'
bad-chord error line 6 invalid chord token: 'Dm7'
Cmaj7 and Dm7 are rejected because quality is written with glyphs, not words
— it's C^7 and D-7. G7 and Csus are fine. See the grammar below.
Chord grammar¶
Chord quality uses glyphs, not words:
| Glyph | Meaning |
|---|---|
- |
minor |
^ |
major-7 (C^7); bare C^ is allowed |
o |
diminished |
h |
half-diminished |
+ |
augmented |
sus |
suspended |
Extensions include b5 #5 6 b9 9 #9 11 #11 b13 13, the add\d+ form (Cadd9),
combined shorthand (C69), alt (C7alt), and slash bass (D-/C). The
spelled-out words maj/min/dim/aug are rejected — only the glyph forms
are valid.
The grammar is permissive by design: pathological-but-valid repeats such as
C7777 and Csussus are accepted. Chord validity is defined by
is_valid_chord, which delegates to the music_dsl Chord parser (the
executable source of truth). See the theory model for the parser
behind it.
The AST¶
The IR is neutral — it stores no vendor-format detail:
LeadSheet = { "schema_version": 1, "meta": Meta, "sections": [Section] }
Meta = { "title"?: str, "composer"?: str, "style"?: str, "key"?: str, "time"?: [int,int] }
Section = { "label": str, "kind": "a"|"b"|"c"|"d"|"intro"|"verse", "measures": [Measure] }
Measure = { "cells": [Cell], "ending": int|null, "bar_open": bool,
"barline": "normal"|"repeat_end"|"final", "nav": [NavItem],
"hints": [str], "line": int }
Cell = { "chord": str, "beats": int|null, "alt": str|null }
NavItem = ["segno"] | ["coda"] | ["tocoda"] | ["fine"]
| ["text", "<...>"] | ["time", [int, int]]
The projection round-trips losslessly (from_json(to_json(ir)) == ir) and the
canonical text printer re-flows layout so that parse(serialize(ir)) == ir
modulo source line numbers. Both round-trips, plus a canonical text golden, are
gated by the conformance suite on every AST-bearing case.
Findings¶
The linter never throws on arbitrary input; malformed input becomes findings.
Findings are compared on (code, severity, line) only — message text is
non-normative and may differ between ports. An error finding means compilation
fails; a warning-only chart compiles successfully. The full findings-code
table (23 codes: bad-chord, beat-sum, unbalanced-repeat,
missing-header, …) is defined in §7 of the normative spec.