184 rules

language.md

Normative. Mirrored from docs/spec/language.md in the compiler repo; every rule id is an anchor.

The Jock Language — Description

Status: NORMATIVE. This document states what Jock is. It carries no chronology: when and why each rule became true is docs/record/language-arc.md, and every rule that has one links to it.

Rules carry flat L-n ids. The ids are append-only — assigned in the order they were written down, never in reading order, and never renumbered, because crates/, tests/ and tools/ cite them. Each rule names the corpus vectors that pin it, or says ⟨none — UNPINNED⟩ so the gap is countable rather than invisible.

The surface syntax is settled, and the release may ship zero-versioned precisely so it can settle further — so read the tables below as current rather than as a contract. The molds in crates/jockc/hoon/sur/jock.hoon ($keyword, $jpunc, $jatom, $tex, $jock-form) must match the tables here; a mismatch is a bug in one or the other.

Jock is a statically typed language that compiles to the Nock ISA (via Nockasm). Its syntax is Swift-inspired first, Rust-inspired second; its semantics are Nock-native (everything is a noun; functions are closures, emitted as cores at the ABI only — no core algebra is user-visible (types.md S9.8); recursion is the loop primitive). It is not an imitation of either source language: where Swift/Rust idiom conflicts with noun semantics, the noun wins.

Design principles

  1. One meaning per spelling. No syntactic form may mean different things in different contexts (see the adjacency law, §2.4).
  2. Kernel and sugar. Every construct is either a kernel form (one typed lowering rule, one Nockasm production) or sugar (one AST rewrite). A keyword enters the language only with: grammar entry, classification, its rule, and a positive + negative corpus vector.
  3. Loud failure. Where a plausible mis-spelling would otherwise silently change meaning, the grammar is arranged so it fails to parse, with a hint.
  4. Provenance. Every AST node, type expression, and emitted Nockasm node carries a source position.

1. Programs

A program is a sequence of ;-terminated statements followed by a final expression, whose value is the program's value:

let a: @ = 3;
func inc(n: @) -> @ { n + 1 };
inc(a)

L-184 · A program is ;-terminated statements followed by a final expression, whose value is the program's value. A missing terminator refuses %parse-expected %';', pointed at the start of the next statement. ⟨t-arc-term-semi, t-text-split-trail⟩

L-185 · Binding forms are STATEMENTS and require the ; — let, func, declarations, import, assert, assignment. Control expressions with braced bodies (if, match, loop) do not take a trailing ; when they are the final expression, and a braced declaration needs none at all (L-81). ⟨t-arc-decl-nosemi, t-arc-term-semi⟩

L-186 · Sequencing is EXPLICIT. Adjacent expressions never silently compose. Sequencing uses ;, and names enter scope through bindings, declarations and imports only. Juxtaposition does not exist anywhere in the grammar — collections and tuples are comma-separated (L-79) — so two adjacent expressions are either separated by ; or a parse error, which is what lets L-1 read whitespace as the only adjacency signal. ⟨t-arc-ctor, t-parse-unglued⟩

2. Lexical structure

2.1 Source, comments, identifiers

L-52 · Source is UTF-8 with LF-terminated lines. Tab and CR are lexical errors — loud failure, so there is no invisible-whitespace ambiguity to debug. ⟨t-lex-tab-n⟩

L-53 · Comments are // to end of line and /* … */, stripped by the lexer. Non-nesting. ⟨t-lex-comment-p⟩

L-54 · A comment separates tokens exactly as whitespace does. A token after a comment is never glued, so a comment can never create or destroy a postfix binding (L-1). ⟨t-lex-comment-p⟩

L-55 · Token positions are 1-based line and column, columns counting tape characters — bytes, for ASCII. Every lexer vector asserts the positions, so the whole vex-lex file pins this; the three named here are representative. ⟨t-lex-num-p, t-lex-type-p, t-lex-uni-p⟩

L-56 · There are two identifier classes, distinguished lexically. A name has a lower-case initial (foo, myCount) and denotes values, functions and fields. A type name has an upper-case initial (Point, List) and denotes types, structs, classes, traits and aliases. ⟨t-lex-term-p⟩

L-57 · The casing split is load-bearing, not convention: the parser distinguishes name tokens from type tokens, so it is the Rust convention enforced as grammar rather than as style. It is also what lets L-59 hold — a hyphen can be part of a name because no type name can contain one.

L-58 · An identifier is a letter followed by letters and digits. No underscores; _ is the wildcard. ⟨t-lex-term⟩

L-59 · Names admit glued hyphens, Hoon-Term-style. a-1 and foo-bar are each ONE name token, never a subtraction. A trailing hyphen (a-) is not a name and refuses. Hyphens never appear in type names — a - after a type name is minus. The readings cannot collide because binary - is spaced on both sides (§4.2). ⟨t-term-hyphen⟩

L-60 · Stdlib multi-word naming prefers hyphens; user style is free. Both hyphenated (starts-with) and camelCase (myCount, peekUnder) are legal everywhere names are legal. The prelude and stdlib prefer hyphenated Term-style for new multi-word names, with starts-with as the precedent, while existing camelCase prelude names stand. The rule governs what the stdlib adds, not what users write. peekTone (2026-11-05) is camelCase although it is new, because it joins peekContext/peekUnder/peekTry as a fourth member of one family: a hyphenated peek-tone beside those three would read as a different kind of thing. Consistency inside a named set beats the default for the set's next member.

2.2 Keywords

L-61 · The table below is the keyword inventory. A word not in it is an ordinary identifier. A keyword enters the language only with a grammar entry, a kernel-or-sugar classification, its rule, and both a positive and a negative corpus vector — so the inventory and the mold $keyword in crates/jockc/hoon/sur/jock.hoon cannot drift apart silently.

KeywordClassRule
letkernelimmutable binding: %let
varkernelmutable binding: %let with mut=%.y; %edit targets only var bindings (OPEN-14)
funckernelnamed (recursive) function: %func — kernel by decision (OPEN-10 resolved): one lowering rule owns self-name binding, recursion, and the generics binders
lambdakernelanonymous function value: %lambda
if / elsekernel%if; else if chains flatten to nested %if at parse
matchkernelpattern/type discrimination: %match
switchretired, and NOT reservedcollapsed into match (OPEN-15), then unsquatted 2026-10-29: it is an ordinary name and not in $keyword. Listed here because a reader who remembers the word comes looking. Vector t-switch-name
loop / recurkernelrecursion point / re-entry: %loop / %recur, both optionally labeled (loop outer { … recur outer }) — note no ; before the closing brace; a trailing separator there refuses %parse-expected %'}'
deferreserved, unimplementedthe keyword lexes and the parser then refuses %parse-todo %defer unconditionally (parse.hoon); sugar.hoon carries a %defer arm no program can reach. Reserved on the same footing as $(…) re-entry, so the meaning can land without reinterpreting any legal program.
evalkernelNock evaluation: %eval (Nock 2)
crashkernel%crash (Nock [0 0])
importkernel%import; resolution happens at environment construction, not parse
as (as?, as!)kernelcast: %cast (particle of expression grammar, not a standalone form)
assertsugar→ if cond { next } else { crash }
printkernelprint e; k / print(e, N); k. Rules L-33 to L-37 below
and, or, xor, notsugarword connectives (OPEN-13): and/or → nested %if (short-circuit); xor/not → %if/stdlib rewrites
structkernel (declaration)%decl: def-registration in +mint (types.md S9.1 — desugar has no environment, so declarations are one kernel form the parser emits directly)
class / implkernel (declaration)%decl: registration + method minting; standalone instances are impl Trait for Type { … }; (types.md S9.4/S9.5)
traitkernel (declaration)%decl. Rules L-62 to L-66 below
forkernel (declaration)separates trait from target in impl Area for Shape; with a parameterized trait the arguments ride on the trait name, impl<T> Idx<@, T> for List<T>
alias / iskernel (declaration)alias New is Old. Rules L-67 to L-69 below
enumkernel (declaration)%decl: tagged sum. Rules L-70 to L-73 below
insugarmembership. Rules L-74 to L-76 below
privatekernel (declaration)marks the next declaration or func unexported. Rules L-17 to L-25 below

trait — L-62 to L-66

L-62 · A trait's methods carry full signatures, not just names: add(self: Self, other: Self) -> Self;. ⟨t-decl-op-gen⟩

L-63 · A trait may be parameterized, with unbounded binders on the name: trait Idx<I, A> { at(self: Self, i: I) -> A; }. ⟨t-class-ptrait⟩

L-64 · An impl supplies the arguments as a first glued group, impl<T> Idx<@, T> for List<T> — one group means an unparameterized target, by the adjacency law (L-1). A class's impl clause may apply a trait the same way, class K(St) impl Idx<@, T>. ⟨t-class-ptrait, t-err-class-ptrait⟩

L-65 · Operator binding is per-METHOD, not per-trait: add(+);. The signature is implied by the family, several hooks may appear per trait, and they mix freely with signed methods. The trait-name form of the hook is retired. ⟨t-decl-op-gen⟩

L-66 · One dictionary serves every instantiation. Trait binders erase exactly as impl binders do, so the dictionary is one core per (trait, head) and coherence is per (trait, head) — see types.md.

alias — L-67 to L-69

L-67 · alias New is Old is transparent, expanded at elaboration. ⟨t-alias-ok, t-elab-alias-p⟩

L-68 · It is an ALIAS, not a newtype. The two names denote one type; there is no wrapping, no distinct identity, and nothing to unwrap. ⟨t-decl-or-alias⟩

L-69 · The is is load-bearing. An alias has neither braces nor an = of its own, so without is its two halves would sit adjacent with nothing between them. Omitting it refuses. ⟨t-arc-alias-is, t-arc-alias-no-is⟩

enum — L-70 to L-73

L-70 · An enum is a tagged sum declaration. ⟨t-arc-enum⟩

L-71 · Bunt is the FIRST case's bunt, by declaration order. This deliberately breaks Hoon's last-case $% convention, so a reader coming from Hoon should not assume. Well-foundedness is checked at registration (types.md S10.3). ⟨t-arc-enum⟩

