Skip to main content

Module toml

Module toml 

Source
Expand description

TOML codec. Ported from ~/dev/omnist/omnist/formats.py’s read_toml/write_toml/check_toml.

§Crate choice

Like yaml.rs (issue #18), this module delegates raw tokenization to a well-tested crate – [toml_edit] – rather than hand-rolling TOML’s grammar (tables, dotted keys, inline tables, array-of-tables headers, four temporal literal forms, three integer radixes). toml_edit is used read-side only: read_toml walks its parsed DocumentMut into this crate’s canonical crate::document::Value. The writer is hand-written (mirroring json.rs/yaml.rs’s writers), producing TOML text directly from a crate::document::Value rather than round- tripping back through toml_edit’s own formatter – see “Writer output shape” below for why.

Everything omnist-specific stays hand-written Rust, not delegated to the crate:

  • Null-adjustment (strip_nulls) – TOML has no null at all; dropping a null-valued field/array-item and recording it via crate::report::WriteReport is this module’s own logic (see “No null” below).
  • Temporal canonicalization (format_datetime) – turning toml_edit’s parsed Date/Time/Datetime structs into this port’s canonical ISO-string spelling (see “Native temporal types” below).
  • Integer digit captoml_edit itself rejects any integer literal that doesn’t fit in i64 with a generic “integer number overflowed” parse error that discards the offending literal’s text; this module recovers the raw digit run from the error’s byte span and re-derives the same 4300-digit-cap-vs-genuine-overflow distinction json.rs/yaml.rs already make (see “Integer digit cap” below for why this needed hand-written recovery rather than being free from the crate).
  • Depth guard, shape-check reuse – see their own sections below.

§No null (the one unrepresentable-value case TOML has)

Spec Sec8.3.8/Sec8.3.9 (updated 2026-08-24): writing a node containing a null-valued field now fails the write unconditionally (write.unsupported-value, via [crate::report::unsupported_value_error]) regardless of strict, rather than the old “drop the field and record a warning” behavior. TOML has no null token at all – dropping the edge didn’t just alter what’s represented at that position, it erased the edge’s existence entirely, with zero trace on read-back that a labeled edge with that path was ever there (confirmed live; retired the null.omitted code – see strip_nulls’s doc comment). check_toml still reports the same condition as a write.unsupported-value Severity::Error adjustment for preview purposes, since it never produces output to begin with.

If stripping nulls leaves a document whose root isn’t a table (object), write_toml raises WriteError unconditionally – not part of the report, mirrors Python’s write_toml raising WriteError("TOML needs a top-level table (the root must be an object)") outside of finish_write entirely (so it fires even when report is supplied and even though the message never enters the accumulated WriteReport).

§Integer digit cap (omnist-ts#54 / json.rs / yaml.rs precedent) –

not a natural 64-bit range check

The issue’s own framing floated “TOML integers are spec’d as 64-bit, so this may be a natural range check rather than the 4300-digit-cap mechanism used elsewhere” – live-checked against tomllib (Python’s stdlib TOML reader, which omnist.formats.read_toml wraps directly) and found not to be the case: tomllib.loads("x = " + "9"*4300) parses successfully to a full-precision Python int (no 64-bit truncation, no error) – while tomllib.loads("x = " + "9"*4301) raises ValueError: Exceeds the limit (4300 digits) for integer string conversion, the identical CPython int(str)-conversion guard read_json’s comment documents (sys.set_int_max_str_digits), not a TOML-spec-mandated 64-bit bounds check at all – for decimal literals only. Hex/octal/ binary literals are a genuine exception, not a false one: live-confirmed tomllib.loads("x = 0x" + "f" * 5000) (and even 10000 fs) parses successfully with no error at all, matching CPython’s own documented carve-out (sys.set_int_max_str_digits explicitly exempts power-of-two bases) – an earlier draft of this comment claimed hex/octal/binary hit the identical digit-limit ValueError, which was wrong and has been corrected. So Python’s TOML integer handling is json.rs’s/yaml.rs’s existing 4300-digit-cap pattern for decimal literals, and uncapped for hex/octal/binary.

This port does not replicate that decimal/non-decimal split: every radix goes through the same toml_overflow_error recovery and the same 4300-digit cap, because toml_edit itself enforces a strict i64 range at parse time regardless of radix (see below) – there is no path in this implementation for an oversized hex/octal/binary literal to reach Scalar::Int uncapped the way Python’s arbitrary-precision int does. This is a disclosed divergence from Python, not parity: kept deliberately rather than special-cased away, because (a) TOML 1.0 itself specifies 64-bit signed integers and toml_edit enforces 64-bit bounds at parse time, so an “uncapped hex” path would still fail for anything over i64::MAX, and (b) capping the digit run uniformly preserves the same superlinear-conversion DoS protection json.rs/yaml.rs apply, without carving out a radix-specific exemption.

Where this module’s implementation had to diverge from json.rs’s straight-line reuse: toml_edit itself enforces a strict 64-bit range at parse time ("9"*20 / i64::MAX + 1 both fail with a generic “integer number overflowed” TomlError that does not expose the original digit run), which is stricter than Python’s real behavior for anything between 20 and 4300 digits. To keep this port’s observable integer-literal error behavior matching Python’s (not toml_edit’s internal, incidentally-stricter parse limit), toml_overflow_error recovers the raw literal text from the failed parse’s byte span (TomlError::span) and re-derives the digit count itself, producing the same two-tier message json.rs/yaml.rs give (“exceeds the 4300-digit cap” vs “out of range for a 64-bit integer”) instead of surfacing toml_edit’s own message directly.

§Native temporal types (the opposite direction from JSON’s problem)

Unlike JSON (no temporal type at all) and like YAML (a native but looser timestamp grammar), TOML has four first-class temporal literal forms (local date, local time, local datetime, offset datetime) that are stricter-shaped than YAML’s – toml_edit’s own parser already fully validates calendar/clock fields (leap years, per-month day counts, valid hour/minute/offset ranges: live-confirmed 2024-02-30, 2024-13-01, 25:00:00, 00:60:00, and a +25:00 offset are all parse errors, not accepted-then-rejected-later), so read_toml does not need to re-validate calendar/clock fields the way yaml.rs’s normalize_timestamp must (YAML’s own crate does no such validation).

crate::document::Scalar has real Date/Time/Datetime variants (issue #105) – toml_value_to_value reads toml_edit’s own already fully-validated Datetime struct’s date/time presence to construct the right one directly (real provenance, not a shape guess), and format_datetime renders the canonical ISO spelling either way: zero-padded, T-joined, a bare Z offset normalized to +00:00 (matching yaml.rs’s identical normalization – this port’s canonical temporal strings never contain a literal Z, only a numeric offset, which is what crate::schema::is_iso_datetime’s regex expects). Fractional seconds beyond microsecond precision are truncated, not rounded, to six digits – live-confirmed against tomllib: 00:32:00.9999999 (7 nines) reads as datetime.time(0, 32, 0, 999999) (truncated, not rounded to 1000000 and carried), matching this module’s nanosecond / 1000 integer-truncating conversion exactly.

UTC-offset preservation (the omnist-ts#51-pattern check this issue calls for): an offset datetime’s numeric offset is preserved exactly in the canonical string (-07:00 stays -07:00across read+write), so this module does not repeat the OML writer’s offset-erasure bug – confirmed by this module’s round_trips_offset_datetime_preserving_negative_offset and _positive_offset tests.

On the write side, a genuine Scalar::Date/Datetime writes as a native TOML temporal literal (unquoted) unconditionally – no shape-check, since the variant itself is the provenance signal (issue #105, the same fix issue #99 already applied to OML). An ordinary Scalar::Str always writes quoted, however date-shaped its text – this now matches Python’s write_toml exactly (previously diverged: Python’s document model retains a real datetime.date/time/ datetime object end-to-end, so a plain str that merely looks like a date – live-confirmed: tomli_w.dumps({'a': '1979-05-27'}) – always wrote as the quoted string a = "1979-05-27", never a native literal; this port’s pre-#105 Scalar had no way to make that distinction, so it wrote bare unconditionally whenever the text merely looked temporal-shaped – a real bug, confirmed live and fixed, not a permitted variation). A Scalar::Time carrying a UTC offset is the one remaining case with no native TOML spelling at all (TOML’s local time literal has no offset field) – see write_scalar’s own has_offset fallback.

§Depth guard reuse

read_toml parses TOML text into a crate::document::Value, then builds a Doc via Doc::of – which calls crate::document::check_write_depth internally (see document.rs). write_toml/check_toml/strip_nulls walk an already-built Doc (via Doc::to_grouped), whose every node was depth-checked at construction time – exactly json.rs’s reasoning (not yaml.rs’s, which pre-processes raw structures before Doc::of ever runs) – there is nothing left to re-guard on the way out, so strip_nulls does not take or check a depth parameter at all.

toml_edit additionally enforces its own, separate recursion cap while parsing – empirically found (see this module’s tests) to reject TOML text nested roughly 81 levels deep (inline tables), well below this crate’s own 200-level MAX_DEPTH. This means a read-side test can only ever observe toml_edit’s own ParseError firing first, never this crate’s DocumentError – the depth-guard-reuse obligation is instead demonstrated the way json.rs/yaml.rs already do, by building an over-deep Value directly and confirming Doc::of rejects it (see deeply_nested_document_write_reuses_doc_construction_depth_guard).

§Writer output shape (architecture freedom, per issue #1)

This module always emits nested tables and table-arrays as inline TOML ({ k = v } / [ v, v ]), never [section]/[[section]] headers. This is a deliberate divergence from tomli_w’s (and most hand-written TOML’s) header-based style, chosen because it is unambiguous, needs no header-nesting state machine, and is fully spec-valid TOML – the “one constraint” from issue #1 is observable behavior (what a round trip produces), not byte-for-byte resemblance to tomli_w’s pretty-printing choices, and inline tables/arrays parse back to an identical Doc either way.

Functions§

check_toml
Report what writing TOML would adjust, without producing output.
read_toml
Parse TOML text into a Doc.
write_toml
Project a Doc to TOML text. See this module’s doc comment for the null-adjustment, integer-cap, temporal, and output-shape decisions.