petrbalvin 9874464213 perf(decode): resolve interfaces through cached type flags
The decoder asked every value whether it implements Unmarshaler or encoding.TextUnmarshaler by boxing it into an interface and asserting, which allocated on every scalar field. A per-type flag cache answers first and an interface value is built only where the assertion can succeed; interface destinations are still asked dynamically. A monomorphic hint in front of the cache keeps the hot walk off the sync.Map probe, and it re-points at published cache entries so a miss allocates nothing.

Representative document: 233 to 167 allocations; long document typed decode: 107 674 to 63 772 allocations, about 6.1 to about 3.8 ms.
2026-09-20 22:15:23 +02:00
2026-08-19 08:12:00 +02:00
2026-09-19 00:14:39 +02:00
2026-08-19 08:12:00 +02:00

interpres

A TOML 1.1 parser and encoder for Go, written with the standard library alone. interpres (Latin for interpreter) gives zero-dependency programs an encoding/json-style API for reading and writing TOML, and passes the entire official toml-test suite: 214 valid, 467 invalid and 214 encoder cases, zero failures.

Features

  • Full TOML 1.1: bare, quoted and dotted keys; tables and arrays of tables; basic and literal strings including multiline, with the 1.1 \e and \xHH escapes; integers in the four radixes with _ separators; floats with exponents, inf and nan; booleans; the four date-time kinds, seconds optional as of 1.1; arrays and inline tables, multi-line as of 1.1.
  • Decoding and encoding: Parse for an untyped tree, Unmarshal and Marshal for structs and maps, mirroring encoding/json.
  • Strict decoding: NewDecoder().DisallowUnknownFields() rejects keys that match no destination field, at every struct depth.
  • Custom types: Marshaler and Unmarshaler let a type control its own TOML representation in both directions, and encoding.TextMarshaler and TextUnmarshaler are honoured by default, so net.IP, time.Duration and user types with text methods need no configuration.
  • Cancellation: every entry point has a *Context sibling that honours a context.Context.
  • Ordered documents: Parse gives a *Document that keeps the key order, tells an inline table from a header one, and carries the comments; ParseMap gives the plain map[string]any tree.
  • Configurable emission: Encoder options for declaration-order output, omitting empty arrays, literal multiline strings, and inlining small sub-tables.

Install

As a library:

go get sourcedock.dev/petrbalvin/interpres/v2

Requires Go 1.27.1 or newer. The module imports only the standard library.

Quick start

git clone https://sourcedock.dev/petrbalvin/interpres.git
cd interpres
just example

just example runs the tour in examples/basic: it decodes an embedded document into a struct, prints it, and re-encodes it under both Encoder layouts.

Usage

Decode into a struct

type Config struct {
	Title  string `toml:"title"`
	Server struct {
		Host string `toml:"host"`
		Port int    `toml:"port"`
	} `toml:"server"`
}

var cfg Config
err := interpres.Unmarshal(data, &cfg)

Fields match by the toml:"name" tag, or by the lower-cased field name when no tag is present; toml:"-" skips a field. Parse returns a *Document that also carries the key order and the comments, ParseMap returns the plain map[string]any tree, and UnmarshalContext accepts a context.

Encode from a struct

out, err := interpres.Marshal(cfg)

writes:

title = "example"

[server]
host = "127.0.0.1"
port = 9090

Tables are laid out scalars first, then sub-tables, then arrays of tables, which is the layout that re-parses to the same tree.

Strict decoding

err := interpres.NewDecoder().
	DisallowUnknownFields().
	Decode(data, &cfg)

A key with no matching field becomes an error instead of a silent drop.

Custom types

type Port int

func (p Port) MarshalTOML() (any, error) {
	return int64(p), nil
}

type IP struct{ net.IP }

func (ip *IP) UnmarshalTOML(data any) error {
	s, ok := data.(string)
	if !ok {
		return fmt.Errorf("ip: not a string")
	}
	ip.IP = net.ParseIP(s)
	return nil
}

The value MarshalTOML returns is encoded in place of the receiver; UnmarshalTOML receives the parsed value verbatim.

Encoder options

out, err := interpres.NewEncoder().
	GroupByKind(false).      // preserve declaration order
	OmitEmptyArrays().       // skip empty scalar arrays
	UseLiteralMultiline(80). // long multi-line strings as literal blocks
	Marshal(cfg)

Cancellation

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

out, err := interpres.MarshalContext(ctx, cfg)

ParseContext, UnmarshalContext, (*Decoder).DecodeContext and (*Encoder).MarshalContext follow the same pattern.

The full rules for field matching, numeric conversion and emission live in docs/API.md.

Development

just build       # compile bin/interpres-decode
just test        # the suite, with the 80 percent coverage floor
just toml-test   # the official compliance suite, needs toml-test on PATH
just fmt         # gofmt in place

See docs/DEVELOPMENT.md for the full workflow, and CONTRIBUTING.md for how to contribute.

Documentation

Licence

MIT. See LICENSE.

Copyright © 2026 Petr Balvín

S
Description
TOML 1.1 parser and encoder in pure Go: an encoding/json-style API, documents that keep key order and comments through a round trip, strict decoding and cancellation; standard library only, passing the entire official toml-test suite.
Readme MIT
2.8 MiB
v2.0.0
Latest
2026-09-22 19:52:42 +00:00
Languages
Go 98.4%
Just 1.2%
Perl 0.4%