Skip to main content

unitracker-grammar

The canonical unitracker document as a Lark grammar, with an LALR parser for it and the note table its expressions read as F(k), and the modulator expression language: checked, printed in its one text and compiled to Python.

A document names no player. It holds everything it plays: instruments, programs, expressions, phrases and the tables its expressions index. Its only constants are the note table at its clock and the header defaults (PAL clock, 3 voices, tempo 6). Format 1.22's slots line names, per cell, the modulator slots that fire in order, each with its default binding and the voice events it holds and resets on (slots: {freq: {wave: {mod: note, reset: [row.note]}, tick: {hold: [step-1, row]}}}); its mod records are templates over named parameters and its x records instances (x1: porta {s: 32}); a slot. prefix on a cell's row key, instrument field or command (tick.freq: x3, !tick.freq:x3) names the slot it binds or stops; its order line names the phases of a tick in the order they run (order: [events, ctrl, pulse, filter, freq, pw, cut, read]). Format 1.23's state line, after order, declares per cell its state variables and their sizes in bytes (state: {freq: {t: 1}, pw: {r: 1, q: 2}}), a copy per voice (one for cut), 0 at tick 0, which only code templates read and write; a mod record may be a code block (vib: |, then a statement per line at the record's indentation plus 2) instead of an expression. Its templates stand in one top-level mod: section after tables: and before init, records at 2 spaces and code lines at 4, each keyed by its signature ( vib(l, c, s): |, walk(s): v + s, note(): F(N); parameters separated by , ); a call binds one wherever a cell is bound, its integer arguments in the signature's order without spaces (pw: walk(-2), !tick.freq:vib(0,4,64), slots: {freq: {base: {mod: note()}}}). The grammar admits every version's forms (a row's mod: records may also be keyed by a signature): which a version plays is the player's to say. A definition may also stand inline at its one use in place of its name: a program after its kind in run: and !run: (run: pw[{pw: 5}, end], !run:ctrl[{ctrl: 64}, end]), an instrument after an event's note (C4.{ad: 9, sr: 240}), a phrase with its transpose in a play entry (play: [[C4, -*3]+2, ~]) and, wherever a cell is bound, an anonymous code body on one line (asm{lda v; up:; adc #1; sta v; bne up; rts}, statements separated by ; ); a command and a slot's default also bind a quoted expression (!pw:'v + 40'). A program record (restart, ctrl, pw, filter) may be a template keyed by its signature (arp(a, b): [{ctrl: 65, note: +a}, {wait: a, note: -b}, {loop: 0}]), its rows reading its parameters where they take an integer, as a relative note (+a, -a), as a cell value alone (pw: b) and as a call's argument (pw: walk(b)); a call binds it wherever a program name stands: an instrument's ctrl, pw, filter and restart fields (restart: hr(2,9,220)), and run: and !run:, bare or after its kind (run: ctrl:arp(7,4), !run:arp(7,4)). Format 1.21's hold line names, per cell, the events its modulators hold on (unitracker-player's semantics).

Install

pip install "unitracker-grammar @ git+https://github.com/anarkiwi/unitracker-grammar"

Use

from unitracker_grammar import parse, CanonicalError, frequency, table
tree = parse(text)        # a lark.Tree, or CanonicalError naming the line and column
frequency(57)             # F(57): A4 at the PAL clock
table(clock_hz=1022727)   # F(k) for every note whose word fits, from C0, at NTSC

parser() is the cached lark.Lark parser and GRAMMAR the grammar file resource.

Expressions

unitracker_grammar.expr owns the modulator expression language. An expression is a stdlib ast node, read from a text by tree or from a parse tree's expr and fexpr rules by the Nodes transformer.

from unitracker_grammar import expr, frequency
expr.canonical("1024 + 16*min(3, φ)")       # '16*min(3, φ) + 1024', its one text
fn = expr.compile_expr("v + W[φ]", {"_F": frequency, "_T": {"W": expr.indexed([1, 2])}})
fn(10, 1, 48)                               # (v, φ, N, n=0) -> 12

v is the cell's value, φ the modulator's tick count, N the sounding note and n the event note; a template's other lowercase names are its parameters, which params lists and bind replaces with integers. source is the Python source of an expression (ExpressionError outside the grammar), printed the one text of a node and written an integer literal as an int. // and % raise ZeroDivisionError on a divisor of 0; with zero=True they are 0.

Code templates

