// Copyright 2026 moonpddl contributors
//
// SPDX-License-Identifier: MIT
///|
/// Token kind produced by the PDDL lexer.
priv enum TokenKind {
LParen
RParen
Sym
VarTok
}
///|
priv struct Token {
kind : TokenKind
text : String
pos : Position
}
///|
/// Character cursor with line/column tracking. PDDL is ASCII in practice;
/// `String::to_array` gives code-point indexing, which is all we need.
priv struct Lexer {
chars : Array[Char]
mut i : Int32
mut line : Int32
mut col : Int32
}
///|
fn Lexer::make(source : String) -> Lexer {
{ chars: source.to_array(), i: 0, line: 1, col: 1, }
}
///|
fn Lexer::at_end(self : Lexer) -> Bool {
self.i >= self.chars.length()
}
///|
fn Lexer::here(self : Lexer) -> Position {
{ line: self.line, column: self.col, }
}
///|
fn Lexer::advance(self : Lexer) -> Char {
let c = self.chars[self.i]
self.i += 1
if c == '\n' {
self.line += 1
self.col = 1
} else {
self.col += 1
}
c
}
///|
fn is_symbol_char(c : Char) -> Bool {
!(c == '(' || c == ')' || c == ';' || c == '?' || c.is_whitespace())
}
///|
fn Lexer::skip_trivia(self : Lexer) -> Unit {
while !self.at_end() {
let c = self.chars[self.i]
if c.is_whitespace() {
ignore(self.advance())
} else if c == ';' {
while !self.at_end() && self.chars[self.i] != '\n' {
ignore(self.advance())
}
} else {
break
}
}
}
///|
fn Lexer::read_symbol(self : Lexer) -> String {
let sb = StringBuilder::StringBuilder()
while !self.at_end() && is_symbol_char(self.chars[self.i]) {
sb.write_char(self.advance().to_ascii_lowercase())
}
sb.to_string()
}
///|
/// Splits a PDDL source into tokens. Symbols are normalized to lowercase,
/// comments (`;` to end of line) and whitespace are dropped.
fn lex(source : String) -> Array[Token] raise PddlError {
let lx = Lexer::make(source)
let tokens : Array[Token] = []
while true {
lx.skip_trivia()
if lx.at_end() {
break
}
let pos = lx.here()
let c = lx.chars[lx.i]
if c == '(' {
ignore(lx.advance())
tokens.push({ kind: LParen, text: "(", pos, })
} else if c == ')' {
ignore(lx.advance())
tokens.push({ kind: RParen, text: ")", pos, })
} else if c == '?' {
ignore(lx.advance())
let name = lx.read_symbol()
if name == "" {
raise syntax_error(lx.here(), "expected a variable name after '?'")
}
tokens.push({ kind: VarTok, text: name, pos, })
} else {
let text = lx.read_symbol()
if text == "" {
raise syntax_error(pos, "unexpected character '\{c}'")
}
tokens.push({ kind: Sym, text, pos, })
}
}
tokens
}