Data formats · Lesson 21.4

TOML

Source
Parse a TOML document, read integers, floats, dates and arrays from its tables, and see a bad document refused by line and column.

TOML is a configuration format — the one every Rux.toml in this course is written in. It is made for people to write by hand: one key = value per line, comments with #, and [name] to open a section. And unlike JSON, its values have real types. An integer is not a float even when the float is whole, and a date is a date, not a string that happens to look like one.

This lesson reads a small settings document with the Toml package. Reading the result works the same way as in JSON: find a key, ask what kind of value it holds, take it out.

import Text::{ String, StringBuilder, StringView, TextError };
import Time::Date;
import Toml::{ TomlParse, TomlValue };

The document

TOML is line-based, so the program writes the document as one literal per line:

let good: char8[..][8] = [
    "name = \"Rux\"",
    "released = 2026-10-04",
    "",
    "[server]",
    "port = 8080",
    "ratio = 0.75",
    "tags = [\"web\", \"api\"]",
    "# A comment, which the parser skips."
];
let document = Lines(allocator, good)?;

and Lines joins them, each followed by a newline, with a StringBuilder. Unescaped, the text it builds is:

name = "Rux"
released = 2026-10-04

[server]
port = 8080
ratio = 0.75
tags = ["web", "api"]
# A comment, which the parser skips.

name and released belong to the top-level table; [server] opens a nested table that holds the rest.

Joining can fail — the builder needs memory — so Lines returns String ! TextError, and Main is a fallible Main, func Main() -> ! TextError, so that ? can pass the failure on.

Parsing

TomlParse(allocator, text) -> TomlValue ! TomlParseFailure

The result is the top-level table as a TomlValue tree, or no tree at all — only the reason, and where it was found:

func Load(allocator: Allocator, text: char8[..]) {
    match TomlParse(allocator, text) {
        .Success(root) => Inspect(root),
        .Failure(failure) =>
            PrintLine("refused at line {}, column {}: {}", failure.line, failure.column, failure)
    }
}

A TOML document is written in lines, so a TomlParseFailure reports a line and a column rather than only a byte — the place a person would look in an editor. Load takes a char8[..], so the String from Lines is passed as document.View().Bytes().

Reading values

Key looks a key up in a table. Like JSON's Find, it answers a pointer that is null when the key is missing:

func Key(table: &TomlValue, key: char8[..]) -> *TomlValue {
    return table.Find(StringView::FromValidated(key));
}

Each kind has its own question, answered through an out-parameter, and each answers only for its own kind:

let server = Key(root, "server");
var port: int64 = 0;
var ratio = 0.0;
let portIsInteger = (*Key(*server, "port")).AsInteger(@port);
let portIsFloat = (*Key(*server, "port")).AsFloat(@ratio);
PrintLine("port      {} (integer: {}, float: {})", port, portIsInteger, portIsFloat);

8080 is an integer, so AsInteger says true and AsFloat says false. In JSON both would be "a number"; in TOML the spelling decides the type, and 8080.0 would be a float.

TOML valueKindRead with
"Rux", 'Rux'textAsText()
8080, 0x1F90integerAsInteger(@x), into an int64
0.75, 8080.0floatAsFloat(@x), into a float64
truebooleanAsBoolean(@x)
2026-10-04local dateAsDate(@x), into a Date
["web", "api"]arrayLength(), At(i)
[server], { … }tableFind(key), Length()

A date comes out as the Date from the Date lesson, ready to compare or do arithmetic on:

var released = Date { year: 1970, month: 1, day: 1 };
(*Key(root, "released")).AsDate(@released);
PrintLine("released  {}", released);

Arrays are walked by index, and a key that is not there is simply a null pointer:

let tags = Key(*server, "tags");
for i in 0..(*tags).Length() {
    PrintLine("tags[{}]   {}", i, (*(*tags).At(i)).AsText());
}
PrintLine("debug     present: {}", Key(*server, "debug") != null);

Refusals

let twice: char8[..][3] = ["[server]", "port = 8080", "port = 9090"];
let repeated = Lines(allocator, twice)?;
Load(allocator, repeated.View().Bytes());
Load(allocator, "released = 2026-13-04");

In TOML a key given twice in one table is an error, not a quiet overwrite — two values for one setting is almost always a mistake, and refusing it is the only way to make sure nobody acts on the wrong one. And a value must be spelled the way its type is: 2026-13-04 is shaped like a date, but there is no thirteenth month, so it is not the shape TOML defines for any value.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// TOML is a configuration format — the one every `Rux.toml` in this course is written in. A
// document is a table of keys and values, `[name]` opens a nested table, and unlike JSON the
// values have real types: an integer is not a float even when the float is whole, and a date is
// a date rather than a string that looks like one.
//
//     TomlParse(allocator, text) -> TomlValue ! TomlParseFailure
//
// The result is a `TomlValue` tree, read the same way as a JSON one: `Find` answers a pointer
// that is null when the key is missing, and `AsInteger`, `AsFloat`, `AsDate` and the rest
// answer `false` when the value is some other kind. A refused document gives no tree at all,
// only the reason and where it was found, by line and column.
import Allocator::{ Allocator, SystemAllocator };
import Io::PrintLine;
import Text::{ String, StringBuilder, StringView, TextError };
import Time::Date;
import Toml::{ TomlParse, TomlValue };

