///|
/// TOML Value - represents any TOML value
pub(all) enum TomlValue {
String(String)
Integer(Int64)
Float(Double)
Boolean(Bool)
DateTime(String) // Simplified: store as string
Array(Array[TomlValue])
Table(Map[String, TomlValue])
} derive(Show, Eq)
///|
/// Token types for the lexer
enum Token {
// Literals
TokString(String)
TokInteger(Int64)
TokFloat(Double)
TokBool(Bool)
TokDateTime(String)
// Symbols
TokEqual // =
TokDot // .
TokComma // ,
TokLeftBracket // [
TokRightBracket // ]
TokLeftBrace // {
TokRightBrace // }
TokNewline // \n
// Identifiers
TokIdent(String)
// Special
TokEOF
} derive(Show, Eq)
///|
/// Lexer state
struct Lexer {
input : String
mut pos : Int
mut line : Int
mut col : Int
}
///|
/// Create a new lexer
fn Lexer::new(input : String) -> Lexer {
{ input, pos: 0, line: 1, col: 1 }
}
///|
/// Peek at current character without consuming
fn Lexer::peek(self : Lexer) -> Char? {
if self.pos >= self.input.length() {
None
} else {
self.input.get_char(self.pos)
}
}
///|
/// Consume and return current character
fn Lexer::next(self : Lexer) -> Char? {
match self.peek() {
Some(ch) => {
self.pos += 1
if ch == '\n' {
self.line += 1
self.col = 1
} else {
self.col += 1
}
Some(ch)
}
None => None
}
}
///|
/// Skip whitespace (except newlines)
fn Lexer::skip_whitespace(self : Lexer) -> Unit {
while true {
match self.peek() {
Some(' ') | Some('\t') | Some('\r') => {
self.next() |> ignore
continue
}
_ => break
}
}
}
///|
/// Skip a comment (from # to end of line)
fn Lexer::skip_comment(self : Lexer) -> Unit {
while true {
match self.peek() {
Some('\n') | None => break
_ => {
self.next() |> ignore
continue
}
}
}
}
///|
/// Check if a character is a valid identifier start
fn is_ident_start(ch : Char) -> Bool {
(ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '-'
}
///|
/// Check if a character is a valid identifier continuation
fn is_ident_continue(ch : Char) -> Bool {
(ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '_' ||
ch == '-'
}
///|
/// Lex an identifier or boolean
fn Lexer::lex_ident(self : Lexer) -> Token {
let start = self.pos
while true {
match self.peek() {
Some(ch) if is_ident_continue(ch) => {
self.next() |> ignore
continue
}
_ => break
}
}
// Build string from characters to avoid StringView::to_string() which may raise
let chars : Array[Char] = []
for i = start; i < self.pos; i = i + 1 {
match self.input.get_char(i) {
Some(ch) => chars.push(ch)
None => break
}
}
let text = String::from_array(chars)
match text {
"true" => TokBool(true)
"false" => TokBool(false)
_ => TokIdent(text)
}
}
///|
/// Lex a string (basic or literal)
fn Lexer::lex_string(self : Lexer) -> Token raise {
self.next() |> ignore // consume opening quote
let chars : Array[Char] = []
while true {
match self.next() {
Some('"') => return TokString(String::from_array(chars))
Some('\\') => {
match self.next() {
Some('n') => chars.push('\n')
Some('t') => chars.push('\t')
Some('r') => chars.push('\r')
Some('\\') => chars.push('\\')
Some('"') => chars.push('"')
Some(ch) => chars.push(ch)
None => raise Failure("Unexpected end of string")
}
continue
}
Some(ch) => {
chars.push(ch)
continue
}
None => raise Failure("Unterminated string")
}
} else {
// Unreachable - while true always continues or returns
raise Failure("Unreachable")
}
}
///|
/// Lex a number (integer or float)
fn Lexer::lex_number(self : Lexer) -> Token raise {
let start = self.pos
let mut is_float = false
// Handle optional sign
match self.peek() {
Some('+') | Some('-') => self.next() |> ignore
_ => ()
}
// Collect digits
while true {
match self.peek() {
Some(ch) if ch >= '0' && ch <= '9' => {
self.next() |> ignore
continue
}
Some('_') => {
self.next() |> ignore // Skip underscores in numbers
continue
}
Some('.') => {
is_float = true
self.next() |> ignore
continue
}
Some('e') | Some('E') => {
is_float = true
self.next() |> ignore
match self.peek() {
Some('+') | Some('-') => self.next() |> ignore
_ => ()
}
continue
}
_ => break
}
}
// Build string from characters, skipping underscores
let chars : Array[Char] = []
for i = start; i < self.pos; i = i + 1 {
match self.input.get_char(i) {
Some('_') => continue
Some(ch) => chars.push(ch)
None => break
}
}
let text = String::from_array(chars)
if is_float {
try {
let f = @strconv.parse_double(text)
TokFloat(f)
} catch {
_ => raise Failure("Invalid float: \{text}")
}
} else {
try {
let i = @strconv.parse_int64(text)
TokInteger(i)
} catch {
_ => raise Failure("Invalid integer: \{text}")
}
}
}
///|
/// Get the next token
fn Lexer::next_token(self : Lexer) -> Token raise {
while true {
self.skip_whitespace()
match self.peek() {
None => return TokEOF
Some('#') => {
self.skip_comment()
continue
}
Some('\n') => {
self.next() |> ignore
return TokNewline
}
Some('=') => {
self.next() |> ignore
return TokEqual
}
Some('.') => {
self.next() |> ignore
return TokDot
}
Some(',') => {
self.next() |> ignore
return TokComma
}
Some('[') => {
self.next() |> ignore
return TokLeftBracket
}
Some(']') => {
self.next() |> ignore
return TokRightBracket
}
Some('{') => {
self.next() |> ignore
return TokLeftBrace
}
Some('}') => {
self.next() |> ignore
return TokRightBrace
}
Some('"') => return self.lex_string()
Some(ch) if (ch >= '0' && ch <= '9') || ch == '+' || ch == '-' =>
return self.lex_number()
Some(ch) if is_ident_start(ch) => return self.lex_ident()
Some(ch) => raise Failure("Unexpected character: \{ch}")
}
} else {
// Unreachable
raise Failure("Unreachable")
}
}
///|
/// Tokenize entire input
fn tokenize(input : String) -> Array[Token] raise {
let lexer = Lexer::new(input)
let tokens : Array[Token] = []
while true {
let tok = lexer.next_token()
tokens.push(tok)
if tok == TokEOF {
return tokens
}
continue
} else {
// Unreachable
raise Failure("Unreachable")
}
}
///|
/// Parser state
struct Parser {
tokens : Array[Token]
mut pos : Int
}
///|
/// Create a new parser
fn Parser::new(tokens : Array[Token]) -> Parser {
{ tokens, pos: 0 }
}
///|
/// Peek at current token
fn Parser::peek(self : Parser) -> Token {
if self.pos >= self.tokens.length() {
TokEOF
} else {
self.tokens[self.pos]
}
}
///|
/// Consume and return current token
fn Parser::next(self : Parser) -> Token {
let tok = self.peek()
if self.pos < self.tokens.length() {
self.pos += 1
}
tok
}
///|
/// Skip newline tokens
fn Parser::skip_newlines(self : Parser) -> Unit {
while true {
match self.peek() {
TokNewline => {
self.next() |> ignore
continue
}
_ => break
}
}
}
///|
/// Expect a specific token
fn Parser::expect(self : Parser, expected : Token) -> Unit raise {
let tok = self.next()
if tok != expected {
raise Failure("Expected \{expected}, got \{tok}")
}
}
///|
/// Parse a value
fn Parser::parse_value(self : Parser) -> TomlValue raise {
match self.peek() {
TokString(s) => {
self.next() |> ignore
String(s)
}
TokInteger(i) => {
self.next() |> ignore
Integer(i)
}
TokFloat(f) => {
self.next() |> ignore
Float(f)
}
TokBool(b) => {
self.next() |> ignore
Boolean(b)
}
TokDateTime(dt) => {
self.next() |> ignore
DateTime(dt)
}
TokLeftBracket => self.parse_array()
TokLeftBrace => self.parse_inline_table()
tok => raise Failure("Unexpected token in value: \{tok}")
}
}
///|
/// Parse an array
fn Parser::parse_array(self : Parser) -> TomlValue raise {
self.expect(TokLeftBracket)
self.skip_newlines()
let items : Array[TomlValue] = []
while true {
match self.peek() {
TokRightBracket => {
self.next() |> ignore
return Array(items)
}
_ => {
items.push(self.parse_value())
self.skip_newlines()
match self.peek() {
TokComma => {
self.next() |> ignore
self.skip_newlines()
continue
}
TokRightBracket => continue
tok => raise Failure("Expected comma or ] in array, got \{tok}")
}
}
}
} else {
raise Failure("Unreachable")
}
}
///|
/// Parse an inline table
fn Parser::parse_inline_table(self : Parser) -> TomlValue raise {
self.expect(TokLeftBrace)
let table : Map[String, TomlValue] = {}
while true {
match self.peek() {
TokRightBrace => {
self.next() |> ignore
return Table(table)
}
TokIdent(key) => {
self.next() |> ignore
self.expect(TokEqual)
let value = self.parse_value()
table[key] = value
match self.peek() {
TokComma => {
self.next() |> ignore
continue
}
TokRightBrace => continue
tok =>
raise Failure("Expected comma or }} in inline table, got \{tok}")
}
}
tok => raise Failure("Expected key in inline table, got \{tok}")
}
} else {
raise Failure("Unreachable")
}
}
///|
/// Parse a key path (e.g., "a.b.c")
fn Parser::parse_key_path(self : Parser) -> Array[String] raise {
let keys : Array[String] = []
while true {
match self.peek() {
TokIdent(key) => {
keys.push(key)
self.next() |> ignore
match self.peek() {
TokDot => {
self.next() |> ignore
continue
}
_ => return keys
}
}
tok => raise Failure("Expected identifier in key path, got \{tok}")
}
} else {
raise Failure("Unreachable")
}
}
///|
/// Set a nested value in a table
fn set_nested(
table : Map[String, TomlValue],
keys : Array[String],
value : TomlValue,
) -> Unit raise {
if keys.length() == 0 {
raise Failure("Empty key path")
}
if keys.length() == 1 {
table[keys[0]] = value
return
}
let first_key = keys[0]
let rest_keys = keys[1:].to_array()
match table.get(first_key) {
Some(Table(nested)) => set_nested(nested, rest_keys, value)
Some(_) =>
raise Failure("Key already exists with non-table value: \{first_key}")
None => {
let nested : Map[String, TomlValue] = {}
table[first_key] = Table(nested)
set_nested(nested, rest_keys, value)
}
}
}
///|
/// Get or create a nested table
fn get_or_create_table(
table : Map[String, TomlValue],
keys : Array[String],
) -> Map[String, TomlValue] raise {
if keys.length() == 0 {
return table
}
let first_key = keys[0]
let rest_keys = keys[1:].to_array()
match table.get(first_key) {
Some(Table(nested)) => get_or_create_table(nested, rest_keys)
Some(_) =>
raise Failure("Key already exists with non-table value: \{first_key}")
None => {
let nested : Map[String, TomlValue] = {}
table[first_key] = Table(nested)
get_or_create_table(nested, rest_keys)
}
}
}
///|
/// Parse the entire TOML document
pub fn parse(input : String) -> TomlValue raise {
let tokens = tokenize(input)
let parser = Parser::new(tokens)
let root : Map[String, TomlValue] = {}
let mut current_table = root
parser.skip_newlines()
while true {
match parser.peek() {
TokEOF => return Table(root)
// Table header [section]
TokLeftBracket => {
parser.next() |> ignore
// Check if it's an array of tables [[section]]
let is_array = match parser.peek() {
TokLeftBracket => {
parser.next() |> ignore
true
}
_ => false
}
let keys = parser.parse_key_path()
if is_array {
parser.expect(TokRightBracket)
}
parser.expect(TokRightBracket)
parser.skip_newlines()
current_table = get_or_create_table(root, keys)
continue
}
// Key-value pair
TokIdent(_) => {
let keys = parser.parse_key_path()
parser.expect(TokEqual)
let value = parser.parse_value()
set_nested(current_table, keys, value)
parser.skip_newlines()
continue
}
TokNewline => {
parser.next() |> ignore
continue
}
tok => raise Failure("Unexpected token at top level: \{tok}")
}
} else {
raise Failure("Unreachable")
}
}
///|
/// Convert TomlValue to a pretty string
pub fn TomlValue::to_pretty_string(self : TomlValue) -> String {
match self {
String(s) => "\"\{s}\""
Integer(i) => i.to_string()
Float(f) => f.to_string()
Boolean(b) => b.to_string()
DateTime(dt) => dt
Array(items) => {
let parts = items.map(fn(v) { v.to_pretty_string() })
let sep = ", "
let joined = parts.join(sep)
"[\{joined}]"
}
Table(map) => {
let parts : Array[String] = []
for k, v in map {
parts.push("\{k} = \{v.to_pretty_string()}")
}
let sep = ", "
let joined = parts.join(sep)
"{ \{joined} }"
}
}
}