hraness

file formats and protocols as small languages

a grammar is a grammar, even without a parser

Drafted by an AI agent at Ben Guo's direct request from the Hraness source repositories, and checked against those sources before publication.

Nobody writes a parser generator for a JSON file, and yet a versioned data format has everything a language has: a grammar (what shapes are legal), a semantics (what the legal shapes mean), and a pragmatics (who is allowed to produce which productions). Platonik’s public artifacts (experiments, receipts, challenge bundles, season manifests, journal entries, export bundles) make a good case study precisely because nobody designed them as “a language.” They were designed as schemas, and the language emerged anyway.

a grammar without a parser frontend

The Experiment struct in crates/platonik-core/src/model.rs is a grammar production in the plainest sense: it declares the nonterminals (sources, depots, beacons, valves, cells, links, events) and their arities. Every struct carries #[serde(deny_unknown_fields)], which is a grammar rule: an experiment containing a key the schema doesn’t know is not a valid experiment, full stop. There is no error-correcting parse, no best-effort mode, no “ignore unknown extensions.” The reader is strict because the writer is a ledger: a document that can’t be interpreted exactly must not be interpreted approximately.

The same pattern repeats at every envelope layer, each with a literal schema string that acts as the document’s declared grammar:

  • platonik-habitat-v1 through v4: experiment versions, resolved by protocol_for_version
  • platonik-receipt-v1: {schema, protocol, experiment_hash, result_hash, experiment, result}
  • platonik-challenge-v1, platonik-challenge-submission-v1, platonik-challenge-result-v1, platonik-challenge-board-v1
  • platonik-challenge-season-v1, platonik-season-result-v1, platonik-season-salt-v1, platonik-season-board-v1, platonik-season-admission-v1, platonik-season-verify-v1
  • platonik-habitat-journal-v1 and platonik-habitat-bundle-v1: the append-only save format and its export

Each schema name is a versioned grammar identifier, and deny_unknown_fields makes every one of them closed. A “v5 document” or a document with an extra field fails at the grammar level, before any semantic question is asked. That is exactly how a language should treat a program that doesn’t parse.

validation is the semantics

Syntax says what a document may contain; semantics says what it means. For a data format, semantics lives in the validator, and validate_experiment in crates/platonik-core/src/sim.rs is a proper semantics, roughly two hundred lines of referential and arithmetic rules that no schema definition alone could express:

Grid dimensions must be 3–32.               # arity constraints
Entity IDs must be unique within each kind. # scope rules
Spark IDs must be globally unique.          # cross-production naming
Valve needs adjacent depot and matching     # referential integrity
zero/one outlets.
Links require local adjacent endpoints,     # a routing grammar where
port 0–3, and delay 1–16.                   #   wires have distance
Event tick is outside the run.              # temporal well-formedness

These are semantics in the denotational sense: they pick out which syntactically well-formed values denote actual worlds. “A valve must sit adjacent to its depot and to two beacons of opposite polarity” is not a type error JSON Schema could catch; it is a geometric invariant of the language’s world model, and it lives in exactly the right place, between parse and execution. The 64 KiB canonical-input cap (MAX_INPUT_BYTES) is the grammar’s own size bound: it applies not to the submitted text but to the reserialized document, which is the format equivalent of checking the parsed AST rather than the source file.

The journal format makes the same point with time added. A Store in crates/platonik-cli/src/journal.rs is a sequence of Entry values ({schema, revision, previous_hash, event}) where each entry’s previous_hash must equal the hash of the committed predecessor. Loading a journal is parsing a grammar whose rules include “entry n names entry n−1 exactly” and “committed bytes must be canonical.” A gap in the sequence or a non-canonical byte is a syntax error in a temporal grammar: the file is not merely corrupt, it doesn’t parse.

the wire protocol is a language too

Inside the experiment, the signal system is a tiny communication grammar. A Link connects an Endpoint (a Cell{id, port} or a Depot{id}) to a recipient cell and port, with delay: 1..=16 ticks and an enabled flag. A Signal carries exactly one bit: bool plus provenance (receipt_spark), a send tick, and a delivery tick. It is a protocol whose entire payload alphabet is {0, 1}, framed by declared wiring rather than addresses invented at runtime.

