Files
diviocityandAlexander Medvednikov 6bab2bc8f0 toml: parse repeated hexadecimal digits without stripping prefixes (#31225)
* fix: allow toml parsing of HEX value with repeating b characters

* fix: allow toml parsing of HEX value with repeating b characters

* toml: preserve invalid numeric rejection beside repeated hex digits

---------

Co-authored-by: Alexander Medvednikov <alexander@medvednikov.com>
2026-10-27 08:26:32 +03:00
..
2026-09-17 10:47:56 +03:00

Description

toml is a fully fledged TOML v1.0.0 compatible parser written in pure V. The module is tested against the official compliance tests.

Usage

Parsing files or strings containing TOML is easy.

Simply import the toml module and do:

doc1 := toml.parse_text(<string content>) or { panic(err) }
doc2 := toml.parse_file(<file path>) or { panic(err) }

Hexadecimal integers use the 0x prefix and allow repeated digits, including b: color = 0xbbbbbb. Binary and octal literals still accept only digits valid for their base.

Parser AST and table views

toml.ast.DateTime keeps its public date and time components, including their text and source positions, alongside the complete date-time text.

The public Parser.find_table, find_sub_table and find_in_table methods return !&map[string]ast.Value. Each reference has a heap header sharing the table's storage. Insertions, deletions and reservations through a view are visible through other views. Growing the parent table does not invalidate the reference: it holds no pointer into the parent's inline values. Keep shared table storage alive until all views are finished; use .clone() when an independent table is needed.

Parser.double_array_of_tables_contents(target_key) returns a one-element []ast.Value containing the parsed table body. It leaves the following table header available for the caller to continue parsing.

Example

Here we parse the official TOML example and print out some values.

import toml

const toml_text = '# This is a TOML document.

title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates

[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
connection_max = 5000
enabled = true

[servers]

  # Indentation (tabs and/or spaces) is allowed but not required
  [servers.alpha]
  ip = "10.0.0.1"
  dc = "eqdc10"

  [servers.beta]
  ip = "10.0.0.2"
  dc = "eqdc10"

[clients]
data = [ ["gamma", "delta"], [1, 2] ]

# Line breaks are OK when inside arrays
hosts = [
  "alpha",
  "omega"
]'

fn main() {
	doc := toml.parse_text(toml_text) or { panic(err) }
	title := doc.value('title').string()
	println('title: "${title}"')
	ip := doc.value('servers.alpha.ip').string()
	println('Server IP: "${ip}"')
}

Struct conversion

toml.decode[T] decodes TOML text into a struct and toml.encode[T] encodes a struct as TOML. A field maps to the key of the same name; @[toml: <name>] changes the key and @[skip] ignores the field. Fields missing from the document keep their default values. A from_toml(toml.Any) or to_toml() string method on T replaces the generic conversion.

Partial tables preserve the existing defaults inside a present optional struct. When that optional field is none, a table initializes it from the struct's own defaults.

TOML has no struct embedding, so the fields of an embedded struct are read from and written to the same table as the fields of the embedding struct. When decoding, a table named after the embedded struct (e.g. [Db]) is accepted too and takes precedence. Embedded toml.Date, toml.Time and toml.DateTime are scalars, stored under their type name (e.g. Date = 2026-09-30).

Array elements and map values may be scalars, enums, structs, arrays or maps, at any nesting depth, so []JobTitle, map[string][]int and [][]map[string]Item all decode. An element that does not fit the element type, such as a number where a struct is expected, is skipped. An enum element must be an integer, so a string, boolean, float, array or table in an enum position is skipped rather than coerced; a plain enum field still follows Any.int(), which reads a non-numeric value as 0.

import toml

struct Db {
	host string
	port int
}

struct Config {
	Db
	name   string @[toml: app_name]
	secret string @[skip]
}

config := toml.decode[Config]('app_name = "app"\nhost = "localhost"\nport = 5432')!
assert config.host == 'localhost' // promoted from Db
assert config.name == 'app'
encoded := toml.encode(config)
assert encoded == 'host = "localhost"\nport = 5432\napp_name = "app"'
assert toml.decode[Config](encoded)! == config

String values are encoded as TOML basic strings with quotes, backslashes, and control characters escaped. Non-ASCII text is preserved.

Map fields support string, boolean, and integer keys up to 64 bits. TOML keys are strings; integer keys are parsed as decimal numbers with the destination type's range, including full-width i64 and u64 keys. Invalid or out-of-range keys are skipped, as are out-of-range narrow integer values. Boolean keys must be true or false. Other map key types are rejected at compile time. Nested maps retain their own key types.

Value retrieval

The toml module supports easy retrieval of values from TOML documents by using a small and simple query syntax as argument to the value() function.

Keys in map entries are denoted by . and array entries uses [<int>]. Quoted keys are also supported by using the delimiters " or '.

doc.value('table.array[0].a."b.c"')

To query for a value that might not be in the document you can use the .default_to(...) function to provide a default value.

For cases where a default value might not be appropriate or to check if a value exists you can use doc.value_opt('query')! instead.

import toml

const toml_text = '
val = true

[table]
array = [
	{ a = "A" },
	{ b = "B" }
]
'

doc := toml.parse_text(toml_text) or { panic(err) }

assert doc.value('val').bool() == true
assert doc.value('table.array[0].a').string() == 'A'

// Provides a default value
assert doc.value('non.existing').default_to(false).bool() == false

// Check if value exist
// doc.value_opt('should.exist') or { ... }
// or
if value := doc.value_opt('table.array[1].b') {
	assert value.string() == 'B'
}

// You can pass parts of the TOML document around
// and still use .value()/.value_opt() to get the values
arr := doc.value('table.array')
assert arr.value('[1].b').string() == 'B'

Conversion

Any TOML value can be converted to a V equivalent type.

TOML values are represented as the toml.Any sum-type that can be converted to a V type.

TOML value V conversion (via toml.Any)
val = "Basic string" .string()
val = 'Literal string' .string()
val = true .bool()
val = 1979-05-27T07:32:00Z .datetime() (toml.DateTime)
val = 1979-05-27 .date() (toml.Date)
val = 07:32:59 .time() (toml.Time)
val = 3.14 .f32() / .f64()
val = 100 .int() / .i64() / .u64()

Read more about values in the TOML specification.

TOML to JSON

The toml.to module supports easy serialization of any TOML to JSON.

import toml
import toml.to

const toml_text = '
val = true
[table]
array = [
	{ a = "A" },
	{ b = "B" }
]
'

doc := toml.parse_text(toml_text) or { panic(err) }
assert to.json(doc) == '{ "val": true, "table": { "array": [ { "a": "A" }, { "b": "B" } ] } }'

toml.scanner.Scanner.peek(0) returns the byte at the current scanner position without advancing it, or end_of_text when the position is at the end of the input. Negative offsets look behind that position. An offset outside the input returns end_of_text.