Grouped emission walks the entries slice in three passes instead of copying them into per-kind slices, error paths render their key path only when an error names it, inline tables are measured once because a measuring encoder no longer nests another measurement, clean string runs are written in one write, and date-times render through the single-buffer path. The output buffer comes from a sync.Pool and returns to it within a 1 MiB retention cap, the output handed to the caller as a copy; repeated marshals keep the live heap flat, verified over 500 runs. The encoder resolves Marshaler and the text interfaces through the same per-type flag cache and hint the decoder uses, and docs/ARCHITECTURE.md now lists the caches and the pool as the library's shared state. Representative document: 177 to 141 allocations, 12 230 to 6170 bytes per operation; long document: 97 175 to 63 660 allocations, 6.97 to 2.79 MB per operation, about 7.8 to about 3.4 ms.
6.5 KiB
Architecture
How interpres is put together. Every file, package and arrow below exists in the source tree; nothing is aspirational.
Overview
interpres is one public library package, one command, and one example. The library implements the whole of TOML 1.1, decoding and encoding, in the standard library alone; the command wraps the parser and the encoder for the toml-test compliance harness, against which it stands at 214 valid, 467 invalid and 214 encoder cases with zero failures; the example demonstrates the API.
flowchart TD
CLI[cmd/interpres-decode<br/>toml-test adapter] --> API
EX[examples/basic<br/>usage demo] --> API
subgraph Lib [package interpres]
API[interpres.go<br/>public API and types]
API --> P[parser.go<br/>recursive-descent parser]
API --> DEC[decode.go<br/>tree onto Go values]
API --> ENC[encode.go<br/>Go values to TOML]
P --> N[number.go<br/>numeric tokens]
P --> DT[datetime.go<br/>date-time atoms and types]
end
The public API in interpres.go is a thin facade: every entry point funnels
into ParseContext for parsing and into the unexported decoder and encoder
for the reflection work. parser.go owns the grammar; it leans on
number.go and datetime.go for the two token families that need their own
strict validation.
Packages
| Path | Responsibility |
|---|---|
. (package interpres) |
The whole library. interpres.go declares the exported surface (Parse, Unmarshal, Marshal, the *Context variants, Decoder, Encoder, Marshaler, Unmarshaler, SyntaxError, the local date-time types); everything below it is unexported. |
cmd/interpres-decode |
The toml-test adapter, both directions. Reads TOML on stdin, writes tagged JSON on stdout; with -encode it reads tagged JSON and writes TOML. Owns no parsing logic and no emission logic. |
examples/basic |
A runnable tour of the API. Documentation in executable form, not part of the library. |
Inside the library package, one file owns one concern:
| File | Responsibility |
|---|---|
parser.go |
The recursive-descent parser. Produces the map[string]any tree, records the nodes a Document is built from, and enforces the structural rules of TOML 1.1 (table redefinitions, dotted keys, arrays of tables, multi-line inline tables). Reports a 1-based line on failure. |
document.go |
The parsed-document types: Document, Table and Entry, which carry the key order, whether a table was written inline, and the comments. The values they expose are the parser's own tree, not a copy. |
number.go |
Strict numeric tokens: integers in the four radixes with _ separators, and floats including inf and nan. Rejects leading zeros, misplaced underscores and malformed fractions. |
datetime.go |
The three local date-time wrapper types and parseDateTime, which classifies a token into the four date-time kinds under the strict TOML grammar. |
decode.go |
Maps the parsed tree onto Go values by reflection: struct fields, maps, slices, scalar conversion with overflow checks, Unmarshaler dispatch. |
encode.go |
The reverse walk: builds an intermediate tomlDoc per table (which is what preserves declaration order and enables the group-by-kind partition) and then emits it as TOML. |
The boundary that matters: parser.go produces only untyped trees
(map[string]any, []any, []map[string]any, scalars); decode.go and
encode.go are the only files that touch reflect; the command never touches
either, it consumes Parse alone.
Data flow
Decoding is parse, then one reflection walk. SyntaxError values are produced
inside parser.go and returned as-is; conversion errors are produced inside
decode.go and wrapped with the key path as they unwind.
sequenceDiagram
participant Caller
participant API as interpres.go
participant P as parser.go
participant D as decode.go
Caller->>API: Unmarshal(data, v)
API->>P: ParseContext(ctx, data)
P->>P: number and datetime atoms
P-->>API: map tree or *SyntaxError
API->>D: decode(tree, reflect value)
D-->>API: nil or wrapped field error
API-->>Caller: error
Encoding walks the other way. encode.go first builds a tomlDoc from the
value, then emits it; the two phases are why GroupByKind can reorder entries
without a second reflection pass, and why cancellation is checked during both.
sequenceDiagram
participant Caller
participant API as interpres.go
participant B as encode.go build
participant E as encode.go emit
Caller->>API: Marshal(v)
API->>B: build tomlDoc from struct or map
B-->>API: tomlDoc or error
API->>E: emitDoc(doc)
E-->>API: bytes or error
API-->>Caller: bytes, error
State and lifetime
- The exported
DecoderandEncoderhold configuration only. EveryDecode,DecodeContext,MarshalandMarshalContextcall allocates its own unexported worker, so a configured type is safe for concurrent use; the setter methods are not, and must finish before the value is shared. - The parser is allocated per
ParseContextcall; the parser itself caches nothing between documents. - The shared state is a set of caches and pools whose entries are immutable
once published, each growing with the number of distinct types rather than
with document size: the struct-schema cache in
decode.go(async.Mapkeyed onreflect.Type, holding the flattened field layout the decoder and the encoder both consult), the per-type interface flag caches indecode.goandencode.go(recording whereMarshaler,Unmarshalerand the text interfaces can be found, so a walk builds an interface value only where the assertion can succeed), each fronted by a monomorphic hint holding the type resolved last, and the encoder's output-buffer pool inencode.go(sync.Pool, buffers returned to it only within a 1 MiB retention cap). A published schema or flag set never mutates, so concurrent callers only race to build an identical value, the same trade-offencoding/json's field cache makes. - The date-time wrappers are values, not pointers, and are immutable in use.
- Nothing in the library starts goroutines; apart from the caches and the pool above, which never mutate a published entry, there is no shared mutable state.
Dependencies
None. go.mod declares the module and the Go version and carries no requires;
the library imports the standard library only, which is the point of the
project. The toml-test binary is a development and CI tool, never a module
dependency.