TOML
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 value | Kind | Read with |
|---|---|---|
"Rux", 'Rux' | text | AsText() |
8080, 0x1F90 | integer | AsInteger(@x), into an int64 |
0.75, 8080.0 | float | AsFloat(@x), into a float64 |
true | boolean | AsBoolean(@x) |
2026-10-04 | local date | AsDate(@x), into a Date |
["web", "api"] | array | Length(), At(i) |
[server], { … } | table | Find(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.
// 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
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.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().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").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
- Write the port in hexadecimal,
port = 0x1F90. What doesAsIntegerread? - Add
debug = trueto the[server]table and read it withAsBoolean. (The array's length,[8], has to grow too.) - Open
[server]a second time further down the document. Is that refused like a repeated key? - 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
AsDatefills in - Fallible main —
Mainreturning! TextError - Manifest — the
Rux.tomlfile, the TOML you write most often