Project

teptris

0.0
The project is in a healthy, maintained state
A native C extension over libteptris, a C99 TOML 1.1 parser/emitter. API shape mirrors the tomlib gem.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

teptris-ruby — TOML for Ruby at libleptris speed

A native C-extension (no FFI, no fallback) Ruby TOML library over the libteptris — the TOML counterpart of leptris-ruby (XML) and yeptris-ruby (YAML+JSON). API-shape and value semantics mirror the tomlib gem: offset and local datetimes become Time, dates become Date, local times stay String.

Performance (end-to-end, Ruby tier)

Best-of-8 runs on a GitHub ubuntu-latest runner (ruby 3.3), 2026-09-21, over the generated bench corpus (~700 KB per shape). Full per-shape table regenerated on every main push by the lang-tier lane.

Shape teptris tomlib 0.7.3 tomlrb 2.0.4

array_heavy

3.0 ms / 237 MB/s

21.7 ms / 33 MB/s

532 ms / 1.4 MB/s

cargo_like

4.0 ms / 88 MB/s

66.7 ms / 5.3 MB/s

307 ms / 1.2 MB/s

datetime_heavy

31.4 ms / 34 MB/s

12,837 ms / 0.1 MB/s

792 ms / 1.4 MB/s

deep_tables

2.9 ms / 84 MB/s

7.0 ms / 35 MB/s

247 ms / 1.0 MB/s

mixed

6.2 ms / 70 MB/s

17.2 ms / 25 MB/s

427 ms / 1.0 MB/s

scalar_float

16.8 ms / 88 MB/s

18,761 ms / 0.1 MB/s

855 ms / 1.7 MB/s

scalar_int

14.8 ms / 87 MB/s

15,135 ms / 0.1 MB/s

868 ms / 1.5 MB/s

scalar_string

9.1 ms / 142 MB/s

3,587 ms / 0.4 MB/s

430 ms / 3.0 MB/s

teptris is 7-110x tomlib and 20-280x tomlrb per shape end to end (parse + materialize into Ruby objects); tomlib’s scalar-heavy cliffs are its datetime/number paths.

Install

gem install teptris

RubyGems resolves a prebuilt platform gem where one exists; every other platform (FreeBSD, OpenBSD, Solaris/illumos, armv7/riscv64/ ppc64le, any-glibc Linux, Windows Ruby 3.0) gets the source gem, which compiles the vendored engine and the extension with the installing Ruby’s own toolchain — a C compiler is required and the install fails loudly without one (the extension is the binding; there is no pure-Ruby fallback).

Supported platforms (prebuilt = PGO-trained where the runner allows,

static, self-contained)

Platform gem Ruby Notes

x86_64-linux, aarch64-linux (fat)

3.3–4.0

one ext per minor; GLIBC >= 2.14 (x86_64) / 2.17 (aarch64)

x86_64-linux-musl, aarch64-linux-musl (fat)

3.3–4.0

one ext per minor; built in Alpine containers

arm-linux, arm-linux-musl

3.3

32-bit ARM; qemu-emulated build (no PGO), single minor

powerpc64le-linux, s390x-linux (fat)

3.3–4.0

big-endian servers; qemu-emulated builds (no PGO)

s390x-linux-musl

3.3

Alpine on IBM Z; qemu-emulated (no PGO), single minor (alpine 3.4+/4.0 s390x images not published)

x86_64-darwin, arm64-darwin (fat)

3.3–4.0

one ext per minor; macOS >= 11.0

x64-mingw32 (fat)

3.0

msvcrt-era RubyInstaller

x64-mingw-ucrt (fat)

3.1–4.0

one ext per Ruby minor

aarch64-mingw-ucrt (fat)

3.4–4.0

RubyInstaller ARM64 builds

ruby (source)

3.0–4.0

everything else — including 3.0–3.2 on fat platforms and any minor on the emulated tier; compiles at install

A native extension binds the Ruby minor that built it, so the fat gems ship one extension per minor under lib/teptris/<minor>/ and the loader picks yours. On a minor a platform gem does not carry, the LoadError names the remedy: install the source gem (gem install teptris --platform ruby), which compiles the vendored engine at install.

Every prebuilt gem also carries the FULL ENGINE SOURCE at ext/teptris_ext/engine (beside the prebuilt extension): untar the gem, cd ext/teptris_ext && ruby extconf.rb && make to rebuild the extension against your own toolchain. The ruby-platform source gem vendors the same tree and compiles automatically at install.

The published-gem smoke lane installs each Windows fat gem from rubygems on every released Ruby and round-trips it; mingw and darwin gems ship byte-identical Ruby sources (EOLs aside).

Tip
Musl (Alpine) note

Some rubygems versions (verified on 3.5.22) resolve the glibc gem for musl rubies even with an explicit --platform, failing at load with a missing ld-linux-*.so. If that happens, install the musl gem file directly:

