///|
/// TOML Value types
pub(all) enum TomlValue {
String(String)
Integer(Int64)
Float(Double)
Boolean(Bool)
DateTime(String) // ISO 8601 datetime as string
Array(Array[TomlValue])
Table(Map[String, TomlValue])
} derive(Show, Eq)
///|
/// Convert TomlValue to JSON-like string for debugging
pub fn TomlValue::to_string(self : TomlValue) -> String {
match self {
String(s) => "String(\{s})"
Integer(i) => "Integer(\{i})"
Float(f) => "Float(\{f})"
Boolean(b) => "Boolean(\{b})"
DateTime(dt) => "DateTime(\{dt})"
Array(arr) => {
let items = arr.map(fn(v) { v.to_string() }).join(", ")
"Array([\{items}])"
}
Table(tbl) => {
let items = []
for k, v in tbl {
items.push("\{k}: \{v.to_string()}")
}
let joined = items.join(", ")
"Table({\{joined}})"
}
}
}
///|
/// Error types for TOML parsing
pub(all) suberror ParseError {
UnexpectedChar(Int, Char)
UnexpectedEof
InvalidEscape(Int, Char)
InvalidNumber(Int, String)
InvalidKey(Int, String)
InvalidValue(Int, String)
DuplicateKey(String)
InvalidTable(String)
} derive(Eq, Show)
///|
/// Token types for lexer
enum Token {
// Literals
TString(String)
TInteger(Int64)
TFloat(Double)
TBoolean(Bool)
TDateTime(String)
// Symbols
TLeftBracket // [
TRightBracket // ]
TLeftBrace // {
TRightBrace // }
TEquals // =
TDot // .
TComma // ,
// Identifiers
TIdent(String)
// Special
TNewline
TEof
} 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 current character without consuming
fn Lexer::peek(self : Lexer) -> Char? {
if self.pos >= self.input.length() {
None
} else {
self.input.get_char(self.pos)
}
}
///|
/// Peek ahead n characters
fn Lexer::peek_ahead(self : Lexer, n : Int) -> Char? {
let pos = self.pos + n
if pos >= self.input.length() {
None
} else {
self.input.get_char(pos)
}
}
///|
/// Advance position by n characters
fn Lexer::advance(self : Lexer, n : Int) -> Unit {
for i = 0; i < n; i = i + 1 {
match self.peek() {
Some('\n') => {
self.line = self.line + 1
self.col = 1
}
Some(_) => self.col = self.col + 1
None => ()
}
self.pos = self.pos + 1
}
}
///|
/// Skip whitespace (space and tab only, not newlines)
fn Lexer::skip_whitespace(self : Lexer) -> Unit {
while true {
match self.peek() {
Some(' ') | Some('\t') => self.advance(1)
_ => break
}
}
}
///|
/// Skip comment (from # to end of line)
fn Lexer::skip_comment(self : Lexer) -> Unit {
match self.peek() {
Some('#') => {
self.advance(1)
while true {
match self.peek() {
Some('\n') | None => break
Some(_) => self.advance(1)
}
}
}
_ => ()
}
}
///|
/// Check if character is valid for identifier start
fn is_ident_start(c : Char) -> Bool {
match c {
'a'..='z' | 'A'..='Z' | '_' | '-' => true
_ => false
}
}
///|
/// Check if character is valid for identifier continuation
fn is_ident_continue(c : Char) -> Bool {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' => true
_ => false
}
}
///|
/// Check if character is a digit
fn is_digit(c : Char) -> Bool {
match c {
'0'..='9' => true
_ => false
}
}
///|
/// Parse a bare key (unquoted identifier)
fn Lexer::parse_bare_key(self : Lexer) -> String raise ParseError {
let start = self.pos
match self.peek() {
Some(c) if is_ident_start(c) => {
self.advance(1)
while true {
match self.peek() {
Some(c) if is_ident_continue(c) => self.advance(1)
_ => break
}
}
try! self.input[start:self.pos].to_string()
}
Some(c) =>
raise ParseError::InvalidKey(self.pos, "Invalid key character: \{c}")
None => raise ParseError::UnexpectedEof
}
}
///|
/// Parse a basic string (double-quoted)
fn Lexer::parse_basic_string(self : Lexer) -> String raise ParseError {
self.advance(1) // skip opening quote
let mut result = ""
while true {
match self.peek() {
Some('"') => {
self.advance(1)
break
}
Some('\\') => {
self.advance(1)
match self.peek() {
Some('b') => {
result = result + "\u{0008}"
self.advance(1)
}
Some('t') => {
result = result + "\t"
self.advance(1)
}
Some('n') => {
result = result + "\n"
self.advance(1)
}
Some('f') => {
result = result + "\u{000C}"
self.advance(1)
}
Some('r') => {
result = result + "\r"
self.advance(1)
}
Some('"') => {
result = result + "\""
self.advance(1)
}
Some('\\') => {
result = result + "\\"
self.advance(1)
}
Some(c) => raise ParseError::InvalidEscape(self.pos, c)
None => raise ParseError::UnexpectedEof
}
}
Some('\n') =>
raise ParseError::InvalidValue(self.pos, "Newline in basic string")
Some(c) => {
result = result + c.to_string()
self.advance(1)
}
None => raise ParseError::UnexpectedEof
}
}
result
}
///|
/// Parse a literal string (single-quoted, no escapes)
fn Lexer::parse_literal_string(self : Lexer) -> String raise ParseError {
self.advance(1) // skip opening quote
let start = self.pos
while true {
match self.peek() {
Some('\'') => {
let result = try! self.input[start:self.pos].to_string()
self.advance(1)
return result
}
Some('\n') =>
raise ParseError::InvalidValue(self.pos, "Newline in literal string")
Some(_) => self.advance(1)
None => raise ParseError::UnexpectedEof
}
} else {
raise ParseError::UnexpectedEof
}
}
///|
/// Parse a number (integer or float)
fn Lexer::parse_number(self : Lexer) -> Token raise ParseError {
let start = self.pos
let mut has_dot = false
let mut has_exp = false
// Handle sign
match self.peek() {
Some('+') | Some('-') => self.advance(1)
_ => ()
}
// Parse digits
while true {
match self.peek() {
Some(c) if is_digit(c) => self.advance(1)
Some('_') => self.advance(1) // Allow underscores in numbers
Some('.') if not(has_dot) && not(has_exp) => {
has_dot = true
self.advance(1)
}
Some('e') | Some('E') if not(has_exp) => {
has_exp = true
has_dot = true // Exponent implies float
self.advance(1)
// Handle exponent sign
match self.peek() {
Some('+') | Some('-') => self.advance(1)
_ => ()
}
}
_ => break
}
}
let view = try! self.input[start:self.pos]
let num_str = view.to_string().replace(old="_", new="")
if has_dot || has_exp {
let f = @strconv.parse_double(num_str[:]) catch {
_ => raise ParseError::InvalidNumber(start, num_str)
}
TFloat(f)
} else {
let i = @strconv.parse_int64(num_str[:]) catch {
_ => raise ParseError::InvalidNumber(start, num_str)
}
TInteger(i)
}
}
///|
/// Get next token
pub fn Lexer::next_token(self : Lexer) -> Token raise ParseError {
while true {
self.skip_whitespace()
match self.peek() {
None => return TEof
Some('#') => self.skip_comment()
Some('\n') | Some('\r') => {
self.advance(1)
return TNewline
}
Some('[') => {
self.advance(1)
return TLeftBracket
}
Some(']') => {
self.advance(1)
return TRightBracket
}
Some('{') => {
self.advance(1)
return TLeftBrace
}
Some('}') => {
self.advance(1)
return TRightBrace
}
Some('=') => {
self.advance(1)
return TEquals
}
Some('.') => {
self.advance(1)
return TDot
}
Some(',') => {
self.advance(1)
return TComma
}
Some('"') => return TString(self.parse_basic_string())
Some('\'') => return TString(self.parse_literal_string())
Some(c) if is_digit(c) || c == '+' || c == '-' =>
return self.parse_number()
Some('t') | Some('f') =>
// Try to parse boolean
if self.peek() == Some('t') &&
self.peek_ahead(1) == Some('r') &&
self.peek_ahead(2) == Some('u') &&
self.peek_ahead(3) == Some('e') {
self.advance(4)
return TBoolean(true)
} else if self.peek() == Some('f') &&
self.peek_ahead(1) == Some('a') &&
self.peek_ahead(2) == Some('l') &&
self.peek_ahead(3) == Some('s') &&
self.peek_ahead(4) == Some('e') {
self.advance(5)
return TBoolean(false)
} else {
// Not a boolean, parse as identifier
return TIdent(self.parse_bare_key())
}
Some(c) if is_ident_start(c) => return TIdent(self.parse_bare_key())
Some(c) => raise ParseError::UnexpectedChar(self.pos, c)
}
} else {
TEof
}
}
///|
/// Parser state
struct Parser {
lexer : Lexer
mut current : Token
mut peek_tok : Token
}
///|
/// Create a new parser
pub fn Parser::new(input : String) -> Parser raise ParseError {
let lexer = Lexer::new(input)
let p = { lexer, current: TEof, peek_tok: TEof }
p.advance() // Load first token
p.advance() // Load second token
p
}
///|
/// Advance to next token
fn Parser::advance(self : Parser) -> Unit raise ParseError {
self.current = self.peek_tok
self.peek_tok = self.lexer.next_token()
}
///|
/// Skip newlines
fn Parser::skip_newlines(self : Parser) -> Unit raise ParseError {
while true {
match self.current {
TNewline => self.advance()
_ => break
}
}
}
///|
/// Parse a key (possibly dotted)
fn Parser::parse_key(self : Parser) -> Array[String] raise ParseError {
let keys = []
while true {
match self.current {
TIdent(name) => {
keys.push(name)
self.advance()
}
TString(name) => {
keys.push(name)
self.advance()
}
_ => raise ParseError::InvalidKey(self.lexer.pos, "Expected key")
}
match self.current {
TDot => {
self.advance()
continue
}
_ => return keys
}
} else {
keys
}
}
///|
/// Parse an array
fn Parser::parse_array(self : Parser) -> Array[TomlValue] raise ParseError {
self.advance() // skip [
self.skip_newlines()
let values = []
while true {
match self.current {
TRightBracket => {
self.advance()
return values
}
_ => {
values.push(self.parse_value())
self.skip_newlines()
match self.current {
TComma => {
self.advance()
self.skip_newlines()
}
TRightBracket => ()
_ =>
raise ParseError::InvalidValue(
self.lexer.pos,
"Expected , or ] in array",
)
}
}
}
} else {
values
}
}
///|
/// Parse an inline table
fn Parser::parse_inline_table(
self : Parser,
) -> Map[String, TomlValue] raise ParseError {
self.advance() // skip {
let table : Map[String, TomlValue] = {}
while true {
match self.current {
TRightBrace => {
self.advance()
return table
}
_ => {
let keys = self.parse_key()
match self.current {
TEquals => self.advance()
_ =>
raise ParseError::InvalidValue(
self.lexer.pos,
"Expected = in inline table",
)
}
let value = self.parse_value()
// Insert value at nested key path
if keys.length() == 1 {
table[keys[0]] = value
} else {
// Handle nested keys in inline table
let mut current = table
for i = 0; i < keys.length() - 1; i = i + 1 {
let key = keys[i]
match current.get(key) {
Some(Table(tbl)) => current = tbl
None => {
let new_table : Map[String, TomlValue] = {}
current[key] = Table(new_table)
current = new_table
}
_ => raise ParseError::DuplicateKey(keys.join("."))
}
}
current[keys[keys.length() - 1]] = value
}
match self.current {
TComma => self.advance()
TRightBrace => ()
_ =>
raise ParseError::InvalidValue(
self.lexer.pos,
"Expected , or } in inline table",
)
}
}
}
} else {
table
}
}
///|
/// Parse a value
fn Parser::parse_value(self : Parser) -> TomlValue raise ParseError {
match self.current {
TString(s) => {
self.advance()
String(s)
}
TInteger(i) => {
self.advance()
Integer(i)
}
TFloat(f) => {
self.advance()
Float(f)
}
TBoolean(b) => {
self.advance()
Boolean(b)
}
TLeftBracket => Array(self.parse_array())
TLeftBrace => Table(self.parse_inline_table())
_ => raise ParseError::InvalidValue(self.lexer.pos, "Expected value")
}
}
///|
/// Insert value at dotted key path
fn insert_at_path(
table : Map[String, TomlValue],
keys : Array[String],
value : TomlValue,
) -> Unit raise ParseError {
if keys.length() == 0 {
raise ParseError::InvalidKey(0, "Empty key path")
}
if keys.length() == 1 {
if table.contains(keys[0]) {
raise ParseError::DuplicateKey(keys[0])
}
table[keys[0]] = value
} else {
let key = keys[0]
let rest = keys[1:]
match table.get(key) {
Some(Table(nested)) => insert_at_path(nested, rest.to_array(), value)
None => {
let nested : Map[String, TomlValue] = {}
table[key] = Table(nested)
insert_at_path(nested, rest.to_array(), value)
}
_ => raise ParseError::DuplicateKey(keys.join("."))
}
}
}
///|
/// Get or create table at path
fn get_or_create_table(
table : Map[String, TomlValue],
keys : Array[String],
) -> Map[String, TomlValue] raise ParseError {
if keys.length() == 0 {
return table
}
let key = keys[0]
let rest = keys[1:]
match table.get(key) {
Some(Table(nested)) =>
if rest.length() == 0 {
nested
} else {
get_or_create_table(nested, rest.to_array())
}
None => {
let nested : Map[String, TomlValue] = {}
table[key] = Table(nested)
if rest.length() == 0 {
nested
} else {
get_or_create_table(nested, rest.to_array())
}
}
_ => raise ParseError::InvalidTable(keys.join("."))
}
}
///|
/// Parse TOML document
pub fn parse(input : String) -> Map[String, TomlValue] raise ParseError {
let parser = Parser::new(input)
let root : Map[String, TomlValue] = {}
let mut current_table = root
parser.skip_newlines()
while true {
match parser.current {
TEof => return root
TNewline => parser.advance()
TLeftBracket => {
parser.advance()
// Check for array of tables [[...]]
let is_array = match parser.current {
TLeftBracket => {
parser.advance()
true
}
_ => false
}
let keys = parser.parse_key()
if is_array {
// Array of tables
match parser.current {
TRightBracket => parser.advance()
_ =>
raise ParseError::InvalidTable("Expected ] for array of tables")
}
match parser.current {
TRightBracket => parser.advance()
_ =>
raise ParseError::InvalidTable("Expected ]] for array of tables")
}
// Create new table in array
let new_table : Map[String, TomlValue] = {}
if keys.length() == 1 {
match root.get(keys[0]) {
Some(Array(arr)) => arr.push(Table(new_table))
None => root[keys[0]] = Array([Table(new_table)])
_ =>
raise ParseError::InvalidTable(
"Key already exists as non-array",
)
}
current_table = new_table
} else {
let parent_keys = keys[0:keys.length() - 1].to_array()
let array_key = keys[keys.length() - 1]
let parent = get_or_create_table(root, parent_keys)
match parent.get(array_key) {
Some(Array(arr)) => arr.push(Table(new_table))
None => parent[array_key] = Array([Table(new_table)])
_ =>
raise ParseError::InvalidTable(
"Key already exists as non-array",
)
}
current_table = new_table
}
} else {
// Regular table [...]
match parser.current {
TRightBracket => parser.advance()
_ => raise ParseError::InvalidTable("Expected ]")
}
current_table = get_or_create_table(root, keys)
}
parser.skip_newlines()
}
_ => {
// Key-value pair
let keys = parser.parse_key()
match parser.current {
TEquals => parser.advance()
_ => raise ParseError::InvalidValue(parser.lexer.pos, "Expected =")
}
let value = parser.parse_value()
insert_at_path(current_table, keys, value)
parser.skip_newlines()
}
}
} else {
root
}
}
///|
/// Parse TOML string and return Result
pub fn parse_toml(input : String) -> Result[Map[String, TomlValue], ParseError] {
Ok(parse(input)) catch {
err => Err(err)
}
}