diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-07-12 16:03:29 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-07-12 16:03:29 +0900 |
| commit | 22f96694a96ed9fba169e77bc27d19d00edca083 (patch) | |
| tree | 8f59c1d3f211c296b005a3f6ede8206bc3675a68 /src/mml_composemusic_mcp/ast_nodes.py | |
| parent | 3bb311ad8e0f10617805c8737d92d13e6a627818 (diff) | |
MML構文解析処理を追加
Diffstat (limited to 'src/mml_composemusic_mcp/ast_nodes.py')
| -rw-r--r-- | src/mml_composemusic_mcp/ast_nodes.py | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/src/mml_composemusic_mcp/ast_nodes.py b/src/mml_composemusic_mcp/ast_nodes.py new file mode 100644 index 0000000..bc4fdc4 --- /dev/null +++ b/src/mml_composemusic_mcp/ast_nodes.py @@ -0,0 +1,230 @@ +"""Common AST node definitions for ppmck and pyxel MML.""" + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class ASTNode: + """Base class for all AST nodes.""" + + line: int + column: int + + +@dataclass +class Program(ASTNode): + """Root program node.""" + + tracks: list[Track] = field(default_factory=list) + + +@dataclass +class Track(ASTNode): + """A single track definition.""" + + track_id: str # "A" / "B" / "T" / "N" / "L" (ppmck) or "0"-"3" (pyxel) + channel: str # "Pulse1" / "Pulse2" / "Triangle" / "Noise" / "Loop" + mode: str # "ppmck" / "pyxel" + statements: list[Statement] = field(default_factory=list) + headers: list[Header] = field(default_factory=list) + + +@dataclass +class Header(ASTNode): + """ppmck # header line.""" + + key: str + value: str + unterminated: bool = False + + +# --- Statements --- + + +@dataclass +class NoteStmt(ASTNode): + """A note command (c, d, e, ...).""" + + note_name: str # normalized to lowercase: "c"-"b" + accidental: int = 0 # +1 sharp, -1 flat, 0 none + length: int | None = None # None means use default length + dots: int = 0 + + +@dataclass +class RestStmt(ASTNode): + """A rest command (r / R).""" + + length: int | None = None + dots: int = 0 + + +@dataclass +class OctaveStmt(ASTNode): + """Octave command or relative octave shift.""" + + value: int | None = None # absolute octave number + direction: str | None = None # "up" or "down" for relative shifts + + +@dataclass +class LengthStmt(ASTNode): + """Default length command (l / L).""" + + value: int + + +@dataclass +class VolumeStmt(ASTNode): + """Volume command (v / V).""" + + value: int + + +@dataclass +class DutyStmt(ASTNode): + """Duty/tone command (q / @).""" + + value: int + + +@dataclass +class TempoStmt(ASTNode): + """Tempo command (t / T).""" + + value: int + + +@dataclass +class GateTimeStmt(ASTNode): + """Gate time command (Q, pyxel only).""" + + value: int + + +@dataclass +class TransposeStmt(ASTNode): + """Transpose command (K, pyxel only).""" + + value: int + + +@dataclass +class DetuneStmt(ASTNode): + """Detune command (Y, pyxel only).""" + + value: int + + +@dataclass +class TieStmt(ASTNode): + """Tie/slur command (&).""" + + target: NoteStmt | RestStmt | int | None = None + + +@dataclass +class RepeatStartStmt(ASTNode): + """Start of repeat section ([).""" + + pass + + +@dataclass +class RepeatEndStmt(ASTNode): + """End of repeat section (] with optional count).""" + + count: int | None = None # None means infinite + + +@dataclass +class BarStmt(ASTNode): + """Bar line (|), visual only.""" + + pass + + +@dataclass +class ExtCmdStmt(ASTNode): + """pyxel @ENV / @VIB / @GLI command.""" + + cmd: str # "ENV" / "VIB" / "GLI" + slot: int + params: list[int] = field(default_factory=list) + + +# --- Stage 2 statements (kept in AST but ignored by semantic analyzer) --- + + +@dataclass +class CountLengthStmt(ASTNode): + """ppmck count length note (c%48).""" + + note_name: str + accidental: int + count: int + + +@dataclass +class EnvelopeDefStmt(ASTNode): + """ppmck envelope definition (@v0 = { ... }).""" + + slot: int + points: list[int] = field(default_factory=list) + loop_points: list[int] = field(default_factory=list) + + +@dataclass +class SweepStmt(ASTNode): + """ppmck sweep command (s1,2).""" + + speed: int + depth: int + + +@dataclass +class NoiseModeStmt(ASTNode): + """ppmck noise mode command (m0 / m1).""" + + mode: int + + +Statement = ( + NoteStmt + | RestStmt + | OctaveStmt + | LengthStmt + | VolumeStmt + | DutyStmt + | TempoStmt + | GateTimeStmt + | TransposeStmt + | DetuneStmt + | TieStmt + | RepeatStartStmt + | RepeatEndStmt + | BarStmt + | ExtCmdStmt + | CountLengthStmt + | EnvelopeDefStmt + | SweepStmt + | NoiseModeStmt +) + + +def _node_to_dict(node: Any) -> Any: + """Recursively convert an AST node to a JSON-compatible dict.""" + if isinstance(node, list): + return [_node_to_dict(item) for item in node] + if not isinstance(node, ASTNode): + return node + data: dict[str, Any] = {"__type__": node.__class__.__name__} + for key, value in node.__dict__.items(): + data[key] = _node_to_dict(value) + return data + + +def ast_to_dict(node: ASTNode) -> dict[str, Any]: + """Convert an AST node to a JSON-compatible dict.""" + return _node_to_dict(node) |