// A TOML document is lines, so it is written here one literal per line and joined.
func Lines(allocator: Allocator, lines: char8[..][..]) -> String ! TextError {
    var builder = StringBuilder(allocator);
    for i in 0..lines.length {
        builder.Append(lines[i])?;
        builder.Append("\n")?;
    }
    return builder.IntoString();
}

func Key(table: &TomlValue, key: char8[..]) -> *TomlValue {
    return table.Find(StringView::FromValidated(key));
}

func Inspect(root: &TomlValue) {
    PrintLine("name      {}", (*Key(root, "name")).AsText());

    // The same question asked of an integer and a float. Each answers only its own kind.
    let server = Key(root, "server");
    var port: int64 = 0;
    var ratio = 0.0;
    let portIsInteger = (*Key(*server, "port")).AsInteger(@port);
    let portIsFloat = (*Key(*server, "port")).AsFloat(@ratio);
    PrintLine("port      {} (integer: {}, float: {})", port, portIsInteger, portIsFloat);
    (*Key(*server, "ratio")).AsFloat(@ratio);
    PrintLine("ratio     {}", ratio);

    var released = Date { year: 1970, month: 1, day: 1 };
    (*Key(root, "released")).AsDate(@released);
    PrintLine("released  {}", released);

    let tags = Key(*server, "tags");
    for i in 0..(*tags).Length() {
        PrintLine("tags[{}]   {}", i, (*(*tags).At(i)).AsText());
    }
    PrintLine("debug     present: {}", Key(*server, "debug") != null);
}

func Load(allocator: Allocator, text: char8[..]) {
    match TomlParse(allocator, text) {
        .Success(root) => Inspect(root),
        .Failure(failure) =>
            PrintLine("refused at line {}, column {}: {}", failure.line, failure.column, failure)
    }
}

func Main() -> ! TextError {
    var system = SystemAllocator();
    let allocator: Allocator = system;

    let good: char8[..][8] = [
        "name = \"Rux\"",
        "released = 2026-10-04",
        "",
        "[server]",
        "port = 8080",
        "ratio = 0.75",
        "tags = [\"web\", \"api\"]",
        "# A comment, which the parser skips."
    ];
    let document = Lines(allocator, good)?;
    Load(allocator, document.View().Bytes());

    // Two refusals. A key given twice in one table is an error in TOML, not a quiet overwrite,
    // and a value must be spelled the way its type is: there is no thirteenth month.
    PrintLine("");
    let twice: char8[..][3] = ["[server]", "port = 8080", "port = 9090"];
    let repeated = Lines(allocator, twice)?;
    Load(allocator, repeated.View().Bytes());
    Load(allocator, "released = 2026-13-04");
}

Besides Io, its Rux.toml lists Allocator, Text, Time and Toml under [Dependencies].

Run it

cd Examples/DataFormats/Toml
rux run
name      Rux
port      8080 (integer: true, float: false)
ratio     0.75
released  2026-10-04
tags[0]   web
tags[1]   api
debug     present: false

refused at line 3, column 12: key given a value twice in the same table
refused at line 1, column 17: not the shape TOML defines

Common mistakes

Ignoring the answer of AsInteger.
The out-parameter is written only when the answer is true. Change port = 8080 to port = 8080.0 and the program prints port 0 (integer: false, float: true): the value is a float now, and port kept its starting value. Check what the conversion answers before using the result.
Passing a String where text is wanted.
Load and TomlParse take a char8[..]. Load(allocator, document) fails with error: argument 2 to 'Load' has type 'String', but parameter 'text' requires 'char8[..]'. Pass document.View().Bytes().
A pointer where a reference is wanted.
Key answers a pointer, but takes its table as a reference. Key(server, "tags") fails with error: argument 1 to 'Key' has type '*TomlValue', but parameter 'table' requires '&TomlValue'. Dereference it: Key(*server, "tags").
Reading through a missing key.
Key returns null for a key that is not there, and reading through null crashes the program. The lesson reads server, port and the rest unchecked only because its document is fixed; for a real configuration file, check each pointer — or treat a missing key as "use the default".

Try it yourself

  1. Write the port in hexadecimal, port = 0x1F90. What does AsInteger read?
  2. Add debug = true to the [server] table and read it with AsBoolean. (The array's length, [8], has to grow too.)
  3. Open [server] a second time further down the document. Is that refused like a repeated key?
  4. Add a key with a value of your own and read it back. Then change the spelling of its value so the parser refuses it, and read the line and column.

Learn more

  • JSON — the same way of reading a tree, for a format without types
  • TOML write — changing the tree and writing it back
  • Date — the type AsDate fills in
  • Fallible main — Main returning ! TextError
  • Manifest — the Rux.toml file, the TOML you write most often