Zero-allocation, AOT-clean UTF-8 RON (Rusty Object Notation) parser + source-generated binder for .NET.
ronmamon binds RON straight into your types with no reflection, no runtime codegen, and no
per-document heap churn — a forward-only ref struct reader feeds a generated IRonFormatter<T>
per [RonObject] type. It is built for NativeAOT and the kind of config/data layer where you want
the file to read like the struct it deserializes into.
GameConfig(
name: "bob",
display: (
scroll: ( friction: 0.78, spring: 96.0 ),
tab: ( width: 2 ),
),
servers: [
( id: "alpha", port: 8080 ),
( id: "beta", port: 9090 ),
],
mode: Dark, // enums are bare identifiers
)RON nests with delimited values — ( … ) structs, [ … ] lists, { … } maps, each with an
explicit close token. That single property is the whole point: a nested value is read recursively
and returned, then assigned to its field. So binding is pure recursive value-descent — there is
no header state machine, no section cursor, no write-back, and struct and class targets generate
the identical shape. Deeply-nested config that would be a wall of [a.b.c] headers in TOML is just
natural nesting in RON.
[RonObject]
public partial struct GameConfig
{
public string name = "";
public DisplayConfig display = new();
public List<Server> servers = new();
public Mode mode = Mode.Light;
public GameConfig() { }
}
var cfg = RonSerializer.Deserialize<GameConfig>(File.ReadAllBytes("game.ron"));
string ron = RonSerializer.Serialize(cfg); // readable, indented, round-trips- Reflection-free + NativeAOT-clean. A Roslyn source generator emits an
IRonFormatter<T>per[RonObject]; the resolver is a generic-static cache (Cache<T>.Formatter), so resolution is a single static-field read with zero boxing.[ModuleInitializer]self-registration — no scan. - Zero-allocation parse. Forward-only
Utf8RonReaderoverReadOnlySpan<byte>,SearchValuesscanning, span value-decoders. Keys interned per-document. - Binds fields and properties, by a snake_case naming policy (override with
[RonMember("…")], exclude with[RonIgnore]). Defaults come from the parameterless ctor → serde#[serde(default)]parity: a document naming a few keys leaves everything else at its ctor default. structandclassboth work as value targets — value types bind natively (no boxing), andList<struct>/Dictionary<string, V>are trivial.- Custom converters for any type via
RonFormatterResolver.RegisterStatic<T>— newtypes, packed values, untagged unions (branch on the reader token), all plug in uniformly. - Enums are bare identifiers, accepted case-insensitively as either PascalCase (
Dark) or snake_case (dark), serialized snake_case. - Indented, readable writer — one field per line; the format's whole appeal is legible deep nesting.
- 133-test corpus covering scalars, nesting, collections, enums, comments, trailing commas, serde-default, unknown-field skip, malformed-input rejection, and serialize→deserialize fixed-point.
Supported: structs Name( field: val ) (optional name), lists [ ], string-keyed maps { },
scalars (int with _ separators + radix, float incl. inf/-inf/NaN, bool, string with escapes
and \u{XXXX}), bare-identifier enums, // and /* */ comments, trailing commas. Not yet
implemented: explicit Some(x)/None Option tokens (nullable binds via missing-key→null), tuples
without field names, char/raw-string literals.
ronmamon is part of the holo-q tree and has a sibling path-dependency on Poet (the source-emission substrate the generator is authored on, analyzer-time only). Clone it alongside:
some-dir/
ronmamon/ (this repo)
Poet.cs/ (../../repo-kit/Poet.cs in the org tree)
Then dotnet build (or the org's rk build ronmamon). The generator project bundles Poet.dll
into the analyzer load context, so a consumer needs only the two ProjectReferences (runtime +
generator-as-analyzer).
ronmamon is the nesting-native sibling of Tomatonl, the org's
TOML lib — same architecture (Poet-generated binders, generic-static resolver, Utf8 ref-struct
reader), minus TOML's section-header machinery, which RON's delimited nesting makes unnecessary.
MIT.