toml

    A TOML v1.0.0 parser and serializer for MoonBit with full toml-test compliance.

    toml
    parser
    config
    serialization
    Download zip
    Author
    Version
    0.1.1
    License
    MIT
    Last updated
    19 hours ago
    Downloads
    5

    Dependencies

    #moonbit-toml

    A TOML v1.0.0 parser and serializer for MoonBit, with full compliance against the official toml-test v1.0.0 suite.

    Published on mooncakes.io as sa2360/toml.

    moon add sa2360/toml

    #Highlights

    • 100% toml-test v1.0.0 compliance — all 96 valid and 185 invalid cases of the official suite pass. The suite is embedded in the repo: moon test alone runs all 275 conformance cases plus unit tests.
    • Round-trip guarantee — every valid suite case is additionally parsed → encoded → re-parsed and required to be structurally equal.
    • Order-preserving tables — key definition order is kept, so encoding is stable and diffs are readable.
    • Precise errors — every parse failure carries a 1-based line/column and a structured cause (UnexpectedChar, DuplicateKey, TableConflict, …).
    • Strict UTF-8 — the bundled toml2json CLI validates UTF-8 byte sequences (rejecting surrogates, overlong forms and out-of-range code points).
    • Pure MoonBit, no FFI. Works on wasm, wasm-gc, js and native backends.

    #Usage

    // moon.mod: import { "sa2360/toml" }

    #Parsing

    let doc = @toml.parse("title = \"Example\"\n[owner]\nname = \"Tom\"")
    doc.get_string("title") // Some("Example")
    doc.get_path("owner.name") // Some(Str("Tom")) — dotted path lookup
    doc.get_int("port") // Option[Int64]
    doc.get_double / get_bool / get_array / get_table / get_datetime

    parse raises a structured ParseError; err.message() renders e.g. line 3, column 5: duplicate key 'apple'. Single values can be decoded with @toml.parse_value("1_000").

    #Serializing

    let text = @toml.encode(doc) // full document ([table] sections, [[array of tables]])
    let inline = @toml.encode_value(v) // a single value in inline form

    encode writes sub-tables as [a.b] sections and arrays of tables as [[a.b]], preserving first-definition key order. The output round-trips.

    #JSON interop

    let json = value.to_json() // -> moonbitlang/core Json
    let text = value.to_json_string() // compact JSON string
    let back = @toml.Value::from_json(json) // JSON -> TOML (raises on null)

    to_json: integers keep full 64-bit precision through their textual representation, datetimes become strings in canonical TOML notation, and inf / -inf / nan become the strings "inf" / "-inf" / "nan" (JSON has no representation for them). Value implements the standard ToJson, Eq and Debug traits.

    from_json conventions: JSON null raises FromJsonError::JsonNull; integral numbers within 64-bit range become integers (so JSON 2.0 reads back as the integer 2 — JSON cannot distinguish them); JSON strings never become datetimes.

    A runnable demo lives in cmd/example: moon run cmd/example.

    #Value model

    pub(all) enum Value {
    Str(String); Int(Int64); Float(Double); Bool(Bool)
    Datetime(Datetime); Array(Array[Value]); Table(Table)
    }

    TOML integers are 64-bit signed. Datetimes keep their four forms (datetime, datetime-local, date-local, time-local) as components; sub-second precision is stored as nanoseconds (input beyond 9 digits is truncated). Value::equal compares structurally: tables order-insensitively, NaN equals NaN, Z equals +00:00.

    #The CLI

    moon build --target native cmd/toml2json _build/native/debug/build/cmd/toml2json/toml2json.exe config.toml

    Prints the document as toml-test protocol tagged JSON ({"type": "integer", "value": "42"}), exiting non-zero with a positioned error message on invalid input.

    #Testing

    moon test # 316 tests: unit + embedded toml-test suite + doc examples moon test --target native # same suite on the native backend moon bench # parse / encode / to_json benchmarks (+ core JSON reference) moon build --target native cmd/toml2json python scripts/run_toml_test.py # official protocol runner over the CLI (281 cases)

    Nesting of arrays and inline tables is bounded at 200 levels (MAX_NESTING_DEPTH): hostile inputs fail with a positioned error instead of exhausting the stack.

    On a 2026 laptop (wasm backend), parsing a ~1400-line / 200-section synthetic document takes about 1.8 ms, re-encoding it about 0.5 ms. The same document parsed by moonbitlang/core's JSON parser takes ~0.5 ms — JSON is a much simpler grammar (and the core parser is heavily optimized), which is the honest reference point; for configuration-sized inputs the difference is negligible.

    The conformance cases are generated from the vendored suite in toml-test-tests/ (Apache-2.0) by scripts/gen_conformance.py, which converts toml-test's expected tagged JSON into MoonBit value literals at generation time.

    #Layout

    value.mbt Value / Table / Datetime model, accessors, equality error.mbt Position, ParseErrorData, ParseError scanner.mbt cursor, whitespace/comment/newline handling, char classes strings.mbt the four string kinds and escape validation numbers.mbt integers, floats, datetimes (with range checks) parser.mbt document/table/keyval/array/inline-table + definition rules encode.mbt serializer (documents and inline values) json.mbt JSON interop (to_json, ToJson impl) cmd/toml2json/ native CLI emitting toml-test tagged JSON cmd/example/ runnable demo: parse, read, re-encode, convert to JSON scripts/ conformance generator + CLI runner toml-test-tests/ vendored official suite (v1.0.0)

    #License

    MIT — see LICENSE. The vendored toml-test-tests/ suite is Apache-2.0, Copyright the toml-test authors.


    #moonbit-toml(中文说明)

    面向 MoonBit 的 TOML v1.0.0 解析与序列化库,通过官方 toml-test v1.0.0 全量合规测试(96 个 valid + 185 个 invalid 用例,100%)。已发布到 mooncakes.io:moon add sa2360/toml。

    #特性

    • 官方套件全量合规:测试集已内嵌进仓库,moon test 一条命令即可运行全部 275 个合规用例与单元测试(共 299 个);
    • 往返保证:每个 valid 用例都会执行 解析 → 序列化 → 再解析,要求结果结构相等;
    • 保序表:保留键的首定义顺序,序列化输出稳定;
    • 精确错误:每个解析错误都带 1-based 行号/列号与结构化原因(如 DuplicateKey、TableConflict);
    • 严格 UTF-8:CLI 校验字节序列(拒绝代理区、超长编码、越界码点);
    • 纯 MoonBit 实现,无 FFI,wasm / native 双端行为一致。

    #快速上手

    let doc = @toml.parse("title = \"Example\"\n[owner]\nname = \"Tom\"")
    doc.get_string("title") // Some("Example")
    doc.get_path("owner.name") // Some(Str("Tom"))

    let text = @toml.encode(doc) // 序列化回 TOML 文档
    let json = @toml.Value::Table(doc).to_json_string() // 转 JSON(整数精度无损)

    错误处理采用 MoonBit 惯用的 raise 风格:

    let doc = @toml.parse(input) catch {
    err => println(err.message()) // 例如:line 3, column 5: duplicate key 'apple'
    }

    • 可解释的 API 文档:核心函数带有由 moon check/moon test 校验的文档示例(```mbt check),mooncakes 文档页直接渲染;
    • Value 实现了标准 Eq / Debug trait(表序无关、NaN 相等、Z 等价 +00:00),可直接用于 @debug.assert_eq;

    可运行的示例在 cmd/example:moon run cmd/example。

    #测试

    moon test # 316 个测试:单元测试 + 内嵌 toml-test 全量套件 + 文档示例 moon test --target native # 同一套件在 native 后端 moon bench # parse / encode / to_json 基准(含 core JSON 参照) moon build --target native cmd/toml2json python scripts/run_toml_test.py # 官方协议 runner(281 个用例,含字节级 UTF-8 用例)

    数组与内联表的嵌套深度上限为 200 层(MAX_NESTING_DEPTH),恶意输入会得到带行列号的报错而不是栈溢出崩溃。

    性能参考(wasm 后端,约 1400 行 / 200 节合成文档):parse 约 1.8ms,encode 约 0.5ms;同一文档 core 的 JSON 解析器约 0.5ms——JSON 语法简单得多且核心库深度优化,这是如实的参照点,配置文件体量下差异可忽略。

    #已知取舍

    • 日期时间中小数秒超过 9 位时按纳秒截断(输出侧尾部零会被规范化);
    • Table::get_path 按字面 . 分段,无法寻址“名字本身带点且需要引号”的键;
    • 多行字符串中的 CRLF 统一归一化为 LF(TOML 规范明确允许实现自行归一化);
    • 日期时间中秒不可省略(RFC 3339 的 partial-time 允许省略仅适用于纯时间值,如 07:32)。

    #License

    MIT(见 LICENSE);内嵌的 toml-test-tests/ 测试集为 Apache-2.0,版权归 toml-test 作者所有。

    FromJsonError

    pub suberror FromJsonError {
    JsonNull
    } derive(Eq,
    Debug
    )

    Error raised by Value::from_json for JSON values that have no TOML representation.

    ParseError

    pub suberror ParseError {
    ParseError(ParseErrorData)
    } derive(
    Debug
    )

    The error type raised by parse and parse_value.
    impl Show for ParseError

    ParseError::message

    fn ParseError::message(self : ParseError) -> String

    A short human readable description of the error, e.g. line 3, column 5: duplicate key 'apple'.

    DateParts

    pub(all) struct DateParts {
    year : Int
    month : Int
    day : Int
    } derive(Eq,
    Debug
    )

    Date part of a TOML datetime: a calendar date. year is 0..=9999, month 1..=12, day 1..=31.

    Datetime

    pub(all) struct Datetime {
    date : DateParts?
    time : TimeParts?
    offset : Int
    has_offset : Bool
    } derive(Eq,
    Debug
    )

    A TOML datetime value. Depending on which of date / time are present and whether an offset was written, this is one of the four TOML datetime forms:

    • offset date-time (date + time + has_offset)
    • local date-time (date + time, no offset)
    • local date (date only)
    • local time (time only)

    offset is in minutes east of UTC (e.g. -07:00 is -420).

    Datetime::kind_name

    fn Datetime::kind_name(self : Datetime) -> String

    The TOML type name of the datetime form: datetime, datetime-local, date-local or time-local.

    ParseErrorData

    pub(all) enum ParseErrorData {
    UnexpectedChar(Position, Char)
    UnexpectedEof(Position)
    UnterminatedString(Position)
    InvalidEscape(Position, Char)
    InvalidUnicodeEscape(Position, String)
    InvalidNumber(Position, String)
    InvalidDatetime(Position, String)
    InvalidValue(Position, Char)
    DuplicateKey(Position, String)
    TableConflict(Position, String)
    InvalidSyntax(Position, String)
    } derive(Eq,
    Debug
    )

    Structured description of a TOML parse failure.

    Position

    pub(all) struct Position {
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    A position (1-based line and column) inside a TOML document.

    Table

    pub struct Table {
    // private fields
    }

    An ordered TOML table. Table preserves the order in which keys were first defined (see keys), and provides lookup by key and by dotted path.

    Table::contains

    fn Table::contains(self : Table, key : String) -> Bool

    Returns true if the table contains key.

    Table::from_array

    fn Table::from_array(entries : Array[(String, Value)]) -> Table

    Builds a table from an array of key/value pairs, keeping the given order. Later entries with an already present key replace the earlier value (but keep the position of the first occurrence).

    Table::get

    fn Table::get(self : Table, key : String) -> Value?

    Returns the value stored under key, or None.

    Table::get_array

    fn Table::get_array(self : Table, key : String) -> Array[Value]?

    Typed accessor on tables: the array under key, if present and an array.

    Table::get_bool

    fn Table::get_bool(self : Table, key : String) -> Bool?

    Typed accessor on tables: the boolean under key, if present and a boolean.

    Table::get_datetime

    fn Table::get_datetime(self : Table, key : String) -> Datetime?

    Typed accessor on tables: the datetime under key, if present and a datetime.

    Table::get_double

    fn Table::get_double(self : Table, key : String) -> Double?

    Typed accessor on tables: the float under key, if present and a float.

    Table::get_int

    fn Table::get_int(self : Table, key : String) -> Int64?

    Typed accessor on tables: the integer under key, if present and an integer.

    Table::get_path

    fn Table::get_path(self : Table, path : String) -> Value?

    Looks up a dotted path such as "database.server.port". Note: path segments are split on '.' and never treated as quoted keys, so this helper cannot address keys whose own name contains a dot.

    Table::get_string

    fn Table::get_string(self : Table, key : String) -> String?

    Typed accessor on tables: the string under key, if present and a string.

    Table::get_table

    fn Table::get_table(self : Table, key : String) -> Table?

    Typed accessor on tables: the table under key, if present and a table.

    Table::is_empty

    fn Table::is_empty(self : Table) -> Bool

    Returns true if the table has no entries.

    Table::keys

    fn Table::keys(self : Table) -> Iter[String]

    Keys of the table in first-definition order.

    Table::length

    fn Table::length(self : Table) -> Int

    Number of keys in the table.

    Table::new

    fn Table::new() -> Table

    Creates an empty table.

    Table::remove

    fn Table::remove(self : Table, key : String) -> Value?

    Removes key from the table. Returns the removed value, or None.

    Table::set

    fn Table::set(self : Table, key : String, value : Value) -> Unit

    Sets key to value. If the key already exists its value is replaced and its position in the key order is kept.

    TimeParts

    pub(all) struct TimeParts {
    hour : Int
    minute : Int
    second : Int
    nanos : Int
    } derive(Eq,
    Debug
    )

    Time part of a TOML datetime. second may be 60 for leap seconds, nanos is the sub-second part in nanoseconds (0..=999999999).

    Value

    pub(all) enum Value {
    Str(String)
    Int(Int64)
    Float(Double)
    Bool(Bool)
    Datetime(Datetime)
    Array(Array[Value])
    Table(Table)
    }

    A TOML value: the payload type of every entry in a Table.
    impl Eq for Value
    impl ToJson for Value
    impl Debug for Value

    Value::as_array

    fn Value::as_array(self : Value) -> Array[Value]?

    Accessor: the element array if the value is an array.

    Value::as_bool

    fn Value::as_bool(self : Value) -> Bool?

    Accessor: the boolean content if the value is a boolean.

    Value::as_datetime

    fn Value::as_datetime(self : Value) -> Datetime?

    Accessor: the datetime content if the value is a datetime.

    Value::as_double

    fn Value::as_double(self : Value) -> Double?

    Accessor: the float content if the value is a float.

    Value::as_int

    fn Value::as_int(self : Value) -> Int64?

    Accessor: the integer content if the value is an integer.

    Value::as_str

    fn Value::as_str(self : Value) -> String?

    Accessor: the string content if the value is a string.

    Value::as_table

    fn Value::as_table(self : Value) -> Table?

    Accessor: the table content if the value is a table.

    Value::equal

    fn Value::equal(self : Value, other : Value) -> Bool

    Structural equality between TOML values. Tables compare key/value-wise (order insensitive), NaN equals NaN, and datetimes compare by components (so 1979-05-27T07:32:00Z equals 1979-05-27T07:32:00+00:00).

    Value::from_json

    fn Value::from_json(j : Json) -> Value raise FromJsonError

    Converts a core Json value into a TOML Value.

    Conventions:
    • JSON null raises FromJsonError::JsonNull (TOML has no null);
    • numbers that are integral and fit in 64 bits become TOML integers, everything else stays a float (so JSON 1.0 becomes the integer 1, and values beyond the double range fall back to inf-style floats);
    • JSON strings never become datetimes — a string that looks like an RFC 3339 date stays a string;
    • objects become tables, arrays become arrays.

    Example

    test {
    let v = @toml.Value::from_json(@json.parse("{\"a\": [1, true]}")) catch {
    _ => abort("unreachable")
    }
    @test.assert_eq(@toml.encode_value(v), "{a = [1, true]}")
    }

    Value::to_json

    fn Value::to_json(self : Value) -> Json

    Converts a TOML value to JSON (moonbitlang/core Json).

    Mapping:
    • integers keep full 64-bit precision through their textual representation;
    • the four datetime forms become strings in the canonical TOML notation;
    • JSON has no representation for non-finite floats, so inf, -inf and nan become the strings "inf", "-inf" and "nan";
    • tables become JSON objects, arrays become JSON arrays.

    Example

    test {
    let doc = @toml.parse("port = 8080\nactive = true")
    @test.assert_eq(
    @toml.Value::Table(doc).to_json_string(),
    "{\"port\":8080,\"active\":true}",
    )
    }

    Value::to_json_string

    fn Value::to_json_string(self : Value) -> String

    Serializes a TOML value to a JSON string (compact form).

    Value::type_name

    fn Value::type_name(self : Value) -> String

    The TOML type name of the value, one of: string, integer, float, boolean, datetime, datetime-local, date-local, time-local, array, table.

    MAX_NESTING_DEPTH

    let MAX_NESTING_DEPTH : Int

    Maximum nesting depth of arrays and inline tables. Bounds parser recursion so hostile inputs fail with a positioned error instead of exhausting the stack.

    encode

    fn encode(table : Table) -> String

    Serializes a table back into a TOML document. Sub-tables and arrays of tables are emitted as [a.b] / [[a.b]] sections; every other value is written inline. Key order follows first-definition order (scalars before nested tables), and the output round-trips: parsing it again yields an equal Table.

    Example

    test {
    let doc = @toml.parse("[server]\nhost = \"localhost\"\nport = 80")
    @test.assert_eq(
    @toml.encode(doc),
    "[server]\nhost = \"localhost\"\nport = 80\n",
    )
    }

    encode_datetime

    fn encode_datetime(dt : Datetime) -> String

    Renders a datetime in its canonical form (uppercase T/Z, two-digit components, trailing zeros of the fractional part removed).

    encode_value

    fn encode_value(value : Value) -> String

    Serializes a single value in inline form (used for arrays and inline tables, and handy for fragments).

    parse

    fn parse(input : String) -> Table raise ParseError

    Parses a full TOML document and returns the root table.

    Raises ParseError when the input is not a valid TOML v1.0.0 document; the error carries the 1-based line/column of the problem.

    Example

    test {
    let doc = @toml.parse(
    "title = \"demo\"\n[server]\nhost = \"127.0.0.1\"\nport = 8080",
    )
    @test.assert_eq(doc.get_string("title"), Some("demo"))
    @test.assert_eq(doc.get_path("server.port"), Some(@toml.Value::Int(8080L)))
    }

    parse_value

    fn parse_value(input : String) -> Value raise ParseError

    Parses a single TOML value fragment such as "1_000", "[1, 2]" or "1979-05-27T07:32:00Z". Useful for decoding individual settings.

    Example

    test {
    @test.assert_eq(@toml.parse_value("1_000").as_int(), Some(1000L))
    @test.assert_eq(@toml.parse_value("1979-05-27").type_name(), "date-local")
    }