The BitSource enum is the send-side grammar: constant{value} | memory{slot} | message{port}. You may transmit a literal, a register’s truthiness, or the bit of a queued message. Nothing else. Compare this to a general-purpose messaging API: no payload sizes, no encodings, no headers. The protocol is small enough that “what can be said” is enumerable, which is why the engine can afford to log every emission outcome (queued, disabled, not_adjacent, full, delivered, consumed, expired) as a complete account of what the wire did.

Even the challenge derivation is a grammar. challenge.rs defines stream(index, stream_id), a pure function over SplitMix64 mixing, and builds cases only through witnessed_case. The published window is a language in the formal sense: the set of admissible documents is defined by a generative rule (derive candidates, keep those the witness passes), not by an enumerated list. Sixty-four ids currently inhabit it, and “challenge-0007” is a name anyone can parse back to index 7 through parse_id.

The CLI’s own contract is a grammar too, at the process boundary. run, verify, inspect, suite bridge-v1 all read a file or - for bounded standard input, write JSON to stdout, and reserve three exit codes with distinct meanings: 0 success, 1 for a valid but failed mission, 2 for invalid input or tampering. That three-way partition is a result grammar: a caller can distinguish “the organism lost” from “the document was forged” without parsing a word of output, and verify can exit 0 on a receipt whose mission failed, because integrity and success are different questions.

canonical forms and strict readers

Two choices make these formats behave like well-defined languages rather than loose conventions. First, canonical serialization: field order is fixed by the serde structs, collections are ordered vectors or BTreeMaps, and whitespace is absent from serde_json::to_vec output. artifact_hash streams that canonical form into SHA-256, so a document’s identity is a function of its parsed meaning; check.rs notes this is sound because the records are versioned, integer-only, and ordered. The canonical form is the grammar’s normal form: two texts that parse to the same value have the same identity, and two values that differ have different identities.

Second, the season scheme treats derivation as a grammar with a secret terminal. Withheld eval cases derive from sha256(salt, entrant, challenge, entry): the salt is a hidden literal in an otherwise public production rule, and the manifest’s commitment (a hash of the salt record) commits to that literal before anyone writes to it. Reveal attaches the salt and every derived case re-derives publicly. The format’s grammar included the commitment all along; only the terminal was withheld.

Even the output of verification is a typed grammar. verify doesn’t print a boolean and a shrug; it emits a platonik-verification-v1 report ({schema, verified, passed, protocol, experiment_hash, result_hash, ticks_completed, work}) where verified and passed are deliberately separate fields. A receipt can be authentic and record a failure: integrity and success are different productions, and the report format makes you say which one you got.

reading formats as languages

The framing earns its keep because it changes what you look for when designing or auditing a format. A grammar asks: what are the productions, are they closed, and what is the normal form, which here is deny_unknown_fields plus canonical bytes plus a hash. A semantics asks: which well-formed values denote real things, which here is validate_experiment, the ledger’s conservation rules, and the witness-run admission. A pragmatics asks: who may produce which productions, which here is the challenge Submission carrying only editable cell programs, the journal accepting only request_id-named appends at the expected revision, and the season workflow binding the entrant field to a GitHub login rather than to whatever the file claims.

Platonik’s formats pass the language test because they were built under the same discipline as the engine: closed, bounded, canonical, versioned, and re-checkable. The next time you sketch a JSON schema, the questions are already waiting. What is the grammar, and is it closed to extension? What is the semantics, and where does validation live? What is the normal form, and can two people produce the same document twice? A format that answers all three is a language; one that doesn’t is a bug report with a schema attached.

sources

  • platonik: crates/platonik-core/src/model.rs for the Experiment, Link, Signal, and Endpoint types; crates/platonik-core/src/sim.rs for validate_experiment; crates/platonik-core/src/check.rs for artifact_hash and platonik-receipt-v1; crates/platonik-cli/src/journal.rs for Entry and the hash-chained store; crates/platonik-core/src/season.rs for the commitment scheme
  • docs/seasons.md: salt derivation and the commitment lifecycle
  • docs/rust-bridge.md: the implemented format surface and its limits