L-72 · An empty enum is well-formed and uninhabited. It is %void, it nests into everything, and it has no bunt — bunting it is an error. ⟨t-bunt-void-n, t-bunt-or-void⟩

L-73 · Never is declared by the prelude, so a program writing enum Never {} verbatim refuses %decl-dup rather than illustrating L-72. Use a fresh name to try it. ⟨none — UNPINNED⟩

in — L-74 to L-76

L-74 · x in s rewrites to s.has(x) — a METHOD call on the collection, so dispatch is ordinary per-head resolution and no operator trait is involved. ⟨none — UNPINNED⟩

L-75 · It sits at the ordered-comparison level, and is non-associative there like the rest of that level (§4.2). ⟨none — UNPINNED⟩

L-76 · ∈ is its glyph alias, one of the three aliases rather than a trait-only operator (§4.2).

L-33 · print e; k takes one String. The statement form is print e; k; with a priority it is print(e, N); k. ⟨t-text-print, t-des-print⟩

L-34 · The priority is a BARE number literal. Severities are static, so N is a literal and nothing else — an expression in that position refuses %parse-print-pri. ⟨t-err-print-pri-expr, t-err-print-num⟩

L-35 · Rendering happens by interpolation inside the one String argument, never by an argument list. Variadic print is deliberately foreclosed, and that is what keeps the trailing-priority shape permanently unambiguous: with a variadic form, print(x, 2) could not be told from a two-value print. ⟨t-text-interp-print⟩

L-36 · It lowers to a slog hint, [11 [%slog [N %leaf tape]] k], through the pinned trip arm. ⟨t-jimp-print, t-jimp-print-first⟩

L-37 · A blind runtime computes and discards. A slog-aware runtime prints — 0 info, 1 warn >, 2 error >>, 3 crit >>>, following NockVM's Slogger — and one without the hint evaluates k regardless, so a program's value never depends on whether anyone was listening. ⟨t-text-print-crit, t-spot-print⟩

private — L-17 to L-25

L-17 · private marks the next declaration or func unexported. It registers, compiles and is fully usable inside its own module; it does not enter the module's $hlib. ⟨t-text-private-local, t-text-private-func⟩

L-18 · It is opt-out, not opt-in. There is no pub. A program that never writes the word cannot be broken by the feature existing — which is the whole reason the default is export.

L-19 · Visibility is module-scoped, following Rust: the declaring module and its descendants. That is the reading which survives multi-file modules; while a module is one file the two are indistinguishable. There is no graduated visibility, because pub(crate) has no referent here — there is no crate layer to be visible within.

L-20 · It takes only a declaration or a func. A private let refuses %parse-private, a module's lets not being exports in the first place. A private impl refuses %parse-private-impl: impls are global facts scoped by reachability, so a private one is either meaningless, the fact travelling anyway, or a coherence hole. ⟨t-err-private-let, t-err-private-impl⟩

L-21 · Nothing public may mention a private type, refused at the module as %export-private — otherwise an importer receives a name it can never resolve. ⟨t-jimp-export-private, t-jimp-export-private-field, t-jimp-export-private-alias⟩

L-22 · Both export surfaces are checked, not just signatures: an arm's signature and an exported typedef's body — a public struct Pub { f: Priv }, an enum case payload, an alias target, a trait or class method signature — and the types-only module exit as well as the ordinary one. ⟨t-jimp-private-arm, t-jimp-private-type, t-jimp-private-public⟩

L-23 · A class's state is the sanctioned exception. Construction is sealed and every method speaks Self, so the state name never crosses a boundary. This is what lets lib/rational and lib/interval keep their state structs private. ⟨t-jimp-private-class-state⟩

L-24 · Privacy is API hygiene, not a security boundary. An erasure cast reads private fields across a module boundary. Note the shape precisely, because it is easy to check and conclude the opposite: the direct r as (Int, Int) is refused %mint-cast, a private state struct not unfolding in an importer. The leak is the two-step (r as *) as! (Int, Int), which reads the fields. Making such a nominal non-congruent for as is the recorded follow-on. ⟨t-jimp-private-forge⟩

L-25 · What keeps a sealed nominal canonical is its SEAL, not its privacy. A class is constructed only by its declaring module, at every position of a cast target (docs/record/class-crossing.md). lib/rational and lib/interval rest on the seal; privacy is hygiene layered over it.

Stage one: private takes declarations and funcs. Private fields and methods are in scope and not yet built — Record record/language-arc.md.

2.3 Literals

L-77 · The table below is the literal inventory, and the mold $jatom in crates/jockc/hoon/sur/jock.hoon must match it. ⟨t-lex-str-p, t-sint-lit⟩

L-78 · A path literal is /-separated segments. /a/b/c is NORMATIVE, not indicative: it desugars to the %null-terminated chain of its segments, cast to Path, and runtime.md R-49 is the authority — Path is a data nominal over list-of-String with no knots. A bare / refuses %parse-path. ⟨t-des-path, t-parse-path, t-text-path⟩

L-189 · Date and span literals do not exist. There is no spelling, and this is an exclusion rather than a gap being filled. The types are live — Date and Span are the %date and %span auras — and lib/time is how a program gets one: it parses ISO 8601 into a Stamp, converts a Stamp to a Date and back, and transliterates the vendored Hoon's own calendar so the @da means to Urbit what it means here. A literal would be a shorter spelling of time.parse("2026-10-30"), not a capability the language is missing.

What the spelling would have to clear, recorded here because it is the actual obstacle and was nowhere: ~ is the null literal (L-89), so Hoon's ~2026.10.30 and ~d1.h2 lex as ~ followed by a numeric run and refuse %parse-trailing. A glued postfix on ~ is the mechanism that would settle it — ~ followed by an un-spaced alphanumeric run, read as one literal, with the bare ~ unchanged — and that surface is free: ~a and ~1 are parse errors today. It is the same shape the language already uses for a glued - before a decimal, which is an Int literal rather than an operator. Until it is decided, docs/ROADMAP.md holds it. ⟨t-parse-tilde-glued⟩

FormAuraExample
decimal@ (number)42, 1_024 — underscore grouping (OPEN-11); . is never a digit separator
hexadecimalHex0x2a
signedInt+42, -42 — ZigZag. Rules L-6 to L-16 below
floatFloat64 (@rd)3.14 — digit-dot-digit, both sides required, ≤ 15 digits; rounding pinned %n, no mode surface. Float16/32/128 pend need
booleanBool (logical)true, false — the beta ? type spelling is retired (OPEN-2)
char/cordchars'a'
stringString"hello"
term constant(constant atom)%foo — tag spellings match [a-z][a-z0-9]*; %1 is the numeric constant 1
date / spanDate / SpanNONE — L-189. The types are reachable by cast: 0 as Date
pathPath/a/b/c — L-78, normative; runtime.md R-49
interpolated stringString"x = {x}". Rules L-26 to L-32 below
listList<T>[1, 2, 3], [] empty — KERNEL form, typed nominally: List<join of widened elements> (heterogeneous elements are a join error); [] stays sing-null, nesting into every List<T> by N13; against a wanted List<T> the elements check against T directly (at parameterized traits)
mapMap<K->V>[1 -> 100, 2 -> 200] — brackets, told from a list by the arrow; [->] is the empty map
setSet<T>Set<@>([1, 2, 3]) — CONSTRUCTED, not spelled; Set<@>() is empty
tupleproduct(a, b, c)

Interpolation and escapes — L-26 to L-32

L-26 · {expr} renders through .show(), dispatched per head and user-extensible — impl Show for P makes {p} work. Pieces fold through the pinned cat. The Show trait and the prelude's impls for it are runtime.md's, not restated here. ⟨t-parse-interp⟩

L-27 · {{ and }} are literal braces, so "lit {{x}} ok" renders as lit {x} ok. ⟨t-text-interp-esc⟩

L-28 · A backslash escape is one BYTE, written as two hex digits. \0a is a newline, \09 a tab, either case of hex. \\ is a backslash. The five named escapes — \n \t \" \' \\ — are sugar over exactly that form: \n is \0a, per byte, with no separate mechanism. Both string paths take them, "…" and '…' alike. ⟨t-str-esc-backslash, t-str-esc-newline, t-str-esc-tab, t-str-esc-upper, t-str-esc-short⟩

L-29 · Anything else after a backslash refuses %lex-string-escape. The absence of escapes was expensive precisely because it was silent, so their presence is loud. ⟨t-str-esc-bad⟩

Aside — the hex pair collides with octal, and that is accepted. The two digits carry no x marker, so "\41" is A (L-28). In C, Python, JavaScript and Ruby the same four characters are an octal escape and mean !, since 0o41 is 33. Both readings compile and neither warns, which makes this the one divergence in the string syntax that is silent in both directions: L-29's loudness covers an unknown escape, not a known one that means something else elsewhere. Ruled 2026-10-27 — the collision stands rather than being respelled \xNN, recorded here as a known risk rather than left to be discovered. It is stated outright because a reader arriving from any of those four languages cannot guess it, and /reference/transfer/ carries it for the same reason: that page is otherwise built from refusals, and this divergence has none. ⟨t-str-esc-octal-trap⟩

L-30 · Per-byte escapes are what settle Unicode. A Jock String IS a cord and UTF-8 is a byte encoding, so "caf\c3\a9" is café. A \u{…} codepoint form could only ever be sugar over this. ⟨t-str-esc-utf8⟩

L-31 · A fragment is a single expression. No string literals inside a fragment (%lex-interp-str), and no newlines. Fragment diagnostics are fragment-relative, not whole-string-relative. ⟨t-err-interp-str, t-err-interp-open, t-err-interp-brace⟩

L-32 · Comment markers inside a string are literal text. "a // b" is six bytes of a // b, and "a /* b */ c" likewise. The lexer leaves a string body alone; comments are recognised only outside one. ⟨none — UNPINNED⟩ (verified by hand)

Int — L-6 to L-16

