diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-07-12 16:20:37 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-07-12 16:20:37 +0900 |
| commit | 8a1fcf81dbab9e2eff3c4d558c6910bc8208e49e (patch) | |
| tree | 70e6f5f867c8308f72fc106eb4ce0634c146d651 /src | |
| parent | 22f96694a96ed9fba169e77bc27d19d00edca083 (diff) | |
MML構文解析処理一部修正。ドキュメント最新化
Diffstat (limited to 'src')
| -rw-r--r-- | src/mml_composemusic_mcp/lexer.py | 30 | ||||
| -rw-r--r-- | src/mml_composemusic_mcp/parser_base.py | 13 | ||||
| -rw-r--r-- | src/mml_composemusic_mcp/parser_ppmck.py | 91 | ||||
| -rw-r--r-- | src/mml_composemusic_mcp/parser_pyxel.py | 42 |
4 files changed, 137 insertions, 39 deletions
diff --git a/src/mml_composemusic_mcp/lexer.py b/src/mml_composemusic_mcp/lexer.py index 44e577a..94876bf 100644 --- a/src/mml_composemusic_mcp/lexer.py +++ b/src/mml_composemusic_mcp/lexer.py @@ -83,12 +83,23 @@ class Lexer: start += self._advance() return start + def _read_signed_number(self) -> str: + sign = "" + if self._peek() in "+-": + sign = self._advance() + number = self._read_number() + return sign + number if number else sign + def _read_until_newline(self) -> str: start = "" while self.pos < len(self.source) and self.source[self.pos] not in "\r\n": start += self._advance() return start + def _at_line_start(self) -> bool: + line_start = self.source.rfind("\n", 0, self.pos) + 1 + return not self.source[line_start : self.pos].strip() + def _emit(self, ttype: TokenType, value: str, raw: str = "") -> None: self.tokens.append( Token(ttype, value, self.line, self.column - len(raw), raw or value) @@ -116,8 +127,13 @@ class Lexer: self._emit(TokenType.COMMENT, raw, raw) return if ch == "#": - raw = "#" + self._read_until_newline() - self._emit(TokenType.HEADER, raw, raw) + if self._at_line_start(): + self._advance() + raw = "#" + self._read_until_newline() + self._emit(TokenType.HEADER, raw, raw) + else: + self._advance() + self._emit(TokenType.SHARP, "#", "#") return if ch == "|": self._advance() @@ -152,6 +168,10 @@ class Lexer: self._advance() self._emit(TokenType.SHARP, "+", "+") return + if ch == "#": + self._advance() + self._emit(TokenType.SHARP, "#", "#") + return if ch == "-": self._advance() self._emit(TokenType.FLAT, "-", "-") @@ -232,6 +252,10 @@ class Lexer: self._advance() self._emit(TokenType.SHARP, "+", "+") return + if ch == "#": + self._advance() + self._emit(TokenType.SHARP, "#", "#") + return if ch == "-": self._advance() self._emit(TokenType.FLAT, "-", "-") @@ -259,7 +283,7 @@ class Lexer: if ch.isalpha(): cmd = self._advance() if cmd in "OLVTQKY": - num = self._read_number() + num = self._read_signed_number() if cmd in "KY" else self._read_number() raw = cmd + num if cmd == "O": self._emit(TokenType.OCTAVE, num, raw) diff --git a/src/mml_composemusic_mcp/parser_base.py b/src/mml_composemusic_mcp/parser_base.py index 1cdb40f..506a33e 100644 --- a/src/mml_composemusic_mcp/parser_base.py +++ b/src/mml_composemusic_mcp/parser_base.py @@ -68,6 +68,19 @@ class ParserContext: context=context, ) + def add_missing_number_error( + self, token: Token, command: str, context: str = "" + ) -> None: + self.add_error( + code=ErrorCode.SYNTAX_INVALID_NUMBER, + line=token.line, + column=token.column, + message=f"'{command}' の後に数値が必要です。", + severity="error", + hint=f"例: {command}4 のように数値を続けてください。", + context=context, + ) + class ParserError(Exception): """Raised by parsers to abort a single statement and recover.""" diff --git a/src/mml_composemusic_mcp/parser_ppmck.py b/src/mml_composemusic_mcp/parser_ppmck.py index 9540844..892c167 100644 --- a/src/mml_composemusic_mcp/parser_ppmck.py +++ b/src/mml_composemusic_mcp/parser_ppmck.py @@ -28,12 +28,25 @@ CHANNEL_MAP = { "N": "Noise", } +HEADER_KEYS = { + "#TITLE", + "#COMPOSER", + "#PROGRAMER", + "#OCTAVE-REV", + "#INCLUDE", + "#EX-DISKFM", + "#EX-NAMCO106", + "#BANK-CHANGE", + "#EFFECT-INCLUDE", +} + class PpmckParser: def __init__(self, source: str, tokens: list[Token]) -> None: self.source = source self.ctx = ParserContext(tokens) self.tracks_seen: set[str] = set() + self.track_ids_seen: set[str] = set() def parse(self) -> tuple[Program, list[ErrorDetail]]: program = Program(line=1, column=1, tracks=[]) @@ -42,6 +55,16 @@ class PpmckParser: while self.ctx.peek().type != TokenType.EOF: token = self.ctx.peek() if token.type == TokenType.HEADER: + if program.tracks: + self.ctx.add_error( + code=ErrorCode.SYNTAX_UNEXPECTED_TOKEN, + line=token.line, + column=token.column, + message="ヘッダーはトラック定義より前に記述してください。", + severity="error", + hint="すべての # ヘッダーを最初のトラックより前へ移動してください。", + context=context_line(self.source, token), + ) headers.append(self._parse_header()) continue if token.type == TokenType.COMMENT: @@ -76,6 +99,16 @@ class PpmckParser: key = parts[0] if parts else raw value = "" unterminated = False + if key.upper() not in HEADER_KEYS: + self.ctx.add_error( + code=ErrorCode.SYNTAX_UNEXPECTED_TOKEN, + line=token.line, + column=token.column, + message=f"未知のヘッダー '{key}' です。", + severity="error", + hint="MML_BNF.md に定義されたヘッダーを使用してください。", + context=context_line(self.source, token), + ) if len(parts) > 1: rest = parts[1] if rest.startswith('"'): @@ -95,6 +128,15 @@ class PpmckParser: value = rest[1:-1] else: value = rest + self.ctx.add_error( + code=ErrorCode.SYNTAX_UNEXPECTED_TOKEN, + line=token.line, + column=token.column, + message=f"ヘッダー '{key}' の値は二重引用符で囲んでください。", + severity="error", + hint=f'#TITLE "{value}" のように記述してください。', + context=context_line(self.source, token), + ) return Header( line=token.line, column=token.column, @@ -106,6 +148,17 @@ class PpmckParser: def _parse_track(self) -> Track | None: token = self.ctx.advance() ch = token.value.upper() + if ch in self.track_ids_seen: + self.ctx.add_error( + code=ErrorCode.SYNTAX_DUPLICATE_TRACK, + line=token.line, + column=token.column, + message=f"トラック '{ch}' が複数回定義されています。", + severity="error", + hint="各トラックは1回のみ定義できます。重複定義を削除してください。", + context=context_line(self.source, token), + ) + self.track_ids_seen.add(ch) if ch == "L": return Track( line=token.line, @@ -127,16 +180,6 @@ class PpmckParser: ) return None channel = CHANNEL_MAP[ch] - if channel in self.tracks_seen: - self.ctx.add_error( - code=ErrorCode.SYNTAX_DUPLICATE_TRACK, - line=token.line, - column=token.column, - message=f"トラック '{ch}' が複数回定義されています。", - severity="error", - hint="各トラックは1回のみ定義できます。重複定義を削除してください。", - context=context_line(self.source, token), - ) self.tracks_seen.add(channel) track = Track( line=token.line, @@ -215,15 +258,12 @@ class PpmckParser: def _parse_note(self) -> NoteStmt: token = self.ctx.advance() - length, dots = self._read_length(token) accidental = 0 - while True: - if self.ctx.match(TokenType.SHARP): - accidental += 1 - elif self.ctx.match(TokenType.FLAT): - accidental -= 1 - else: - break + if self.ctx.match(TokenType.SHARP): + accidental = 1 + elif self.ctx.match(TokenType.FLAT): + accidental = -1 + length, dots = self._read_length(token) return NoteStmt( line=token.line, column=token.column, @@ -245,6 +285,7 @@ class PpmckParser: def _parse_octave(self) -> OctaveStmt: token = self.ctx.advance() + self._require_value(token, "o") value = int(token.value) if token.value else 4 return OctaveStmt( line=token.line, @@ -273,6 +314,7 @@ class PpmckParser: def _parse_length(self) -> LengthStmt: token = self.ctx.advance() + self._require_value(token, "l") value = int(token.value) if token.value else 4 return LengthStmt( line=token.line, @@ -282,6 +324,7 @@ class PpmckParser: def _parse_volume(self) -> VolumeStmt: token = self.ctx.advance() + self._require_value(token, "v") value = int(token.value) if token.value else 0 return VolumeStmt( line=token.line, @@ -291,6 +334,7 @@ class PpmckParser: def _parse_duty(self) -> DutyStmt: token = self.ctx.advance() + self._require_value(token, "q") value = int(token.value) if token.value else 0 return DutyStmt( line=token.line, @@ -300,6 +344,7 @@ class PpmckParser: def _parse_tempo(self) -> TempoStmt: token = self.ctx.advance() + self._require_value(token, "t") value = int(token.value) if token.value else 120 return TempoStmt( line=token.line, @@ -307,6 +352,12 @@ class PpmckParser: value=value, ) + def _require_value(self, token: Token, command: str) -> None: + if not token.value: + self.ctx.add_missing_number_error( + token, command, context_line(self.source, token) + ) + def _parse_tie(self) -> TieStmt: token = self.ctx.advance() next_token = self.ctx.peek() @@ -373,8 +424,6 @@ class PpmckParser: ) -def parse_ppmck( - source: str, tokens: list[Token] -) -> tuple[Program, list[ErrorDetail]]: +def parse_ppmck(source: str, tokens: list[Token]) -> tuple[Program, list[ErrorDetail]]: parser = PpmckParser(source, tokens) return parser.parse() diff --git a/src/mml_composemusic_mcp/parser_pyxel.py b/src/mml_composemusic_mcp/parser_pyxel.py index db9787d..06cb613 100644 --- a/src/mml_composemusic_mcp/parser_pyxel.py +++ b/src/mml_composemusic_mcp/parser_pyxel.py @@ -163,15 +163,12 @@ class PyxelParser: def _parse_note(self) -> NoteStmt: token = self.ctx.advance() - length, dots = self._read_length(token) accidental = 0 - while True: - if self.ctx.match(TokenType.SHARP): - accidental += 1 - elif self.ctx.match(TokenType.FLAT): - accidental -= 1 - else: - break + if self.ctx.match(TokenType.SHARP): + accidental = 1 + elif self.ctx.match(TokenType.FLAT): + accidental = -1 + length, dots = self._read_length(token) return NoteStmt( line=token.line, column=token.column, @@ -193,6 +190,7 @@ class PyxelParser: def _parse_octave(self) -> OctaveStmt: token = self.ctx.advance() + self._require_value(token, "O") value = int(token.value) if token.value else 4 return OctaveStmt( line=token.line, @@ -221,6 +219,7 @@ class PyxelParser: def _parse_length(self) -> LengthStmt: token = self.ctx.advance() + self._require_value(token, "L") value = int(token.value) if token.value else 4 return LengthStmt( line=token.line, @@ -230,7 +229,8 @@ class PyxelParser: def _parse_volume(self) -> VolumeStmt: token = self.ctx.advance() - value = int(token.value) if token.value else 0 + self._require_value(token, "V") + value = int(token.value) if token.value not in ("", "+", "-") else 0 return VolumeStmt( line=token.line, column=token.column, @@ -239,6 +239,7 @@ class PyxelParser: def _parse_gate_time(self) -> GateTimeStmt: token = self.ctx.advance() + self._require_value(token, "Q") value = int(token.value) if token.value else 80 return GateTimeStmt( line=token.line, @@ -248,7 +249,8 @@ class PyxelParser: def _parse_at(self) -> DutyStmt: token = self.ctx.advance() - value = int(token.value) if token.value else 0 + self._require_value(token, "@") + value = int(token.value) if token.value not in ("", "+", "-") else 0 return DutyStmt( line=token.line, column=token.column, @@ -257,6 +259,7 @@ class PyxelParser: def _parse_tempo(self) -> TempoStmt: token = self.ctx.advance() + self._require_value(token, "T") value = int(token.value) if token.value else 120 return TempoStmt( line=token.line, @@ -266,7 +269,8 @@ class PyxelParser: def _parse_transpose(self) -> TransposeStmt: token = self.ctx.advance() - value = int(token.value) if token.value else 0 + self._require_value(token, "K", signed=True) + value = int(token.value) if token.value not in ("", "+", "-") else 0 return TransposeStmt( line=token.line, column=token.column, @@ -275,13 +279,23 @@ class PyxelParser: def _parse_detune(self) -> DetuneStmt: token = self.ctx.advance() - value = int(token.value) if token.value else 0 + self._require_value(token, "Y", signed=True) + value = int(token.value) if token.value not in ("", "+", "-") else 0 return DetuneStmt( line=token.line, column=token.column, value=value, ) + def _require_value( + self, token: Token, command: str, *, signed: bool = False + ) -> None: + invalid = not token.value or (signed and token.value in ("+", "-")) + if invalid: + self.ctx.add_missing_number_error( + token, command, context_line(self.source, token) + ) + def _parse_tie(self) -> TieStmt: token = self.ctx.advance() next_token = self.ctx.peek() @@ -369,8 +383,6 @@ class PyxelParser: ) -def parse_pyxel( - source: str, tokens: list[Token] -) -> tuple[Program, list[ErrorDetail]]: +def parse_pyxel(source: str, tokens: list[Token]) -> tuple[Program, list[ErrorDetail]]: parser = PyxelParser(source, tokens) return parser.parse() |
