Issue #1020: Decision: how a typed grammar entry meets a type with a text form of its own (json::Value as a field, time::Instant, @json::Rest)

Open and answer this issue on GitHub.

language, ready · opened 2026-10-30 · updated 2026-11-03 · 3 comments

Introduction

Since ADR-grammar-2 (0.0.1314), a grammar entry can take a type, Json::read[Contact](bytes), and the compiler emits one parser for that type. A match on the field’s kind chooses the pattern: Struct(_), List(A), Maybe(A), Text, Map(K, V) and so on.

struct Event { name: String, at: time::Instant, extra: json::Value }
let e = json::read[Event](bytes)    // today: NK1250 on `at` and on `extra`

Some fields are not described by their kind but by their type:

A kind arm cannot express any of these. ADR-grammar-2 D12 refuses such fields with NK1250 until this is decided.

Where the question comes from

ADR-grammar-2 §6, from the architect’s proposal of 2026-10-30. Nothing needs it now: the first slice of #1019 works without it. It becomes needed when a program reads JSON with timestamps or with a part of unknown shape. Both are common, so it is a basic question for the format packages, not bookkeeping.

Options

How other languages do it

Language / tool Answer
Rust, serde the type implements Deserialize itself (C), or #[serde(with = "…")] on the field (E); #[serde(flatten)] for the rest (Rest)
Zig std.json a type may declare jsonParse, checked with @hasDecl (C without a trait)
Go the type implements json.Unmarshaler (C); json.RawMessage keeps a raw part (B, for the format’s own type)
Kotlin serialization @Serializable(with = …) on the type or the field (C/E); JsonElement keeps a tree (B)

Every one of these has both: the format’s own types handled by name, and an opt-in for others.

Recommendation

D: an exact-type arm for the format’s own types (json::Value, @json::Rest’s map), and a bound arm for every other type, with a trait per format. Exact-type arms are tried before bound arms, and bound arms before kind arms. This matches what Go, Kotlin and Zig converged on, keeps the format package closed, and lets time::Instant opt in from std’s side.

Blocks: nothing now. @json::Rest and json::Value fields in #1019 wait for it.

Comments

keywan-ghadami, 2026-11-03

Decision (owner, 2026-11-03)

The owner worked this out in conversation. The result goes beyond options A to E: it is option C, with the trait carrying a rule instead of a function.

What was decided

  1. The type supplies the reading. The format only delimits the value (quotes for a text, the number token for a number). The type’s rule must match the whole content. Formats have no built-in time type.
  2. One trait per lexical kind. std provides FromText and FromNumber. The list of kinds is open, and a format package may add its own traits (json::RawText, toml::LocalDate). There is no new kind keyword, because T::kind already means meta::Type (ADR-330).
  3. A trait declares a rule the way it declares a function:
    trait FromText { rule from_text -> Self }

    The trait decides what the rule reads. FromText reads decoded text, so escapes are already applied. A raw trait of a format reads the characters undecoded, which is how a high-performance type can skip escape decoding. The compiler may still choose to read raw when the type’s grammar accepts no backslash.

  4. An impl defines the rule directly, without a grammar block around it:
    impl FromText for Zeitpunkt {
        rule from_text -> Zeitpunkt =
            d:Datum "T" u:Uhrzeit "Z" ->
            { return Zeitpunkt::new(d, u) }
    }
    • The action is an ordinary function body, with return.
    • A rule in an impl is shorthand for a grammar with exactly this one rule as its entry. It uses the same mechanism and lowering as grammar.
    • An impl may instead point at an entry of a full grammar: rule from_text -> Zeitpunkt = Iso8601::zeitpunkt.
  5. A type name where a rule is expected embeds that type’s rule for the same trait. d:Datum inside a FromText rule needs Datum: FromText. If that impl is missing, the error names exactly that missing impl.
  6. No separate function in the trait. The function form comes from the rule: Zeitpunkt::from_text(s) runs the rule over the whole input and throws when it does not match. This works the same way as calling a grammar entry today (Part II 10.2).
  7. Several forms.
    • Within one trait, the forms are alternatives in the rule, and each alternative’s action yields Self.
    • A type that has both a text form and a number form implements both FromText and FromNumber.
  8. Helper rules:
    • A helper that is a meaningful value of its own (Datum, Uhrzeit) becomes its own type with its own impl FromText.
    • Small private helpers go in the type’s inherent impl, like private helper functions: impl Zeitpunkt { rule zone -> Zone = ... }.
    • Many related rules (a whole format such as ISO 8601) become a grammar, and the impl points at its entry.
    • A trait impl holds only what the trait requires. Extra rules in a trait impl are refused, as extra methods are today (NK1130).
  9. The format side is a bound-guarded arm in match T::kind. It embeds the type the same way:
    T: FromText   => "\"" v:T "\"" -> { return v }
    T: FromNumber => v:T -> { return v }

    When a type implements several traits, the order of the format’s arms decides which one is used. No supertraits are needed (those stay undecided, ADR-341 §6).

  10. A type that reads its text by hand captures the content and passes it to a function in the action: rule from_text -> T = s:text -> { return schnell_lesen(s) }. The compiler needs nothing special for this.

Not covered by this decision

json::Value as a field and @json::Rest (ADR-344 D4) were not discussed. A first reading: json::Value could implement a trait of the json package, which would make the exact-type arm of option B unnecessary. That is not decided yet.

To build

  1. The ADR text (supervisor): rule items in traits and impls, the shorthand, rules named by type, the derived function, and bound-guarded arms. ADR-grammar-2 D12 and §6 point to it.
  2. Parser and checker:
    • A rule item in a trait (signature only).
    • A rule item in a trait impl or an inherent impl, lowered to a one-entry grammar.
    • NK1130 also covers rules.
  3. A type name in rule position resolves to that type’s rule for the enclosing trait. v:T under a bound T: FromText resolves the same way.
  4. The bound-guarded arm T: Trait => in match T::kind. Arm order is tried top to bottom.
  5. The derived function T::from_text(s). This includes the call on a type parameter, which is refused today: fn read[T: FromText](s: String) -> T throws { return T::from_text(s) } gives NK1245.
  6. std: the FromText and FromNumber traits. The json package gets the bound-guarded arms. time::Instant gets impl FromText.
  7. Raw traits for formats (json::RawText) and their undecoded input.
  8. Tests: .nika programs in tests/compiler/ for each step.

Generated by Claude Code

keywan-ghadami, 2026-11-03

Owner, 2026-11-03: #1020 falls under the specification-first exception (#1214). Where the code it changes is Rust (the parser’s grammar block, the checker), it is built in Rust. Recorded in CLAUDE.md.


Generated by Claude Code

keywan-ghadami, 2026-11-03

Owner, 2026-11-03: json::Value as a field is not a special case. It is an enum whose variants are recognised by the shape of the value: a string can only be Text, an object only Object, and so on. It needs no exact-type arm and no trait of its own. The form of enums with payload variants is decided in #1219, and the json::Value field is built there.

@json::Rest remains outside this decision. It concerns how a struct treats members it does not know, not the text form of a type.


Generated by Claude Code

Back to Open Issues