L-6 · Int is ZigZag @s. +n encodes as 2n and -n as 2n−1 — Hoon's own encoding, so small magnitudes stay small and the FFI arms mirror +si. ⟨t-sint-lit⟩

L-7 · An Int literal is a sign glued to a decimal, in operand position. +42 is 84 and -42 is 83. -0, +0 and 0 are the same value. ⟨t-sint-lit, t-sint-lit-neg, t-sint-lit-zero⟩

L-8 · The glued sign cannot be mistaken for arithmetic, because binary +/- are consumed at the additive level first. So a +42 is addition — with a = 5 it is 47, exactly as a + 42 is. ⟨t-sint-not-add⟩

L-9 · An Int literal is a primary, so postfix forms and casts bind tighter than the sign: -42 as @ is (-42) as @. ⟨none — UNPINNED⟩ (verified by hand, not by corpus)

L-10 · The near-misses refuse, each with its own tag. A spaced + 42 refuses %parse-stray. A spaced - 42 reaches the desugarer and refuses %sugar-op-unary, unary minus being reserved (§4.2). A sign glued to a name is the hyphen case: a -1 and a- 1 refuse %parse-minus-glue, while a-1 is a single hyphenated name (§2.1) and a - 1 is subtraction. Hex and Float64 magnitudes keep their refusals. ⟨t-lex-stray-n, t-minus-spaced, t-minus-glued-l, t-minus-glued-r⟩

L-11 · Arithmetic and comparison ride the trait route, as Float64 does — prelude impls over the hoon.ssum family. == remains %same, which ZigZag satisfies. Mixed @/Int arithmetic refuses across aura families. ⟨t-sint-sum, t-sint-sub, t-sint-mul, t-sint-cmp, t-sint-sum-mixed⟩

L-12 · Casting INTO Int refuses through all three doors, at every position, %mint-cast-sint. A cast reinterprets bits, so 42 as Int would read as +21; and the soft doors are no safer, because Int's erasure is a bare unguarded atom whose mold passes everything — so 42 as? Int and 42 as! Int refuse identically.

"At every position" was false until 2026-10-28, and this rule is the reason it is worth saying twice. The check ran on the top of the cast target only, so (5, 4) as! SB gave an Int-declared field the value -3; the as door alone was deep, because its congruence walk (+ecng/+gcng) happened to hold the rule positionwise, and the one nested vector covered that door. +sintf:mint is the shared walk now. It is pairwise: whether a position reinterprets is a fact about the source as well as the target, which is what keeps L-13's ingestion lane open underneath it. ⟨t-sint-cast, t-sint-castp, t-sint-castq, t-sint-castx, t-cast-sint-out, t-cast-sint-out-deep, t-cast-sint-deep, t-cast-sint-deep-soft, t-cast-sint-deep-test, t-cast-sint-deep-tuple, t-cast-sint-deep-same⟩

L-13 · Ingestion is the one sanctioned soft use. An untyped source — an eval product, a poke payload, anything typed * — keeps the mold path, because its bits are ZigZag by provenance. (v as? Int) ?? fallback is the idiom. The lane survives L-12's positionwise check because that check is pairwise — it refuses only where the source's aura at that position is KNOWN and is not %sint — so an untyped source stays moldable however deep the Int sits. Three rows pin it, and a blanket refusal reddens all three. ⟨t-sint-ingest, t-sint-ingestp, t-cast-sint-ingest⟩

L-14 · Conversion from a known nat is a function, not a cast. hoon.sint(n) and hoon.uint(s) convert; uint crashes %uint-neg on a negative. hoon.sabs is the total magnitude. ⟨t-sint-conv, t-sint-uint-neg⟩

L-15 · Int -> @ is general, and is a deliberate footgun. @/Atom is the top of the aura order, so an Int flows into it in every checking position — let s: @ = +2, func f() -> @ { +2 }, and a bare argument alike. It is not confined to a written annotation. +2 then reads 4, because the ZigZag encoding of +n is 2n. That is the price of atoms as the base type, and it is meant to be visible rather than papered over.

A ruling to confine this edge to identity-denoted auras is open; until it lands this rule states the compiler. Record record/language-arc.md. ⟨none — UNPINNED⟩ (genuinely unpinned: nothing in the corpus exercises the implicit edge, which is part of why confining it was thought to be an addition rather than a restriction)

L-16 · Division truncates toward zero (sfra). srem follows the dividend; sdul is the curated Euclidean modulus. ⟨t-sint-div, t-sint-rem, t-sint-dul⟩

L-79 · Separators are commas everywhere — tuples, lists, sets, arguments and type arguments all agree with the map syntax that already used them. The space-separated forms (a b), [1 2 3] and {1 2} do not parse: [1 2 3] refuses %parse-expected %\']\'. ⟨t-arc-ctor, t-arc-map-bracket, t-arc-set-ctor⟩

L-80 · A trailing comma is permitted before every closing delimiter of a comma-separated body — diff-friendly, following Rust and Swift. ⟨none — UNPINNED⟩

L-81 · A braced declaration needs no terminator. struct P { x: @ } and func f() -> @ { 1 } stand alone; the closing brace ends them. One written anyway is accepted. alias, having no braces, keeps its mandatory ; — there the terminator is the only thing saying where it ends. A trailing ; after the program's terminal expression is likewise optional. ⟨t-arc-decl-nosemi, t-arc-term-semi⟩

L-82 · Data shapes take commas; signatures take semicolons. Bodies that enumerate data — struct fields, enum cases — are comma-separated like every other enumeration. Bodies that contain signatures or definitions — trait methods, impl and class arms — are ;-terminated statements, like the declarations they are. ⟨t-arc-enum⟩

L-83 · The last separator is optional; the ones between are not. class C(S) { f(self: C) -> @ { 1 } } and class C(S) { f(self: C) -> @ { 1 }; } are the same class. A dropped separator between items is a loud parse error, %parse-expected %';', never a silent mis-parse. ⟨t-parse-method-nosep, t-text-method-nosemi, t-parse-match-nosep⟩

L-84 · match arms are comma-separated, an arm list being an enumeration of cases (§5.3), with the comma optional on the last arm and a dropped one between arms a loud %parse-expected. ⟨t-parse-match-nocomma, t-parse-match-nosep, t-arc-patt-bare⟩

L-85 · Literal-ness is lexical; constancy is a judgment. The $jatom constant flag records that a token was written as a literal. Whether a type is constant is the checker's question, not the lexer's. ⟨none — UNPINNED⟩ (the flag is q=?(%.y %.n) in $jatom, sur/jock.hoon; no vector asserts its value)

2.4 The adjacency law

L-1 · A postfix form binds only when glued

