summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-12 16:20:37 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-12 16:20:37 +0900
commit8a1fcf81dbab9e2eff3c4d558c6910bc8208e49e (patch)
tree70e6f5f867c8308f72fc106eb4ce0634c146d651 /src
parent22f96694a96ed9fba169e77bc27d19d00edca083 (diff)
MML構文解析処理一部修正。ドキュメント最新化
Diffstat (limited to 'src')
-rw-r--r--src/mml_composemusic_mcp/lexer.py30
-rw-r--r--src/mml_composemusic_mcp/parser_base.py13
-rw-r--r--src/mml_composemusic_mcp/parser_ppmck.py91
-rw-r--r--src/mml_composemusic_mcp/parser_pyxel.py42
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()