unitracker_grammar.code owns format 1.23's code templates: NMOS 6502 routines but brk, jsr and rti, with no indirect modes and no comments, that end in rts. A routine is a tuple of statements, each a Label(name) or an Op(mnemonic, mode, operand): mode is implied (lsr bare is the accumulator's), immediate (Imm(value, byte): #7, #p, #<p, #>p), memory, memory,x or memory,y (Mem(symbol, offset, index): v, v+1, G.lo+3,x), or label (a branch's or jmp's label name).

from unitracker_grammar import code, parse
ops = code.read("lda #c\nsta v\nrts\n")   # checked statements, or CodeError
code.printed(ops)                          # 'lda #c\nsta v\nrts\n', its one text
code.params(ops), code.symbols(ops)        # ('c',), ('v',): in first-use order
code.templates(parse(text))                # {name: statements} of a document's code blocks
code.state(parse(text))                    # {cell: ((name, size), ...)} of its state line

Memory symbols are v (2 bytes: in, the value before the slot; out, the modulator's value), phi (2, φ mod 65536, read-only), nn and ne (the low bytes of N and n), the state variables of the template's cell, and T.lo / T.hi (each entry's low and high byte) for a table T. A code slot fires where an expression would: v, phi, nn, ne and the voice's copy of its cell's state are set, A, X, Y and the flags (but I) are 0, and the routine runs to an rts on an empty stack, within 4096 instructions; v wrapped to the cell's width is its value and the state is stored back. A symbol that is none of these, or a state variable of another cell, is refused when the template is bound. F.lo / F.hi are the note table's bytes, entry k those of frequency(k, clock_hz) for k 0-255.

code.signature("vib(l, c, s)")            # ('vib', ('l', 'c', 's')), CodeError on a repeat
code.declare("vib(c)", ("c",), {"t"})      # the signature, if it declares what the body reads
code.call("vib(0,-4,32)")                  # Call(name='vib', args=(0, -4, 32))
code.inline(code.read("lda v\nrts\n"))     # 'asm{lda v; rts}', an anonymous body's one text
code.signatures(parse(text))               # {name: parameters} of its signed templates, checked
code.programs(parse(text))                 # {name: (kind, parameters)} of its program templates

code.programs checks a document's program templates: each signature declares exactly what its rows read (code.program_reads(body, declared), in first-use order), a program keyed by its name and everything outside the program records read no parameter, a parameter is never part of a larger cell expression nor read by an inline program, and no name is two templates', of programs or modulators. code.call reads a parameter argument as its name (call("pt(b,2)") is Call("pt", ("b", 2))).

In the parse tree a signed record's key is a SIGNATURE token (vib(l, c, s)) where a plain one's is a NAME, a call is one CALL token (vib(0,-4,32)) where a binding may be a name or an expression, and the top-level section is a mods tree of SIGNATURE tokens each followed by its expr or code tree; code.templates keys each by its bare name. A program template's key is a RESTART_SIG, CTRL_SIG, PW_SIG or FILTER_SIG token, a parameter a row reads a PARAM (a REL_PARAM, +a, in a note) outside any factor, and a run's kind before a call a RUN_KIND token (ctrl:) in its program_name. An inline program is a run_program (a PROGRAM_KIND token, then its program), an inline instrument an instrument where an INS token may stand, an inline phrase a phrase where a PHRASE token may, an anonymous body an asm tree of code_* statements (code.Statements reads it as a code), and a command's quoted expression an fexpr.

Grammar

src/unitracker_grammar/canonical.lark; the document format and the meaning of each construct are described in unitracker's docs/format.

Metadata

Release files for unitracker-grammar 0.9.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for unitracker-grammar 0.9.0
File Size Uploaded Provenance
unitracker_grammar-0.9.0.tar.gz 47.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for unitracker-grammar 0.9.0
File Interpreter ABI Platform
unitracker_grammar-0.9.0-py3-none-any.whl Python 3 none any Details

Total release size: 78.1 kB

Release files / unitracker_grammar-0.9.0.tar.gz

Download URL unitracker_grammar-0.9.0.tar.gz
Size 47.8 kB
Tags Source
SHA-256 checksum
How to use checksums
b808a22489972165a56d74729eb0098b234c0ec972f25225b749fbb69c24f203
BLAKE2b-256 checksum
How to use checksums
f5f5d175530ed6252ec2df30fda034bdd3f4a537cecefd8812db8fcef0ea6b1b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / unitracker_grammar-0.9.0-py3-none-any.whl

Download URL unitracker_grammar-0.9.0-py3-none-any.whl
Size 30.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
14987e57e5066d4735b07c4825275266897670d113cdc89fdeb7f24d54d0ec5f
BLAKE2b-256 checksum
How to use checksums
201cb445db2ee18e25aa7e326ae26d1a61ae9e510b36472f85309fcb3b0b1954
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for unitracker-grammar 0.9.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for unitracker_grammar-0.9.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page