A call (, an index [, or a type application < attaches to the expression or type name before it iff no whitespace separates them. The lexer's glued flag is the whole mechanism; no rule anywhere consults anything else.

So fib(n) calls and fib (n) does not; xs[0] indexes and xs [0] does not; Map<@->@> applies and Map <@, @> does not; Point( x: 1 ) constructs.

⟨t-lex-glue-p, t-arc-ctor, t-lex-type-p⟩

L-2 · An un-glued postfix opener is an error, with a hint

Collections are comma-separated (§2.3), so nothing in the grammar is juxtaposition. An un-glued ( or [ after a complete expression is therefore never a second expression — it is a parse error naming the likely intent: %parse-unglued, hinting "f (x) does not call f; write f(x) to call, or ; to sequence".

In type position the same spacing is also an error but reports differently — List <@> refuses %parse-expected, because the type grammar fails before the adjacency check is reached. The outcome is identical; the tag is not.

⟨t-parse-unglued, t-lex-unglue-p⟩

L-3 · { is never postfix

A brace after an expression is always a block, in every syntactic context. There is no glued-brace form, so the braced constructor Point{ x: 1 } does not parse — it refuses %parse-expected. Construction is the glued paren, Point( x: 1 ).

⟨t-arc-ctor-brace⟩

L-4 · A block must produce a value, so {} refuses

Bare {} is not a collection literal and not a value; it refuses %parse-empty-brace. Empty collections are constructed or annotated — Set<@>(), or the empty map literal [->].

⟨t-parse-empty-brace⟩

L-5 · Why the law is kept

Not style. Two reasons, and both are load-bearing.

It is what makes L-2's diagnostic possible: a whitespace-blind grammar must guess what the user meant, and so cannot name the error. And it forecloses the JavaScript newline-swallow hazard, where a line break changes what the previous line meant.

It also removes an ambiguity Rust still carries. Because a brace after an expression is always a block (L-3), the Rust if a == Point { x: 1 } problem cannot be stated in Jock.

Record record/language-arc.md — OPEN-12, dissolved rather than resolved by the glued-paren move.

2.5 Punctuators and special unary forms

L-86 · The punctuator inventory is $jpunc in crates/jockc/hoon/sur/jock.hoon. One glyph leads a built-in form, always with a glued (.

L-87 · $(…) is reserved and NOT IMPLEMENTED. It names recursion re-entry with edits, inside func and loop bodies. The punctuator is reserved so the meaning can land later without reinterpreting any legal program; writing it refuses %parse-todo %recur-edits. ⟨t-recur-edits⟩

L-88 · The opcode surface forms +(e) and ?(e) are absent. Increment is the stdlib inc(e); the cell test is hoon.iscell (* -> Bool), Nock 3 through the substrate — the same arm the compiler uses, not a second mechanism. The cut spellings now refuse %parse-stray, which is unvectored. ⟨t-nx-inc-p, t-abi-iscell-cell, t-abi-iscell-atom⟩

L-89 · ~ is the null/empty literal and _ is the wildcard pattern. ⟨none — UNPINNED⟩

L-90 · -> separates argument types from return types and keys map literals. The two readings never collide, -> being a single two-byte munch read in different positions. ⟨t-arc-map-empty, t-arc-map-bracket, t-text-map-annot-put, t-lex-type-nest⟩

L-91 · & is reserved through 1.0, and refuses. It stays in the punctuator inventory, but the lexer refuses every occurrence with prose — %lex-connective, carrying the &&-habit hint — so no legal program contains it and a future meaning lands without reinterpretation. A glued || pair refuses the same way. Bitwise work is FFI: hoon.dis, hoon.con, hoon.mix. ⟨t-hint-amp, t-hint-amp-mid, t-hint-bar⟩

3. Types (surface syntax)

L-92 · Surface type expressions are $tex: parsed, positioned, and uninterpreted. Every judgment about a type happens in the checker, never in the parser — so the grammar below says what can be written, not what it means.

tex :=  TypeName                        nominal reference:      Point
     |  TypeName<tex, ...>              type application:       List<@>, Map<@->Bool>
     |  (tex, tex, ...)                 product / tuple type:   (Float64, Float64)
     |  name: tex                       named field (in decls): x: Float64
     |  tex -> tex                      function type
     |  @ | Bool | * | Atom | Float64 ...  aura names (§3.1)
     |  Base                            BasedAtom (guard semantics: types.md S3.4)
     |  tex?                            option, postfix (OPEN-2 resolved)
     |  tex | tex                       untagged discriminable union
     |  (inferred)                      omitted annotation: let x = ...

L-93 · The productions above are the whole surface. A type expression is one of: a nominal reference, a type application, a product, a named field, a function type, an aura name (§3.1), Base, a postfix option tex?, an untagged union, or omitted entirely for inference. ⟨t-lex-type-p, t-lex-type-nest⟩

L-94 · An untagged union's members must be erasure-disjoint — provably distinguishable at bounded depth, by atom-versus-cell or by distinct head tags. The discriminability judgment in types.md decides it. ⟨t-elab-union-p, t-elab-union-n⟩

L-95 · T? is sugar for the ~-form of that mechanism, so an option is an ordinary discriminable union rather than a separate kind. ⟨t-elab-unit-p⟩

L-96 · | is the type-position union punctuator and nothing else. It is retired from the operator inventory entirely to hold this single meaning. ⟨t-hint-bar⟩

3.1 Aura names

L-97 · The table below is the utterable aura set. The semantic aura vocabulary is open-ended; these are the names a program may write, and each maps to a $jatom-type. ⟨t-lex-type-p⟩

Surface names for atom auras (the utterable set; the semantic aura vocabulary is open-ended):

Surface$jatom-typeNotes
@, Atom%$the any-aura atom (top of the aura order — the parser maps the surface spelling to %$)
Nat%numberthe aura a decimal literal carries, ordered below Atom. Every Nat is an Atom, so a signature written @ takes one
Bool%logicaltrue / false; beta ? spelling retired (OPEN-2/17). The annotation resolves through the boot alias, not a bare parser mapping, so the %loob guard rides along
Hex%hex
Int%sint
Float64, Float16, Float32, Float128%real*
Byte%charsone UTF-8 byte; resolves through the boot alias (the %byte guard rides along)
UniChar%charsone Unicode scalar, 0x0–0x10FFFF. Shares Byte's aura and refines it with a wider guard, so a Byte flows into a UniChar and a UniChar does not flow back. Nothing converts silently: a scalar is one-to-four bytes, so encoding is an explicit operation
Date, Span%date, %span
String, Path(jnoun)noun types, not atoms
*, Noun—any noun (top)
Base—BasedAtom: atom < 2^64 − 2^32 + 1 (types.md S3.4; OPEN-4 resolved)

4. Expressions

4.1 Calls and access

L-98 · A call is f(a, b) — glued (, comma-separated arguments, f() for none. ⟨t-arc-ctor⟩

L-99 · Field access is p.x. The dot never needs the adjacency rule, being unambiguous either way. ⟨t-lark-param⟩

Positional access — the lark, L-100 to L-105

L-100 · p.< is the head and p.> the tail, composing left-to-right outermost-first: p.<> is the tail of the head. p.<> and p.<.> are both legal and reach the same axis. ⟨t-lark-head, t-lark-compose, t-lark-nest⟩

L-101 · The lark is the only way into an ANONYMOUS product. Structs are entered by name and lists by match; a bare tuple has neither, so it has the lark. ⟨t-lark-expr⟩

L-102 · A lark does not open a seal. types.md S7.1 records why, and what would have to change to reopen it. ⟨none — UNPINNED⟩

L-103 · The general form is p.+5 — the axis named directly, and always a literal, because a computed axis is untypeable. Lark is idiomatic for shallow, self-describing access; +n is the escape hatch for deep or generated axes. ⟨t-lark-atom-n⟩

L-104 · A bare p.5 is deliberately unavailable. The lexer reads digit-dot-digit as one Float64, so p.2.3 would tokenize as name-dot-real. The +n form exists because this one cannot. ⟨none — UNPINNED⟩

L-105 · A glued = after a lark run refuses, %parse-lark-cmp: p.>= 3 would otherwise parse as the lark p.> followed by an assignment. Write p.> >= 3 to compare, or p.> = 3 to assign. ⟨t-lark-eq-glued, t-lark-eq-glued-lt, t-lark-eq-spaced, t-lark-cmp-spaced⟩

SpellingMeaningRewrites to
e[i]index / lookupe.at(i) — the Idx trait
e[a..b]slicee.slice(a, b) — the Slice trait; half-open [a, b)
e[i] = v; ...pute.put(i, v) through %edit — the Put trait

Six further spellings were proposed and are not in the language: e[i?] (membership), e[!i] (delete), e[] (stack peek), e[!] (stack pop), e[i:j] and e[i::j] (slices). They are named methods when their collections land. They are not reserved, not deprecated, and do not parse — see the resolution below.

The index family — L-46 to L-51

L-46 · The family is exactly e[i] and e[i] = v, desugaring to .at() and .put() method calls through the prelude's Idx and Put traits. So indexing dispatches per receiver head, and xs[i] and m[k] are one rule rather than two. ⟨t-index-gone-has, t-index-gone-peek⟩

L-47 · Everything else is withdrawn. has, del and pop are named methods when their collections land, not bracket forms. ⟨t-index-gone-pop, t-index-gone-push⟩

L-48 · There is exactly one range spelling, .., it lives on the Slice trait, and List<T> is its only impl. A String has no slice at all: it must first say which view it means — bytes() or chars() — and it is the view that carries the unit, visibly, at the call, in the reader's line of sight.

The objection was never to slicing. It was to a slice pair — two spellings in the brackets, one meaning bytes and one meaning scalars — smuggling a Byte/UniChar decision into an indexing form. One spelling obeys that rather than reversing it, which is why the withdrawn e[i:j] / e[i::j] pair is not revived under new punctuation. ⟨t-text-slice, t-text-chars-slice, t-index-gone-slice⟩

L-49 · A range is half-open, [a, b), chosen for the composition law: e[0..k] and e[k..n] partition e with no arithmetic at the seam. ⟨t-text-slice-compose⟩

L-50 · Past the end crashes; an inverted range does not. Indexing past the end crashes as at does. An inverted range (b <= a) is the empty list, because a range is not an index and has nothing to be out of. ⟨t-text-slice-inverted, t-text-slice-empty⟩

L-51 · Withdrawn means EXCISED, not refused. A withdrawn spelling is a parse error — xs[1?] reports %parse-expected %']' with the caret on the ? — not a positioned desugar refusal. $index-query holds exactly the forms the language has, three of them: %at, %slice, %put. The alternative kept seven live-but-dead grammar branches, a $index-query case apiece, and a desugar arm whose only customer was its own error message — all of which 1.0 would have frozen. ⟨t-err-slice-place⟩

4.2 Operators

Binary operators are sugar: each rewrites to a stdlib call through the environment's import definitions. Which call, and how a head claims one, is the routing question — rules L-38 to L-45. This section is the grammar: what binds tighter than what, and which spellings the grammar admits at all.

L-106 · ===/!== lower to the kernel %same (Nock 5) and are never overloadable; ==/!= dispatch through Eq and fall back to %same for a head with no impl (L-45; runtime.md says why the fallback is a default rather than an error). !== desugars one step further than ===, to an %if over %same. ⟨t-des-same, t-des-nsame, t-eq-nsame⟩

L-107 · Precedence is thirteen levels, tightest first. The table is the rule; the rows are not separately numbered. ⟨t-parse-prec, t-parse-assoc, t-prec-mul, t-parse-not, t-logic-prec-or-xor, t-logic-prec-or-and, t-logic-prec-not, t-logic-prec-and-xor⟩ — the four t-logic-prec-* rows discriminate the three word stations by a grouping that answers differently when the level is wrong, which is the only way a precedence row can be pinned rather than merely exercised.

LevelOperatorsAssociativity
1postfix: glued call (, glued index [, field ., lark .< .>, axis .+n (L-46 to L-51, L-98 to L-105)left
2as, as?, as!left
3** (reserved — L-112)right
4unary - (reserved — L-112)—
5* / % × ÷ ⊛ ∩ ⊗ ∘ ·left
6+ - (binary - is spaced — L-109) ∪ ⊕ ⊖ ±left
7??right
8< > <= >= in ∈ ⊂ ⊃none
9== != === !==none
10not—
11andleft
12xorleft
13orleft
Aside — xor took its own station on 2026-10-27, and this is a re-blessing of OPEN-5's freeze rather than a correction of it. and and xor shared station 11, left-associative, so a xor b and c grouped as (a xor b) and c. C gives & ^ | three distinct levels in exactly the order and xor or, so a reader arriving from there reads a xor (b and c) — and both groupings are legal, differ in value, and say nothing. true xor true and false was false and is now true. The old grouping was pinned by t-logic-prec-and-xor deliberately, on the day it was found and before it was changed, so that moving the station had to flip a corpus row rather than pass unnoticed. Twelve levels became thirteen; nothing else moved, because the new station was inserted at the loose end and only or renumbered (12 → 13). ⟨t-logic-prec-and-xor, t-logic-prec-or-xor⟩

L-107a · A trait-only glyph takes its ASCII analogue's family. The eleven glyphs of L-116 have no ASCII spelling, so their station cannot be read off one: the parser assigns it, and the table above now names all fourteen. Multiplicative-shaped glyphs parse in +p-mul (station 5), additive in +p-add (6), relational in +p-ord (8). A library may give a glyph any meaning; it may not move it, because precedence is fixed at parse. Ten of the fourteen were missing from the table above until 2026-10-27 — the three family representatives were pinned, the rest were in the parser only. ⟨t-text-glyph-prec, t-parse-subset-nonassoc, t-text-glyph-cap-prec, t-text-glyph-ring-prec, t-text-glyph-dot-prec, t-text-glyph-ominus-prec, t-text-glyph-pm-prec, t-text-glyph-supset, t-parse-supset-nonassoc⟩ — every station row discriminates, binding the glyph over @ and setting it against the ASCII operator one family away: 2 ⊕ 3 ⊗ 4 is 14 where the drifted grouping answers 20, and the five rows added for ∩ ∘ · ⊖ ± are built to the same shape by +gstm/+gsta. ⊂ and ⊃ carry a Bool-signature positive row and a non-associativity refusal apiece. The three t-des-glyph-* vectors are desugar rows (AST in, AST out) and pin non-normalization, not placement; they are not what stations rest on.

Until 2026-10-27 six of the fourteen — ⊖ ± ∩ ∘ · ⊃ — had no vector at any stage, so two thirds of the family's table was parser source read back as prose (jock#128). The ASCII operator each glyph is set against is independently pinned at its own station, so a red row indicts the glyph and not the comparison.

L-108 · The comparisons are NON-ASSOCIATIVE — both stations, 8 and 9 — and must be parenthesized: a < b < c refuses %parse-nonassoc rather than returning a boolean surprise (the Rust rule). in sits at station 8 and is non-associative with the rest (Python's placement). Word connectives sit loosest, also Python-style, so not a == b reads not (a == b). ⟨t-parse-nonassoc, t-in-nonassoc, t-parse-subset-nonassoc, t-parse-supset-nonassoc, t-parse-not⟩

L-109 · Binary - is SPACED ON BOTH SIDES. This is an adjacency rule, not a precedence one: a- 1 and a -1 both refuse %parse-minus-glue with a hint. Fully glued a-1 is one hyphenated Term-style name (L-59), and the right-glued form is reserved for the operand-position Int literal (L-6: +42/-42). ⟨t-minus-glued-l, t-minus-glued-r, t-minus-spaced⟩

L-110 · ?? binds tighter than the comparisons and looser than + - (station 7), right-associative, following Swift's nil-coalescing placement: m[k] ?? 0 == 5 groups as (m[k] ?? 0) == 5, and a fallback may be an arithmetic expression unparenthesized. A chain falls through left to right. ⟨t-prec-qq-cmp, t-prec-qq-add, t-prec-qq-right⟩

L-111 · Every operator is BINARY-ONLY. A unary form refuses %sugar-op-unary at desugar, or %mint-op-unary if it reaches mint by another route. This is why ± has no unary reading. ⟨t-des-op-unary, t-mint-op-unary⟩

L-112 · ** and unary - are reserved through 1.0, at stations 3 and 4 — ** the tighter, so -x ** 2 will read -(x ** 2), the universal convention. Both refuse today (L-111 for the unary form), so no program can depend on the order; it is frozen now precisely so it cannot be renegotiated when they land. ⟨none — UNPINNED⟩

L-113 · Postfix . applies to ANY expression, not just limb paths: f(x).y, (a + b).len() and P( … ).v all parse. A limb receiver extends its path, so a.b.c is one unchanged %limb; any other receiver becomes the %dot sugar form, whose receiver binds once through an irrefutable match and is therefore evaluated exactly once. For a method call the whole call rides inside that binding — a method is not a first-class value. ⟨t-parse-dot, t-parse-dot-expr, t-des-dot, t-des-dot-limb⟩

L-114 · A temporary is not an assignable place. f().x = v refuses %parse-place. ⟨t-err-dot-place⟩

L-115 · Every BUILT-IN operator has an ASCII-first spelling. Three Unicode glyphs are sugar aliases that normalize at desugar to their ASCII equivalents — ∈ to in, × and ÷ to * and /. ⟨t-des-glyph, t-lex-uni-p⟩

L-116 · The other eleven glyphs are TRAIT-ONLY operators: the language lexes and parses them and assigns them no meaning — no pinned kernel arm (+opar:mint answers ~), no ASCII normalization at desugar. Each routes straight to the ops registry, so a library gives it meaning (trait Seq { then(⊛); }) or the program refuses %mint-op. ⊛ (%uni-ast, U+229B) was the first; examples/parser.jock is the worked case. ⟨t-des-glyph-ast, t-des-glyph-cup, t-des-glyph-subset⟩

L-117 · Trait-only glyphs take one of three parse stations, by mathematical convention, with the signature the station implies:

StationOperatorsAssociativityImplied signature
multiplicative (with * / %)⊛ ∩ ⊗ ∘ ·left(Self, Self) -> Self
additive (with + -)∪ ⊕ ⊖ ±left(Self, Self) -> Self
comparator (with < > <= >= in)⊂ ⊃none(Self, Self) -> Bool

So multiplicative-like glyphs bind tighter than additive-like — 2 ⊕ 3 ⊗ 4 is 2 ⊕ (3 ⊗ 4), which gives a combinator library sequencing-binds-tighter-than-alternation for free — and the relations sit with the comparisons, non-associative like them (L-108). ⊂ and ⊃ emit %operator, not %compare: they dispatch through the ops registry exactly like ⊛, borrowing only their station and implied signature from the comparators. ⟨t-text-glyph-prec, t-text-glyph-cup-chain, t-text-glyph-subset, t-parse-subset-nonassoc⟩

L-118 · A punctuator outside the vocabulary refuses %sugar-op. The operator inventory is closed at desugar, so 1 ^ 2 is an error rather than a silently unregistered hook. | is retired from the inventory entirely — it is the type-position union punctuator (§3) and nothing else. ⊂ has no type-position reading either: a subset relation on types, if ever wanted, must find another spelling or a position-split. ⟨t-des-op-bad⟩

L-119 · Glyph BYTES appear in exactly ONE place — the lexer's match tapes, spelled as numeric cords, where (trip 0x97c3) is the UTF-8 pair of ×. Everywhere else the tokens are synthetic tas constants (%uni-mul, %uni-div, …), and no multibyte literal may sit anywhere its value flows: molds, tables, comparisons, or vector texts (a vector cord carrying a glyph is spelled as a numeric atom). The reason is that a glyph literal inside compiler source is a compiler-dependent constant until honk's UTF-8 correction merges — honk collapses it to one unit, hoonc reads the byte pair, and hoonc is right. The differential acceptance lane is what caught this. ⟨t-lex-uni-p, t-lex-uni-n⟩

4.3 Casts and coalescing

p as (Float64, Float64)     // static cast:  must nest, checked at compile time
x as? Atom                  // soft cast:  produces a unit (~ or [~ value])
x as! Atom                  // forced cast:  crash on failure
(x as? Atom) ?? 99          // coalesce:  unwrap-or-default

L-120 · as is STATIC. The source type must nest in the target and nothing is checked at runtime; a cast that does not nest refuses %mint-cast at elaboration. ⟨t-mint-cast, t-run-cast, t-err-chars-cast⟩

L-121 · as? is a SOFT cast producing a unit — [~ value] on success, ~ on failure — so it is the one cast that is a question rather than an assertion. It reads a union arm as well as a mold. ⟨t-parse-asq, t-run-asq-noun, t-run-asq-miss, t-run-asq-or, t-run-asq-or-no⟩

L-122 · as! is a FORCED cast and crashes on failure. It is the spelling an adapter arm uses on an untyped * sample, where the alternative would be to thread a unit through a signature that cannot carry one. ⟨t-run-asx-mold, t-run-asx-rec⟩

L-123 · Both runtime casts check GUARDS, not just shape: a guarded type whose guard fails is a cast failure, crashing under as! and answering ~ under as?. ⟨t-run-asx-grd, t-run-asq-grd-no⟩

L-124 · A cast target must be a MOLD, not a gate. A gate in target position refuses %mint-mold, which keeps the cast forms from becoming an arbitrary call. ⟨t-mint-asx-gate⟩

L-125 · Recursive types cast, under both runtime forms: the target is the declared type, not its one-step unrolling. ⟨t-run-asx-rec, t-run-asq-rec-no⟩

L-126 · ?? unwraps a unit with a default. It is the companion to as? and the reason the soft cast needs no match: (x as? Atom) ?? 99. ⟨t-des-coalesce, t-text-qq⟩

L-127 · Nothing casts INTO Int, under any of the three forms, nor through an as? match pattern, nor inside a struct — all refuse %mint-cast-sint. Casting out of Int is permitted and yields the ZigZag encoding (+7 as @ is 14), which is the asymmetry the refusal exists to make visible: an @ has no sign to recover, so the inbound direction would be a silent reinterpretation. ⟨t-sint-cast, t-sint-castq, t-sint-castx, t-sint-castp, t-cast-sint-deep, t-cast-sint-out, t-cast-sint-out-deep⟩

The inbound refusal is today's behaviour, recorded as such. The Int confinement question (whether Int should be a confined type with explicit conversions, or an aura like any other) is open; when it is ruled, L-127 is re-blessed rather than treated as having been wrong. See ROADMAP.

4.4 Evaluation

L-128 · eval(subj, form) evaluates a noun as Nock (Nock 2). It is a kernel form, not a library call, and it is the escape hatch — spelled like what it is. ⟨t-run-eval, t-run-eval-inc, t-nx-eval-p⟩

L-129 · eval is CALL-SHAPED, like every other kernel form that takes operands: eval 5 refuses %parse-eval. Keyword-led juxtaposition exists nowhere in the language — the same rule gives lambda 5 a %parse-lambda. ⟨t-parse-eval, t-parse-lambda⟩

5. Bindings, mutation, flow

5.1 let, var, and assignment

let a: @ = 3;            // immutable binding
var b: (@, @) = (1, 2);  // mutable binding -- see L-132 on the annotation
b = (3, 4);              // edit:  legal only on var bindings (Nock 10)

L-130 · Assignment is an EDIT, not a declaration. b = e edits an existing var binding through the kernel %edit (Nock 10); assigning a let-bound name refuses %mint-edit-immutable, and an unbound name is an error too. ⟨t-mint-edit-let, t-let-no-edit⟩

L-131 · Elaboration uses the BINDING CLASS. A let of a literal keeps its singleton (constant) type, while a var widens the literal to its general aura at binding, because future edits will store other values. ⟨t-var-widen⟩

L-132 · var widening is SHALLOW. It widens the binding's own type and does not descend into cells, so singletons inside a tuple survive and var b = (1, 2); b = (3, 4) refuses %mill-nest — the second tuple's literals do not nest into the first's singleton types. An annotation is the spelling that works, as in the listing above. Deep widening for var is a recorded design question and deliberately not in this release; until it is settled, annotate a mutable tuple. ⟨t-var-widen-shallow, t-var-widen-annot⟩

L-133 · A singleton never survives a CALL boundary — the signature governs, a singleton nesting into its own aura on the way in (types.md S4). This is the structural defense against Hoon-style TMI pain. Flow-sensitive narrowing is deferred (POSSIBLES P-7). ⟨t-nest-sa-p, t-nest-sing-p, t-nest-sing-n⟩

L-134 · Destructuring let is admitted, for exactly the irrefutable pattern subset (types.md S12.2): let (h, t) = e; is the irrefutable match match e { (h, t) => … } and costs no kernel form, being the same desugaring the .-receiver sugar already uses. Only let destructures — var needs a place to assign back to and a pattern is not one, so var (a, b) = … refuses %parse-var-patt. ⟨t-arc-let-unpack, t-parse-var-patt⟩

5.2 Functions

func fib(n: @) -> @ {
  if n == 0 { 1 }
  else if n == 1 { 1 }
  else { fib(n - 1) + fib(n - 2) }
};

lambda (b: @) -> @ { b + 1 }(41)   // anonymous;  immediately called (glued paren)

L-135 · A named func is recursive by construction — it sees its own name in its body, which is how fib above calls itself. Function values are closures, one-arm cores at the ABI (types.md S9.8). ⟨t-run-grp-self, t-mint-recur-arm⟩

L-136 · A func requires an ANNOTATED return, because the recursion of L-135 needs it: func f(a) -> @ { 1 } refuses %mint-func-annot. ⟨t-mint-func-annot⟩

L-137 · Adjacent funcs form a MUTUAL-RECURSION GROUP. Consecutive func statements see each other's names in every body — the same adjacency rule declarations use (types.md S8.5/S9.1). A group lowers to one shared multi-arm core at the ABI (types.md S9.8), invisible at the surface. A duplicate name within a group refuses %mint-func-dup. ⟨t-run-grp-mutual, t-run-grp-parity, t-run-grp-three, t-run-grp-capture, t-run-grp-value, t-mint-grp-dup⟩

func even(n: @) -> Bool {
  if n == 0 { true } else { odd(n - 1) }
};
func odd(n: @) -> Bool {
  if n == 0 { false } else { even(n - 1) }
};
even(10)

L-138 · Any interposed statement SPLITS the group, and calling forward across a split is an error — never a silent scope surprise. The split is directional: a backward call still resolves, and hoisting the interposed statement above both funcs is the workaround. ⟨t-text-grp-split-decl, t-text-grp-split-let, t-text-grp-split-back, t-text-grp-hoisted⟩

L-139 · The split hint is CONDITIONAL, not detected. %mint-limb on any unresolved bare one-segment name appends "if it is a func declared below, only ADJACENT funcs see each other — move them together". +dimb receives the environment, not the statement spine, and the funcs below have not been minted, so there is nothing to detect: the note never asserts the func exists, and a misspelled name gets it too. ⟨t-diag-limb, t-drec-limb, t-chkj-refuse⟩

L-140 · recur re-enters only the innermost enclosing func or loop. Mutual calls are ordinary calls by name; a bare recur outside any such form refuses %mint-recur. ⟨t-mint-recur-naked, t-mint-recur-limb⟩

L-141 · Lambdas are monomorphic and arity-checked. A lambda assigned to an annotated function type whose arity disagrees refuses %mill-lambda-arity. ⟨t-mill-lambda-arity, t-mill-lambda⟩

Generics (semantics in types.md S9.7):

L-142 · A binder paren attaches to the declaring KEYWORD for a value declaration and to the declared NAME for a type declaration — declaration-site binds, use-site applies, exactly as value parameters already work:

func<T> id(x: T) -> T { x };
func<T: Arithmetic> double(x: T) -> T { x.add(x) };

struct Pair<T> { a: T, b: T };
enum Option<T> { none, some(T) };   // nullary case first:  Option<T> bunts to none

let p = Pair( a: 1, b: 2 );         // T inferred from arguments
let q: Option<@> = Option.some(3);  // or from the expected type

⟨t-run-gen-bound, t-text-class-gen, t-jimp-class-gen⟩

L-143 · Instantiation resolves LOCALLY — from the arguments, or in checking position from the expected type. There is no explicit call-site instantiation syntax. func and class suites may be generic (class Box<T>(BS<T>)); lambdas may not. ⟨t-as-cong-struct, t-text-class-gen-arg, t-mint-fond-inst⟩

L-144 · The type bracket is ANGLE, and it does not fight the comparison. Type arguments and binders are written <…> — List<@>, func<T>, struct Pair<T>, trait Idx<I, A> — while < and > remain the comparison operators at station 8 (L-107). The two never meet, for two independent reasons: a type-argument list is parsed in type position only (after :, after ->, inside another <…>, at a declaration head), where a comparison cannot appear; and the bracket obeys the ordinary glue law, so List<@> applies and List <@> is a parse error exactly as f (x) is. ⟨t-lex-type-p, t-lex-type-nest⟩

L-145 · Nothing in the lexer was changed to make L-144 work. < and > have always been single-byte punctuators — the only two-byte munches are ->, => and .. — so nested arguments close with a plain >> and need no C++-style > > kludge, and a glued Map<@->@>= m hands the = back correctly. .. cannot collide with a numeral, since digit grouping is _ and a real literal is digit-dot-digit, lexed on the number path: 1..2 is 1 .. 2 (that last clause is unvectored). ⟨t-lex-type-nest, t-text-map-annot-put, t-lex-num-p⟩

L-146 · class keeps PARENTHESES for its state struct (class Rat(RatState)). That is a declaration head, where no expression — and therefore no constructor call — can appear, so the glued-paren-constructs rule has nothing to collide with. ⟨t-parse-class, t-jimp-class⟩

L-147 · A generic NULLARY constructor is the one gap in L-143. let q: Option<@> = Option.none; refuses %mint-inst-annot today, because a nullary case offers no argument to solve T from and the expected-type route is unbuilt for it. The payload cases are unaffected, as is an annotated generic struct literal (let p: Pair<@> = Pair( … )); until it is built, a nullary generic case needs a monomorphic declaration. ⟨t-mint-inst-annot⟩

5.3 Conditionals and matching

if a == 3 { 42 } else { 17 }

match a {                  // literal values are patterns too
  1 => 0,
  2 => 21,
  _ => 84,
}

match a {                  // pattern/type dispatch (kernel)
  %1 => 0,
  _  => 84,
}

Arm separation is L-84 and L-83: commas, optional on the last arm, loud if dropped between arms.

L-148 · match is the eliminator, and $patt is closed: _ wildcard, literal atoms, case patterns, cell destructuring, name binding, and type tests — the last legal exactly where as? is legal (L-121). ⟨t-run-match-lit, t-run-match-cell, t-run-match-bind, t-run-match-bool, t-run-match-order, t-parse-patt, t-parse-patt-as, t-parse-patt-case⟩

L-149 · A case pattern is QUALIFIED; a bare name is a BINDING. Write .circle(r) or Shape.circle(r) to match a case, and x to bind. A bare name that happens to be a case of the scrutinee refuses %mint-patt-bare rather than being reinterpreted — because a binding matches everything, so the old coincidence rule meant adding a case to an enum could silently turn somebody's binding into a match and kill every arm below it. ⟨t-arc-patt-bare, t-arc-enum, t-mint-patt-tag, t-mint-patt-ambiguous⟩

L-150 · Exhaustiveness is CHECKED; redundancy only partly, by a cover test over the scrutinee's type (types.md S12.3): a non-exhaustive match errors with a witness pattern, and an unreachable case is an error rather than a warning.

This said "by usefulness analysis", and types.md S12.3 had already corrected itself and said so: +mcov is a conservative five-case cover test, not Maranget specialize/default matrices. It is sound for exhaustiveness and weaker for redundancy — match x { E.a => 1, E.b => 2, E.a => 3 } compiles and returns 1, because duplicate refutable cases are not detected. The two documents disagreed for as long as the correction existed, and types.md was the honest one; this rule is the goal, S12.3 is the implementation. ⟨t-mint-match-inex, t-mint-match-unreach, t-mint-match-sing⟩

L-151 · switch is GONE, and the word is NOT reserved. It was never the value-equality special case this section used to claim: both keywords shared one parser, so switch accepted case patterns, cell destructuring and type tests exactly as match does — a synonym, and two spellings for one construct is the thing the language does not do. Literal values are patterns (L-148), so match covers value dispatch with no second keyword.

This rule used to end "a retired spelling that becomes a legal variable name is worse than one that errors", and the word stayed in $keyword on that reasoning, refusing %parse-switch-gone with the rewrite named. Reversed 2026-10-29, deliberately and with the trade stated: a reservation taxes every program that would like the name, forever, while the did-you-mean serves only readers who remember a spelling that was never in a released Jock and has had zero call sites in this tree since before $patt landed. Bare switch now refuses as any unbound name does, with the limb sentence; switch a { … } refuses %parse-trailing, which is a worse sentence than the old hint and is the acknowledged price. ⟨t-switch-name⟩

L-187 · mut is an ordinary name, as self is (L-158), and for the same reason. It was never in $keyword — +p-let refused it at one site, so let mut x = …, the habit a Rust-shaped writer brings, reported %parse-let-mut with prose instead of dying as expected-'='. Unsquatted 2026-10-29 with switch, same trade: the hint bought one clear sentence for one mistake and charged every program wanting a binding called mut. let mut x = 1 is %parse-expected %'=' at x again — which is not the baffling error the hint's own note called it, since the report now carries a caret under the offending token. ⟨t-let-mut-name⟩

5.4 Loops

let a: @ = 5;
var b: @ = 0;
loop {
  if a == b + 1 { b }
  else { b = b + 1; recur }
}

L-152 · loop { … } marks the recursion point over its braced body and recur re-enters it. There is no statement-marker form. ⟨t-run-loop, t-run-loop-let, t-text-loop⟩

L-153 · Loops may be LABELED (loop outer { … recur outer; … }) and labels scope as written: an inner recur outer re-enters the outer loop, nesting resolves innermost-first, and a label that names no enclosing loop refuses %mint-recur. ⟨t-loop-label, t-loop-label-bad, t-loop-recur-inner, t-mint-recur-outer⟩

L-154 · $(…) with edits is reserved and unimplemented, refusing %parse-todo (L-87 is the reservation). ⟨t-recur-edits⟩

6. Declarations

alias Number is Float64;

struct PointState { x: Float64, y: Float64 };

enum Shape {
  point,                   // nullary case -- first, so Shape bunts to it
  circle(Float64),         // payload case:  positional, unnamed
  rect(Float64, Float64)   // multi-payload:  the payload is a (Float64, Float64) tuple
};

trait Arithmetic {
  add(self: Self, other: Self) -> Self;
  neg(self: Self) -> Self;
};

impl Arithmetic for Float64 {          // standalone:  impl Trait for Type
  add(self: Float64, other: Float64) -> Float64 { self + other };
  neg(self: Float64) -> Float64 { 0.0 - self };
};

class Point(PointState) impl Arithmetic {
  add(self: Point, other: Point) -> Point {
    Point( x: self.x + other.x, y: self.y + other.y )
  };
  neg(self: Point) -> Point { Point( x: 0.0 - self.x, y: 0.0 - self.y ) };
};

let p = Point( x: 42.0, y: 7.0 );     // construction:  glued paren (L-1)
p.x

L-155 · All declarations are ONE kernel form, %decl. The parser emits it directly and +mint registers it in the compile-time environment (def.env), minting method bodies where present (types.md S9). ⟨t-parse-decl, t-run-decl-struct, t-run-decl-alias, t-arc-alias-is, t-arc-alias-no-is⟩

L-156 · Adjacent declarations register SIMULTANEOUSLY, which is what admits mutually recursive unions and structs (types.md S9.1). ⟨t-run-decl-mutual⟩

L-157 · A redeclaration refuses, at every grain. Two declarations of one name refuse %decl-dup, and so does a redeclaration separated by an interposed statement — the compiler does not distinguish the two shapes by tag, though the corpus pins them separately. Duplicate fields, methods and tags refuse %decl-dup-field, %decl-dup-method and %decl-dup-tag. ⟨t-mint-decl-dup, t-mint-decl-redecl, t-decl-dup-field, t-decl-dup-method, t-decl-dup-tag⟩

L-158 · self is an ORDINARY parameter name, not a keyword — but its declared type must be the receiver, so m(self: @) on a class C refuses %mint-meth-self. Trait methods take it first (types.md S9.4). ⟨t-jimp-self, t-mint-meth-self, t-mint-trait-self, t-mint-meth-shape⟩

L-159 · Class state must be a DECLARED STRUCT. A non-struct in state position refuses %decl-class-state. ⟨t-mint-class-state, t-asx-class-state⟩

L-160 · EMISSION ORDER is a jet-correctness requirement, not a convenience: deterministic ordered emission is what lets a jet key on a formula hash. So class arms, impl bodies and object bodies are ordered lists, and declaration order reaches dictionary layout. ⟨t-jdict-cross-multi, t-run-match-order⟩

L-161 · A method call follows the adjacency law — p.getx(), the paren glued to the field access (L-1, L-99). ⟨t-run-meth-basic, t-text-dot-field⟩

Union declarations, fully (semantics in types.md S9.3):

L-162 · Cases are COMMA-SEPARATED in one flat list (L-82: they are data shapes), and a tag may appear once per enum. ⟨t-arc-enum, t-decl-dup-tag⟩

L-163 · Case tags are NAMES, lowercase — forced, not stylistic: tags erase to term atoms (@tas) and the uppercase namespace belongs to types (L-57). enum S { Point } refuses %parse-enum-casing, with a casing hint. ⟨t-parse-casing⟩

L-164 · CASE ORDER IS SEMANTIC: the first case is the bunt, so the convention is a nullary — or at least non-recursive — case first. enum L { nil, cons(@, L) } is accepted and the same two cases reversed are not. The refusal distinguishes the two ways a union can fail to be well-founded: a first case that contains the union while some other case does not is an ordering mistake and refuses %decl-fond-order, naming the reorder; a union with no base case at any ordering refuses %decl-fond. A third tag, %decl-fond-deep, reports the 64-deep unroll cap, which is not evidence of ill-foundedness at all. ⟨t-decl-fond-cycl, t-decl-fond-only, t-decl-fond-reorder, t-decl-fond-deep, t-mint-decl-fond, t-bunt-fond-n⟩

L-165 · A nullary case is BARE; a payload case is POSITIONAL. E.a() on a nullary case refuses %mint-ctor-nullary, and the wrong number of payload arguments refuses %mint-ctor-arity. A named payload (circle(r: Float64)) is not a declaration the grammar has: it refuses %parse-or-face at the colon, hinting to drop the face and bind the name in the match arm instead. A multi-payload case's payload is the right-nested tuple of its types. ⟨t-mint-ctor-nullary, t-mint-ctor-arity, t-run-decl-nullary, t-parse-or-face, t-parse-or-posn⟩

L-166 · Construction is QUALIFIED and APPLIED — Shape.point, Shape.circle(2.0): the same shape as the declaration, per declaration-site-binds/use-site-applies (L-142). An unknown tag refuses %mint-ctor-tag. A bare payload constructor refuses %mint-ctor-applied, a payload constructor not being a first-class value in 1.0 — and the refusal hints the eta wrapper, lambda (x: T) -> Shape { Shape.circle(x) }, which is written by hand. ⟨t-mint-ctor-tag, t-mint-ctor-applied, t-run-decl-ctor, t-qual-ctor, t-text-ctor-applied⟩

L-167 · Patterns are QUALIFIED and POSITIONAL. match s { .point => …, .circle(r) => …, .rect(w, h) => …, } — write .member or Type.member; a bare name is a binding (L-149). rect(w, h) folds to the nested cell pattern matching the payload tuple. ⟨t-run-match-sum-c, t-run-match-sum-p, t-arc-enum⟩

L-168 · Sum-of-products: CASES REFERENCE TYPES, they do not declare them. A case payload is any type — in particular a struct or class declared alongside. That is where named fields live, which is why case payloads have no faces, and behavior attaches by trait, per payload type. ⟨t-decl-or-field, t-run-match-sum-p⟩

L-169 · A declared union is itself a NOMINAL HEAD, so an impl attaches to the union as well as to each payload type — which is how a union delegates to its cases. ⟨t-run-meth-union, t-elab-union-p, t-elab-union-n⟩

L-170 · union as a keyword is RETIRED. enum is the only spelling; union C { … } refuses. ⟨t-arc-union-gone⟩

struct Circle { radius: Float64 };
struct Rect { w: Float64, h: Float64 };

enum Shape { point, circle(Circle), rect(Rect) };

trait Area { area(self: Self) -> Float64; };

impl Area for Circle {
  area(self: Circle) -> Float64 { 3.14 * self.radius * self.radius };
};
impl Area for Rect {
  area(self: Rect) -> Float64 { self.w * self.h };
};
impl Area for Shape {                    // a union is a nominal head (L-169)
  area(self: Shape) -> Float64 {
    match self {
      .point => 0.0,
      .circle(c) => c.area(),
      .rect(r) => r.area(),
    }
  };
};

let s = Shape.rect(Rect( w: 3.0, h: 4.0 ));
s.area()                              // 12.0 -- static dispatch throughout

This is the earlier Shape remodeled: there rect(Float64, Float64) carried bare payloads and destructured as rect(w, h); here the payloads are structs, each case binds one value, and the fields come through the struct. They mix freely — pick per case. The union impl matches and delegates, and the match needs no default because the checker proves the three cases exhaustive (L-150).

What deliberately does not exist is the trait-object route: §9 has it. Open-world polymorphism is a generic bound (func<T: Area> …), resolved statically at each call (L-142).

7. Interop

L-171 · import <lib>; brings a Hoon-compiled library into scope as FFI. An import parses to a plain AST node; binding happens at environment construction, governed by the runtime-library interface spec. An unknown library refuses %mint-import, a wrong arity %mint-call-arity, and a generic import %mint-inst. (Inside a module the entry scan gets there first and refuses %import-unknown, positioned — imports.md §3.) ⟨t-run-import, t-run-import-dec, t-run-import-mix, t-mint-import-unknown, t-mint-import-arity, t-mint-import-gen⟩

L-171a · lib.arm UNAPPLIED is the arm's gate (since 2026-10-28), so a module's functions are first-class across the boundary and pass to a higher-order arm of any module. Exactly two segments: an arm's gate has no fields, so lib.arm.more refuses %mint-field against the gate. A ∀-signed arm still refuses %mint-todo (clue %import-arm-gen), because a limb resolves in synthesis position and has no expected type to solve its binders from; the clue names the eta wrapper, which compiles. A name that is not an exported arm — including a module-level let, which is never exported — refuses %mint-import-arm with the module's arm list. Typing is types.md S11.2. ⟨t-jimp-arm-value, t-jimp-arm-hof, t-jimp-arm-stored, t-jdag-arm-value, t-jdag-arm-generic, t-jimp-arm-gen, t-jimp-arm-gen-eta, t-jimp-arm-value-absent, t-jimp-arm-value-deep⟩

L-172 · A local binding SHADOWS an import. import hoon; then let hoon = 5; binds the literal, not the library — imports take part in ordinary scoping and get no privileged name. The library axis also pegs correctly under later pushes. ⟨t-run-import-shadow, t-boot-import⟩

The scoping is right and the DIAGNOSTIC is not, measured 2026-10-28: naming a shadowed library's arm (import m; let m = 7; m.f(3)) refuses %mint-unimported — "reachable here but not imported; add import m;" — advice already taken on line 1. +ilib asks the frame where the name is bound for the $lib- marker, so a shadowing binding satisfies both of +unimp's conditions and "shadowed" is reported as "not imported". Both the call and the unapplied spelling reach it. Recorded here rather than left for the next person to re-derive; the fix is a compiler change, not a wording one.

L-173 · Values crossing the boundary are typed by EMBEDDING (+embed) and must be molded on return — there is no Nock 12 at the boundary. Not everything embeds: a gate, a type variable, a sum and a %ref all refuse %embed-out. ⟨t-embed-atom-p, t-embed-cell-p, t-embed-noun-p, t-embed-guard-p, t-embed-gate-n, t-embed-var-n, t-embed-sum-n, t-embed-ref-n⟩

8. Sugar rewrite index

A pointer index, not a second statement of the rules: each sugar form is normatively stated once, at the id named here, and the implementation is lib/sugar.hoon and must match. The one block that lives in this section rather than being pointed at is operator routing, below — it is about the rewrite target, which is what this section is for.

SugarRewriteStated at
assert c; eif c { e } else { crash }L-61 (the inventory)
a ?? bmatch on unitL-126
binary operatorsstdlib callsL-38 to L-45
=== / !==kernel %sameL-106
and / ornested %if (short-circuit)L-61
xor / not%if and stdlib rewritesL-61
%dot (postfix ., non-limb receiver)irrefutable match binding the receiver onceL-113
index family.at(i) / .slice(a, b) / .put(i, v)L-46 to L-51
larkhead/tail by axisL-100 to L-105
Unicode glyph aliases∈→in, ×→*, ÷→/L-115
print e; k%slog dynamic hintL-33 to L-37
string interpolation.show() per headL-26 to L-32
collection literalsconstructor calls / raw nounsL-79

Operator routing and the trait hook — L-38 to L-45

L-38 · a + b and the ordered comparisons are kernel forms, and mint routes on the LEFT operand's head. ⟨t-jimp-op-add⟩

L-39 · The pinned @ column serves the NUMERIC aura family only, and checks both operands — a non-numeric aura on either side refuses %mint-op-aura. ⟨t-err-op-aura-char, t-err-op-aura-real⟩

L-40 · Other heads dispatch through traits. "foo" + "bar" concatenates via impl Add for String. Plain auras take the pinned kernel arms directly and are not overloadable; %real and nominal heads elaborate as a.add(b) through the operator's registered trait. ⟨t-jimp-op-add-nominal, t-jimp-op-nominal⟩

L-41 · The hook is spelled on the METHOD, not the trait: trait Add { add(+); }. In that SHORT form the signature is implied by the family — arithmetic (Self, Self) -> Self, comparison (Self, Self) -> Bool — and the parser fills it in. ⟨t-decl-op-gen⟩

L-41a · A WRITTEN signature may carry an operator too, spelled after the return type: plus(self: Self, p: Span) -> Self (+);. This is the only way to give an operator a signature the short form cannot imply, and the right operand's type is the whole point of it — Stamp + Span -> Stamp is unspellable without it, and no choice of Self reaches it, a union included: one type variable cannot be the left operand, the right operand and the result at once. The wrong right operand is then an ordinary %mill-nest at the synthesized call, carrying its position, rather than a refusal of its own. ⟨t-op-het, t-op-het-nest⟩

L-42 · The hook spelling admits exactly the operator vocabulary — + - * / %, ⊛, and < > <= >= == !=. Any other punctuator refuses %parse-trait-op at parse, rather than registering silently. ⟨t-trait-op-junk⟩

L-43 · Policy stays with registration, not spelling. != refuses %decl-op-eq, being derived from ==. A second trait binding an already-claimed operator is PERMITTED, and resolved by the left operand's head: Add keeps + for every numeric head while another trait has it for one nominal. Two traits reaching ONE head is the conflict, and refuses %mint-op-amb where the operator is used rather than where it was declared. ⟨t-err-op-ne, t-op-rebind, t-op-two-heads, t-op-amb⟩

L-44 · An empty () is a nullary signature, not a hook — which the receiver rule then refuses, %decl-trait-self, trait dispatch being receiver-based. ⟨t-err-op-impl, t-err-op-impl-bool, t-err-op-impl-char⟩

L-45 · == and != stay %same. The prelude claims + - * / and the ordered comparisons, with Float64 impls; % is unclaimed and user-bindable. ⟨t-err-op-rmod⟩

Declarations (struct/enum/trait/impl/alias/class) and struct literals are not in this table: both are kernel forms (%decl, %struct-literal) because their rules need the environment (types.md S9.1, S8.12); desugar is a pure AST rewrite and stays that way.

9. What is deliberately absent

An absence is a rule like any other: something was asked for, ruled out, and now refuses or fails to parse. Each row says what the language does instead, and where the positive rule lives.

These are PERMANENT exclusions. Nothing here is waiting for a version number, and this section does not track what the language has not got round to. A feature under consideration is roadmap material — docs/ROADMAP.md for what is planned, POSSIBLES.md for what is merely arguable — and it does not appear in a normative section until it is decided. One word is reserved against a form that does not exist yet; L-188 names it, and it is the only one.

L-178 · The glyph connectives &&, || and ! are absent, the word operators and/or/xor/not taking their place. && and || refuse %lex-connective at the lexer, with a hint, rather than reaching the parser as two punctuators. ⟨t-hint-amp, t-hint-amp-mid, t-hint-bar, t-logic-and-tt, t-logic-and-tf, t-logic-and-ft, t-logic-and-ff, t-logic-or-tt, t-logic-or-tf, t-logic-or-ft, t-logic-or-ff, t-logic-xor-tt, t-logic-xor-tf, t-logic-xor-ft, t-logic-xor-ff, t-logic-not-t, t-logic-not-f, t-logic-short⟩ — the first three pin the refusal, the rest run the replacement: a complete truth table apiece, and t-logic-short carries the short circuit at the surface (false and crash answers false, where crash would refuse if it ran). Both halves are required of an absence that recommends a spelling: until 2026-10-27 this rule's hint pointed the reader at four words no vector in the corpus had ever executed (jock#129), which also made it the standing exception to L-61's own entry requirement of a positive and a negative row per keyword.

L-180 · The core algebra is not in the language. compose, object, and batteries and payloads are not user concepts. A core is a calling convention (types.md S9.8), reached by writing a closure or a trait, and a program never names one. Anonymous records are not a product with the names left on: a product is positional, a struct is named, and nothing sits between them. ⟨none — UNPINNED⟩

L-181 · There are no trait objects or existentials. any Trait does not parse. Dictionaries exist at generic call boundaries only, never as storable values: closed heterogeneity is a declared union (List<Shape>, L-169) and open polymorphism is a generic bound (L-142; types.md S9.4). ⟨none — UNPINNED⟩

L-182 · There are no exceptions, nulls, inheritance or macros. Failure is crash and the unit; optionality is T? (§3); reuse is traits and generics; and code generation is the compiler's business, not the program's. ⟨none — UNPINNED⟩

L-183 · The language has no self-hosting constructs. It cannot quote itself, stage its own evaluation, or reach its own AST. A Jock compiler written in Jock would use the same surface every other program uses, so adding a construct for it would be a language change and not a filling-in. ⟨none — UNPINNED⟩

L-188 · defer is the one reserved word. It is a $keyword, so no program can bind the name, and it refuses %parse-todo %defer wherever a statement may stand — the squat is deliberate, and it is the only one in the language. Every other spelling that refuses %parse-todo is punctuation rather than a name: $(...) re-entry edits refuse %parse-todo %recur-edits (L-157), and $ is a punctuator no program could have used for anything else. ⟨t-parse-defer, t-recur-edits⟩

10. Decision ids

Every OPEN-n decision is resolved. The deliberations — what was asked, when it was settled, and what was superseded — live in docs/record/language-arc.md. The ids are cited from crates/, tests/ and tools/, so they are permanent: never renumbered, never deleted.

What each decision settled is stated as a rule in the body. The index below says where, so a reader following a citation from the compiler lands on the rule rather than on its history.

idthe rule it settled now lives in
OPEN-1L-79 (comma separators), L-1
OPEN-2L-95 (T?), L-97 (Bool)
OPEN-3L-46 to L-51 (the index family)
OPEN-4L-97 (Base)
OPEN-5L-107 (the precedence table)
OPEN-6L-74 to L-76 (in), L-108
OPEN-7L-111 (no unary keyword)
OPEN-8L-107 — and the row is stale; read L-110 and L-112
OPEN-9L-87, L-152
OPEN-10§1 (the kernel/sugar criterion), L-135
OPEN-11L-77, L-145 (underscore grouping)
OPEN-12L-1 — dissolved by the glued-paren move
OPEN-13L-178, L-61 (word connectives)
OPEN-14L-130 to L-132 (shallow widening)
OPEN-15L-151 (switch retired)
OPEN-16L-90 (->)
OPEN-17L-95 (? confined to optionality)
OPEN-18L-88 (hoon.iscell)
OPEN-19L-129 (eval is call-shaped)
OPEN-20L-4 (bare {} is a block and refuses)
OPEN-21L-26 to L-32 (interpolation)
OPEN-22not a language decision — docs/ROADMAP.md; its ordering consequence is L-138

Shelved and promoted ideas keep their P-n ids in docs/POSSIBLES.md; the promotions are dated in the record.