///|
/// Skip newline tokens
fn Parser::skip_newlines(self : Parser) -> Unit {
let next_view = for view = self.view() {
match view {
[Newline(..), .. rest] => continue rest
rest => break rest
}
}
self.update_view(next_view)
}
///|
/// Try to consume a single bare key from the current position.
/// Handles identifiers, strings, integers, booleans (true/false),
/// and special float keywords (inf/nan) in key position.
fn Parser::try_parse_single_key(self : Parser) -> String? {
match self.view() {
[Identifier(name, ..), .. rest] => {
self.update_view(rest)
Some(name)
}
[StringToken(_, multiline=true, ..), ..] => None // multiline strings not allowed as keys
[StringToken(name, ..), .. rest] => {
self.update_view(rest)
Some(name)
}
[IntegerToken(i, ..), .. rest] => {
self.update_view(rest)
Some(i.to_string())
}
[BooleanToken(b, ..), .. rest] => {
self.update_view(rest)
Some(if b { "true" } else { "false" })
}
[FloatToken(_, raw~, ..), .. rest] => {
// Use the raw source text so that tokens like -10e-1 (which the
// lexer parses as a float) can still serve as bare keys.
self.update_view(rest)
Some(raw)
}
[DateTimeToken(LocalDate(s), ..), .. rest] => {
// Only LocalDate (YYYY-MM-DD) is valid as a bare key — it only
// contains digits and dashes. Other datetime variants contain
// colons, T, Z, or offsets which are not bare key characters.
self.update_view(rest)
Some(s)
}
_ => None
}
}
///|
/// Parse a primary value (string, number, boolean)
fn Parser::parse_value(
self : Self,
skip_newlines? : Bool = false,
) -> TomlValue raise {
match self.view(skip_newlines~) {
[StringToken(s, ..), .. rest] => {
self.update_view(rest)
TomlString(s)
}
[IntegerToken(i, ..), .. rest] => {
self.update_view(rest)
TomlInteger(i)
}
[FloatToken(f, raw~, ..), .. rest] => {
// Reject overflow: non-finite values are only valid with the
// canonical spellings inf/nan/+inf/-inf/+nan/-nan.
if (f.is_inf() || f.is_nan()) &&
raw != "inf" &&
raw != "-inf" &&
raw != "+inf" &&
raw != "nan" &&
raw != "-nan" &&
raw != "+nan" {
self.error("Invalid float: \{raw}")
}
self.update_view(rest)
TomlFloat(f)
}
[BooleanToken(b, ..), .. rest] => {
self.update_view(rest)
TomlBoolean(b)
}
[DateTimeToken(dt, ..), .. rest] => {
self.update_view(rest)
TomlDateTime(dt)
}
// TOML Array
[LeftBracket, .. rest] => {
self.update_view(rest)
self.parse_array()
}
// TOML Inline Table
[LeftBrace, .. rest] => {
self.update_view(rest)
self.parse_inline_table()
}
[Identifier("inf", ..), .. rest] => {
self.update_view(rest)
TomlFloat(1.0 / 0.0) // positive infinity
}
[Identifier("nan", ..), .. rest] => {
self.update_view(rest)
TomlFloat(0.0 / 0.0) // NaN
}
_ => self.error("Expected value")
}
}
///|
/// Parse an array [1, 2, 3]
fn Parser::parse_array(self : Self) -> TomlValue raise {
let values = []
while true {
if self.view(skip_newlines=true) is [RightBracket, .. rest] {
self.update_view(rest)
return TomlArray(values)
}
// self.skip_newlines() // here Newline is allowed
values.push(self.parse_value(skip_newlines=true))
match self.view(skip_newlines=true) {
[Comma, .. rest] => self.update_view(rest)
[RightBracket, .. rest] => {
self.update_view(rest)
break
}
_ => self.error("Expected ',' or ']' in array")
}
}
TomlArray(values)
}
///|
/// Parse an inline table {key = value, key2 = value2}
/// No trailing comma allowed unlike Array
fn Parser::parse_inline_table(self : Parser) -> TomlValue raise {
let table = Map([])
self.skip_newlines() // TOML 1.1: allow newlines in inline tables
if self.view() is [RightBrace, .. rest] {
self.update_view(rest)
table[inline_table_marker] = TomlBoolean(true)
return TomlTable(table)
}
while true {
self.skip_newlines()
// Parse dotted key
let key_path = self.parse_dotted_key()
// Expect =
match self.view() {
[Equals, .. rest] => self.update_view(rest)
_ => self.error("Expected '='")
}
// Parse value
let value = self.parse_value()
set_dotted_key_value(table, key_path, value) catch {
error => self.error("\{error}")
}
self.skip_newlines()
match self.view() {
[Comma, .. rest] => {
self.update_view(rest)
self.skip_newlines()
// Trailing comma: check for closing brace after comma
if self.view() is [RightBrace, .. rest2] {
self.update_view(rest2)
break
}
}
[RightBrace, .. rest] => {
self.update_view(rest)
break
}
_ => self.error("Expected ',' or '}' in inline table")
}
}
table[inline_table_marker] = TomlBoolean(true)
TomlTable(table)
}
///|
/// Parse a table path: section.subsection.key
fn Parser::parse_table_path(self : Parser) -> Array[String] raise {
self.parse_dotted_key()
}
///|
fn[A] Parser::error(self : Parser, msg : String) -> A raise {
match self.view() {
[token, ..] => fail("\{msg} at \{@debug.to_string(token.loc())}")
[] => fail("\{msg} at the end of input")
}
}
///|
/// Parse a dotted key path: key.subkey.subsubkey
fn Parser::parse_dotted_key(self : Parser) -> Array[String] raise {
let path = Array::new()
// Parse first key - handle the case where the tokenizer saw a float like "1.2"
match self.view() {
[FloatToken(_, raw~, ..), .. rest] => {
// Use the raw source text so dotted numeric keys like "1.2" split
// correctly and tokens like -10e-1 are preserved verbatim.
self.update_view(rest)
match raw.split_once(".") {
Some((left, right)) => {
path.push(left.to_owned())
path.push(right.to_owned())
}
None => path.push(raw)
}
}
_ =>
match self.try_parse_single_key() {
Some(k) => path.push(k)
None => self.error("Expected key")
}
}
// Parse additional keys separated by dots
let next_view = for view = self.view() {
match view {
[Dot(..), .. rest] => {
self.update_view(rest)
// Check for float token (e.g. 1.2 → keys "1" and "2")
match self.view() {
[FloatToken(_, raw~, ..), .. rest2] => {
self.update_view(rest2)
let float_str = raw
match float_str.split_once(".") {
Some((left, right)) => {
path.push(left.to_owned())
path.push(right.to_owned())
}
None => path.push(float_str)
}
}
_ =>
match self.try_parse_single_key() {
Some(k) => path.push(k)
None => self.error("Expected key after dot")
}
}
continue self.view()
}
rest => break rest
}
}
self.update_view(next_view)
path
}
///|
/// Parse a key-value pair with dotted key support: key.subkey = value
fn Parser::parse_key_value(self : Parser) -> (Array[String], TomlValue) raise {
// Parse dotted key
let key_path = self.parse_dotted_key()
// Expect =
match self.view() {
[Equals, .. rest] => self.update_view(rest)
_ => self.error("Expected '='")
}
// Parse value
let value = self.parse_value()
(key_path, value)
}
///|
/// Parse a TOML document with support for tables
/// Marker key used to track explicitly defined tables.
/// This is removed before returning the final result.
let table_defined_marker : String = "\u0000__defined__"
///|
/// Marker key for inline tables (immutable after definition).
let inline_table_marker : String = "\u0000__inline__"
///|
/// Marker for tables defined via [table] headers (distinct from dotted-key defined).
let header_defined_marker : String = "\u0000__header__"
///|
/// Parse a TOML document and return its root table as a `TomlValue`.
///
/// On success the result is always a `TomlTable` whose contents reflect the
/// document's top-level keys, `[section]` headers, and `[[array]]` of
/// tables. Standard TOML 1.0 plus 1.1 features (optional seconds, `\xHH`
/// escapes, inline-table newlines) are accepted.
///
/// On any lexical or syntactic error, `parse` raises with a message
/// containing the source location. Wrap the call in `try?` to receive a
/// `Result[TomlValue, Error]` instead.
pub fn parse(input : String) -> TomlValue raise {
let tokens = @tokenize.tokenize(input)
let parser = Parser::Parser(tokens)
let main_table = Map([])
for current_table = main_table {
parser.skip_newlines()
match parser.view() {
[EOF, ..] => break
[LeftBracket(loc=loc1), LeftBracket(loc=loc2), .. rest] => {
if !loc1.adjacent(loc2) {
parser.error("Invalid table header: space between '[' and '['")
}
parser.update_view(rest)
let table_path = parser.parse_table_path()
match parser.view() {
[RightBracket(loc=rl1), RightBracket(loc=rl2), .. rest] => {
if !rl1.adjacent(rl2) {
parser.error("Expected ']]' (no space between brackets)")
}
parser.update_view(rest)
}
_ => parser.error("Expected ']]'")
}
// Validate newline/EOF/comment after ]]
match parser.view() {
[EOF, ..] | [Newline, ..] => ()
_ => parser.error("Expected newline or end of file after ']]'")
}
// Create or append to the array of tables structure
let current_table = create_array_of_tables(main_table, table_path) catch {
error => parser.error("\{error}")
}
continue current_table
}
[LeftBracket, .. rest] => {
parser.update_view(rest)
let table_path = parser.parse_table_path()
match parser.view() {
[RightBracket, .. rest3] => parser.update_view(rest3)
_ => parser.error("Expected ']'")
}
// Validate newline/EOF/comment after ]
match parser.view() {
[EOF, ..] | [Newline, ..] => ()
_ => parser.error("Expected newline or end of file after ']'")
}
// Create or get the nested table structure
let current_table = create_nested_table(main_table, table_path) catch {
error => parser.error("\{error}")
}
// Check if this table was already explicitly defined
if current_table.contains(table_defined_marker) {
let path_str = table_path.join(".")
parser.error("Duplicate table definition: [\{path_str}]")
}
current_table[table_defined_marker] = TomlBoolean(true)
current_table[header_defined_marker] = TomlBoolean(true)
continue current_table
}
_ => {
// Parse key-value pair
let (key_path, value) = parser.parse_key_value()
// Mark implicit tables when inside an explicitly defined [table] section
let in_defined_section = current_table.contains(table_defined_marker)
set_dotted_key_value(
current_table,
key_path,
value,
mark_implicit=in_defined_section,
) catch {
error => parser.error("\{error}")
}
// Validate that key-value pair is properly terminated
match parser.view() {
[EOF, ..] => () // EOF is valid
[Newline, ..] => () // Newline is valid (comments are already skipped by tokenizer)
_ =>
parser.error("Expected newline or end of file after key-value pair")
}
}
}
}
cleanup_markers(main_table)
TomlTable(main_table)
}
///|
/// Remove internal marker keys from all tables recursively.
fn cleanup_markers(table : Map[String, TomlValue]) -> Unit {
table.remove(table_defined_marker)
table.remove(inline_table_marker)
table.remove(header_defined_marker)
for _, value in table {
cleanup_value_markers(value)
}
}
///|
fn cleanup_value_markers(value : TomlValue) -> Unit {
match value {
TomlTable(sub) => cleanup_markers(sub)
TomlArray(arr) =>
for item in arr {
cleanup_value_markers(item)
}
_ => ()
}
}