gem fetch teptris --platform aarch64-linux-musl   # or x86_64/arm-linux-musl
gem install ./teptris-*.gem --local

Development

rake compile          # two-stage PGO build of the pinned libteptris tag + the ext
bundle exec rspec

Performance

Both directions are native (C extensions over libteptris; no FFI, no fallback):

  • load: 1.6×–124× faster than tomlib on every corpus shape (mixed 3.4×, array-heavy 5.0×, scalar shapes 100×+ where tomlib degrades quadratically); 50–380× toml-rb; GC allocations at parity.

  • dump: 3×–10× faster than tomlib on every shape (through the C builder API — the engine emitter is the single formatting source).

The engine implements the TOML 1.1 draft grammar (a strict superset of 1.0: \e/\xNN escapes, optional seconds, inline-table newlines and trailing commas) with 100% toml-test conformance, and parses at ≥3× the best C/C++ competitor on every benchmark shape. Full matrix: teptris benchmarks/LEDGER.md.

Usage

require "teptris"

Teptris::TOML.load("title = \"teptris\"\n[owner]\nname = \"t\"\n")
# => {"title" => "teptris", "owner" => {"name" => "t"}}

Teptris::TOML.dump({"title" => "teptris", "owner" => {"name" => "t"}})

begin
  Teptris::TOML.load("a = [1,")
rescue Teptris::ParseError => e
  e.line   # 1
  e.column # 8
end

Descriptor plan path (lutaml-model recipe)

Compile a plan once, materialize only the planned keys in one native pass (Teptris::Descriptor / Teptris::TOML.load_schema, shipped since 0.2.29; C ABI in libteptris teptris/plan.h). The walk returns a nested Hash with String keys and already-cast Ruby values — the shape Serializable.instantiate wants after a wire→attr rename, not the column-array shape of Yeptris::Schema.

# MappingHash-shaped model: wire names match TOML keys.
DESC = Teptris::Descriptor.build(
  children: [
    { name: "id",    kind: :scalar },
    { name: "name",  kind: :scalar },
    { name: "tags",  kind: :collection },          # array of scalars
    { name: "items", kind: :nested, plan: {        # table or [[array-of-tables]]
      children: [
        { name: "sku",   kind: :scalar },
        { name: "qty",   kind: :scalar },
      ] } },
    { name: "extra", kind: :raw },                 # escape hatch: full subtree
  ])

# compile once per model class; reuse across documents
h = Teptris::TOML.load_schema(toml_string, DESC)
# => {
#   "id"=>1, "name"=>"widget", "tags"=>["a","b"],
#   "items"=>[{"sku"=>"x","qty"=>2}, ...],
#   "extra"=>{"whatever"=>true},   # or nil if absent
# }
# unplanned keys never appear

# lutaml-model adapter half (illustrative):
#   kwargs = mapping.wire_to_attrs(h)   # rename + symbolize
#   Model.instantiate(**kwargs)

Row kinds:

  • :scalar — any TOML value (String/Integer/Float/true/false/Time/Date)

  • :collection — array of scalars

  • :nested — recurse via plan:; also spans arrays of tables

  • :raw — the untouched native subtree (callback/escape hatch)

Absent planned keys read as nil. Type tags are not declared on the Ruby surface: TOML already carries the value kind, and the walk materializes the same datetime contract as TOML.load. Wire→attr renames, when_attribute, and polymorphic partitions stay on the framework side (interpretive), matching the yeptris-ruby#184 split.

load_lazy is the complementary path when the plan is not known up front: one parse, host objects only along touched paths.

Many-small batch (load_batch / load_files / load_lazy_batch)

When the corpus is N TOML documents rather than one (unitsml-style streams, config splits, log shards), accept an Array (or list of file paths) and ship one C call:

# Eager: one Array of Hashes, same datetime contract as TOML.load
docs = Teptris::TOML.load_batch(toml_strings,
                                safe_load: [Time, Date],
                                datetime_policy: :native)

# File paths
docs = Teptris::TOML.load_files(["a.toml", "b.toml", "c.toml"])

# Lazy twin: parse N eagerly, materialize only along touched paths
arr  = Teptris::TOML.load_lazy_batch(toml_strings)
arr.each { |doc| doc["x"].value }   # touches one path per doc

Perf shape (best-of-N on ubuntu-latest, ruby 3.3, 2026-09-21): eager load_batch is a wash or slower than per-doc load for tiny / medium docs (Ruby’s per-doc dispatch is amortized by JIT; the batch’s C-side scratch + tight Ruby allocation loses on those shapes) and a clear win for large docs. The shared-options parsing (one safe_load / datetime_policy check per call) and per-doc error report (a single ParseError carrying the failing doc’s line/column) make eager load_batch a useful API even where it isn’t a speedup. load_lazy_batch is the real many-small speedup when materialization is deferred — parse-only across the batch, no per-doc Hash work.

Gem publishing and version numbers are USER release decisions.