audit-runtimes · diff
git:20261030.823453d to git:20261030.3ec8ee8
1 added, 1 removed. Audit A to A.
---
name: audit-runtimes
description: "Audit Nino's runtimes outside .NET (runtimes/go, runtimes/rust, runtimes/typescript) and nino-gen (src/Nino.Codegen), which writes code for them - safety on hostile input, speed against each language's competitors, SIMD beyond strings, allocation, idiomatic API, parity with .NET's bytes and limits, and the templates against the hand-written samples. Use when changing a runtime or a template, when a runtime is slower than a competitor or than the previous main, or for a runtimes review. Triggers: runtimes/go, runtimes/rust, runtimes/typescript, nino.ts, src/Nino.Codegen, nino-gen, Templates/*.sbn, NINOGEN, cargo, go test, npm test, wasm, benchstat, criterion, Go 运行时, Rust 运行时, TS 运行时, 代码生成."
metadata:
internal: true
---
# Runtimes audit
Each runtime's rules are in its `AGENTS.md` (`runtimes/go`, `runtimes/rust`, `runtimes/typescript`) and nino-gen's in
`src/Nino.Codegen/AGENTS.md`: read the one you touch in full. This skill is the checklist across them.
## Parity with .NET
- The same bytes for every value (the conformance corpus round-trips), the same rejections (every malicious case and
truncation), the same depth counting and deferral, the same caps on lengths and allocations (`audit-security`).
- Stricter than .NET is allowed, looser never; each stricter check is listed in the runtime's README.
- A C# feature users can declare is supported by each runtime and by nino-gen's templates, or reported as NINOGEN002
naming the type, never generated code that reads differently.
- Each runtime offers .NET's API shapes in its own idiom, under the same concept names: a new value
(`Serialize`), memory the caller owns and reuses (`Serialize(value, bufferWriter)` with a `NinoArrayBufferWriter`:
TypeScript `serializeInto` with a `NinoWriter` and `reset`, or a `Uint8Array`; Go `Append` and `Writer.Reset`; Rust
`serialize_into`), and `Exact` reads. Why: the owner wants them "the same as C#" in every runtime, and in Node a new
`ArrayBuffer` per value adds up to full GCs (`benchmark`, "Keep only the running serializer's data live").
Reading into an existing value (`Deserialize(data, ref value)`) is TypeScript `deserializeInto` (`NinoCodec.readInto`),
Go `DeserializeInto` (`ReadXInto`) and Rust `deserialize_into` (`NinoRead::read_into`), with one read body per type:
the plain read builds a new value and reads into it (`readX(r, v = new X())`, `ReadX` calling `ReadXInto`, Rust's
struct `read` building `Default`), so a template change to a read path reaches both. The corpus tests read every
valid case into the previous case's value and every hostile input into a valid value (same verdict as a fresh read).
## Safety
No input may panic, throw an unexpected error, read out of bounds, recurse without a bound or allocate out of
proportion (Go: sticky errors, no `recover`; Rust: the crate's lints, `unsafe` only in `src/simd.rs` with SAFETY
comments and Miri; TypeScript: bounds before allocating, `Map` for keys from the data). Fuzzers run on every change
to a read path.
## Speed
- Each runtime leads every case of its `bench` against its competitors, on x64 and Arm64, judged per CPU on runners
against the main-vs-main floor (`benchmark`). A bench that times one input over and over is not a fair lead: its
parse cases must rotate their inputs, and each serializer runs with only its own data live (`benchmark`, "Adding a
case").
- SIMD beyond strings where it measures faster (primitive blocks, bool validation, fixed-size records, transcoding),
with the portable path kept and tested (Go `purego`, Rust without the features, TypeScript without WebAssembly).
- Hot paths: one bounds check per run of fixed-size members and per loop (the idioms per language are in
- `optimize`, "Bounds checks"), no reflection or boxing outside polymorphic slots, rare
+ `optimize-runtimes`, "Bounds checks"), no reflection or boxing outside polymorphic slots, rare
paths out of line (`//go:noinline`, `#[inline(never)]`), writing a value free of allocations.
## Templates (nino-gen)
- Logic in the IR (`Model/`) and each target's C# (`Targets/`), never in the templates, which only assemble text;
a decision about the bytes goes into the IR once, for every language.
- What the hand-written samples do, the templates write (`runtimes/go/internal/sample`, Rust's conformance types,
the TypeScript samples); the generated code is held to `audit-generated-code`.
- Every option is defined once in `Options.cs`; `build/Nino.Codegen.targets` and the integration tests agree.
- The code standard (`audit-code`: DRY, decoupled, clear) applies to these files, and redundancy in them
(duplicated logic, dead code, two ways to do one job) is part of this audit; the sweep across layers is
`audit-redundancy`.
## Proof
Each runtime's checks before committing (its AGENTS.md: vet/race/fuzz, fmt/clippy/test with `NINO_CORPUS`,
`npm ci && npm test`), the regenerated committed outputs (`NINO_UPDATE_GENERATED=1`), and runner A/B for speed.