on 98c19cd help sign in
jibb/docs/spec/grammar.md
342 lines 24 kB history raw
1# Jibb grammar
3Oct 29, 2026 · @iko
5The syntax the compiler accepts.
7## Lexical rules
9- **Indentation is structure**: a deeper line continues the line above; returning to an earlier indentation closes every block since. Spaces only; blank and comment-only lines don't count.
10- **Layout tokens**: a deeper line emits INDENT, the same depth NEWLINE, a shallower one a DEDENT per closed level, then NEWLINE.
11- **Open brackets**: a bracket open at line end takes one item per deeper line and closes at the first line that isn't deeper, with no closer. Brackets opened and closed on one line work as usual. A mold's `[` and `(` open the same way, with one field or mold per deeper line. A line may leave several brackets open: the deeper lines go to the last one, or to the body of the last rune, and the others close with it, so `(column ~ ~[` above its items needs no `)`.
12- **Tight forms**: `a+b`, `a.b`, `` `x ``, `|=%got` and the openers `~[`, `|=(`, `$-(` take no spaces. A `(` right after a wing is a change: `m(note=text)` changes, `m (note text)` calls. `%/` starts an application path, `%/blog/(slug)`.
13- **Spacing**: one space between items; more for alignment. That is style, which the formatter keeps: the parser takes one or more spaces between items, and allows spaces after an opener, before a closer and at the end of a line. Faces `name=value`, puns `=name` and typed path segments `[id=@ud]` take no spaces, and a rune is followed by one.
14- **Comments**: `::` to the end of the line, outside strings; inside a string, `::` is text.
15- **Names**: Hoon terms; lowercase letters, digits and hyphens, starting with a letter.
17## Literals
19| Form | Example | Type |
20| --- | --- | --- |
21| Decimal | `1.000` | `@ud`, or any aura by context; groups of three, as in Hoon |
22| Hexadecimal | `0x1f` | `@ux` |
23| Base 32 | `0v5.fdf99` | `@uv` |
24| Ship | `~zod`, `~sampel-palnet` | `@p` |
25| Date | `~2026.10.23`, `~2026.10.23..14.30.00` | `@da` |
26| Duration | `~s30`, `~h1.m30` | `@dr` |
27| Knot | `~.my-post` | `@ta` |
28| String | `"milk"`, `"Since {day}"` | `@t`, a cord; `{…}` prints by aura; tapes come from `trip`; there is no `'…'` form |
29| Term | `%add`, `%1.000`, `%$` | A union's case, or that constant atom where an atom is expected; a numeric term is `%` and a decimal as Hoon writes it, so `%1.000`, never `%1000` |
30| Loobean | `%.y`, `%.n` | `?` |
31| Null | `~` | A constant: the `~` case of any union with one, such as `(unit A)`, `(list A)`, `(map K V)` or `(set A)` |
32| Unit | `` `x `` | `(unit A)` |
33| Cell | `[a b c]`, `[text="milk" done=%.n]` | A tuple; a record when faced; a cons where a list is expected. `[a]` is `a`; `[]` is an error |
34| List | `~[1 2 3]` | `(list A)` |
35| Term-headed pair | `gap+%s2` | `[%gap %s2]`, any operand after the `+` |
36| Path | `/posts/~zod` | `path` |
37| Application path | `%/blog`, `%/blog/(slug)` | `qath`, checked against routes and static files; no query or fragment |
39Every literal reads exactly as Hoon reads it: `1000` is an error (write `1.000`), `~2026.2.31` is `~2026.3.3`, years BC and sub-second groups are dates, and `~2026.10.29..14.30`, the minute form `print-date` writes, is not a literal.
41## Expressions
43- **Call**: `f a b`; parentheses only group. A call open in parentheses puts its head and first arguments on the first line, then one argument per indented line; a `(` alone at the end of a line puts the head on the first indented line.
44- **Lists**: `~[a b c]`, or `~[` at line end with one item per line. On one line, whitespace separates items, so calls need parentheses: `~[(text "hi")]`, which the checker suggests for `~[text "hi"]`. On indented lines, each line is an item.
45- **Faced cells**: `[text="milk" done=%.n]`, or one `name=value` per line of an open bracket. A change takes one or more of the same items, as `m(done=%.y)`; `m()` is an error.
46- **Pairs and units**: `gap+%s2` and `` `x `` bind tighter than calls, so `pad+%s3 gap+%s2` is two items.
47- **Wing** `title.p`, also for imports and prelude groups, which are records: `card.card`, `space.theme.theme`, `get.by`. **Change** `m(draft="x" error=~)`.
48- **Inline gate**: `|=(p=post.blog title.p)`, or `|=(p title.p)` when the call fixes the type. A bare name in a sample always binds. `|=%got` is case `%got`'s constructor, typed by the gate expected. A bare name binds at any depth of a sample, so `|= [a=@ b]` binds `a` and `b`.
49- **Interpolation**: `{…}` holds any expression; `\{` writes a brace and `\"` a quote; no `<…>`. Strings take Hoon's escapes: `\\` writes a backslash, and `\` with two lowercase hex digits writes that byte, as `\0a` a line feed; any other `\` is an error. A string and each `{…}` close on the line they open, and `::` inside a string is text.
50- **No operators**: `eq a b`, `not x`, `gth a b` are gates.
52## Runes
54A rune's first children sit on its line, the last taking the rest of it; the others follow, indented.
56- `++` **luslus**: defines a value, at top level or as local names at the start of any body (a gate's, an arm's, a `^-`'s, a `~&`'s or a `++`'s), visible to each other and the final expression. Gates may call themselves and their siblings; other values can't depend on themselves, even through a gate, so `++ title (trim title)` is an error.
57- `+$` **lusbuc**: defines a mold, likewise: on its line, or as cases on the lines below, one per line, where several cases make a union and one is just that mold. Molds may be recursive.
58- `|=` **bartis**: a gate with a faced sample, `m=model`, `=msg` (`msg=msg`) or `[a=@ b=@]`; the body on the same line or below.
59- `?-` **wuthep**: the only branch, exhaustive; the subject on its line, then arms of a pattern and an expression or body. Conditions switch on `%.y` and `%.n`.
60- `^-` **kethep**: a cast; the mold on its line, the expression after it or below.
61- `~&` **sigpam**: prints the value on its line, then runs the body below; output goes to the ship's log or the browser's console.
62- `!!` **zapzap**: a crash, an expression of any type; as a mold, the empty mold. It is an operand, so `f !!` and `~[!!]` need no parentheses.
63- `$-` **buchep**: a gate's mold, `$-(sample product)`.
64- `|$` **barbuc**: a generic mold, right after a `+$`'s name: `+$ result |$ [V E]`, then the mold on its line or the cases below.
66Only `|=` and `$-` have wide forms: `|=(sample expr)` and `$-(mold mold)`. Their children are operands, so calls inside need parentheses.
68Left out on purpose:
70- `|%`, `|_`, `|*`: modules are records.
71- `$?`, `$%`: a union is a `+$` with its cases below.
72- `?+`, `?:`: `?-` is the branch.
73- `.=`, `?!`, `=(a b)`, `!x`: `eq` and `not` are gates.
74- `=/`: `++` defines values.
75- `|-`, `$(…)`: gates recurse by name.
76- `~|`: `~&` covers debugging.
77- `*mold`: no defaults.
78- `=>`, `=<`, `.*` and `a:b`: names never shadow, and `.` reads imports too.
79- `!>` and vase runes, `/+`: code also runs in the browser.
80- Sail's `;` runes: layout is Jibb's elements.
81- Tall forms such as `:~`, `:*`, `%:` and `%=`, the closers `==` and `--`, and other wide forms such as `^-(…)` and `?-(…)`: indentation covers them.
82- `@s…` and `@r…` auras: errors wherever used, imports included.
84## Imports
86Imports come first:
88```jibb
89/- todo :: the molds from /sur/todo.hoon this module uses
90/% %todo-action :: a mark's type, named todo-action
91/= card /ui/components/card :: a Jibb module
92```
94An imported Hoon record with duplicate field names is an error where used; imported union cases must differ as nouns. `/%` names a type after its mark, so it takes no face, and any mark name works; Hoon's `/%` binds the mark's core instead. Derived constructors such as `todo-action.todo` supply the agent and mark, so code never writes one. API modules re-export their `/%` types, not `/-` faces or `/=` modules. Import cycles are errors.
96## Modules
98A file is a module: imports, then definitions.
100- `+$ name mold` defines a mold; `++ name expr` a value.
101- Names may refer to each other in any order, and no name may shadow one already in scope, imports and the prelude included.
102- Molds and values have separate namespaces, told apart by position: `todo-action.todo` is a mold and its constructor, `todos.todo` a mold and a request.
103- `main`'s type decides the kind: `(element !!)` a page; `(page R D)` a page with data, from parameters `R` to data `D`; `(program D X M)` an island, with data, model and messages; `(store S)` a store with state `S`. They're built by `(page load view)`, `(program init update view subs)` and `(store init update)`. A module without `main` is a library.
104- Everything is exported and read through the import's face, as `card.card`. The prelude keeps private helpers in a separate internal module; it imports them without exposing them.
105- An API module, `/ui/api/<agent>.jibb`, also exports derived gates: a constructor per `poke` type, adding the agent and mark, and a request per `paths` case, taking a reply over full state, plus a fold for `(facts full diff …)`. An island that watches a request whose case has a separate `scry=` path has a type error. A store's `+$ poke` is a declaration, not a mold; a store exports a constructor per `poke` type and a `state` request. Clashing derived names are errors, including a derived name that clashes with an import face.
107Names the host reads, by file:
109| File | Names |
110| --- | --- |
111| Any module | `main`, the kind |
112| A store | `state`, `poke` |
113| `/ui/api/<agent>.jibb` | `poke`, `paths` |
114| `/ui/theme.jibb` | `theme` |
115| Under `/ui/tests` | `test-…` arms, one test each |
117Every other name is free.
119## Molds
121A mold is only a type: never called on a noun, and with no default. Types are inferred, Hindley–Milner with aura variables: every expression has a principal type, and there is no general subtyping. These one-way widenings are allowed: a value of a specific atom type fits `@`, a value of a specific noun type fits `*`, and a more specific aura fits an ancestor in its Hoon aura family; the reverse of each widening needs a check.
123- **Atoms**: `@` is any atom, regardless of aura; auras such as `@ud`, `@t`, `@p` and `@da` are specific atom types. An `@ud` value fits where `@` is expected, but an `@` value needs a check to fit `@ud`. A more specific aura also fits an ancestor in its Hoon aura family: `@ud` and `@ux` fit `@u`, and `@tD` fits `@t`. The reverse needs a check. Distinct specific auras remain distinct; sharing an ancestor does not make them unify. Each bare `@` independently accepts any atom: it is not an aura variable. Atoms that must share a type take one capital, as `[lo=A hi=A]`, with the atom constraint inferred from the body. `add` takes `[A A]` and returns `A`, with `A` constrained to atoms, so `(add n 1)` with `n=@ud` is a `@ud`. `?` is a loobean.
124- **Constants**: `%add`, `~` and the loobeans have one value each.
125- **Records and tuples**: `[text=@t done=?]`, `[@t ?]`. An empty `[]` is an error. Faces are part of the type, so `[a=@ b=@]` and `[x=@ y=@]` don't unify. An unnamed position can unify with a faced position and take its face, so `[0 0]` fits `[x=@ud y=@ud]`; two different faces don't unify. A record too long for one line can leave its `[` open, with one field per deeper line, as an expression's bracket does.
126- **Unions**: a `+$` with several cases below it. Every union written in Jibb has a name, so one used in a field, a sample, a cast, a gate's mold or an argument gets its own `+$`; only imports bring in unions without one. Tags take their union from context, as Tag resolution describes. Cases must differ as nouns: inhabited cases must differ at some position of their right-nested cells, where one is an atom and the other a cell, or both are different constants. `*`, type variables and molds that can be either, such as `(list A)`, can overlap atoms and cells. `?` counts as the two atoms `%.y` and `%.n`: it is disjoint from cells and overlaps the constants 0 and 1. The empty mold `!!` is disjoint from every case; a cell with an empty position at any depth is itself empty and is also disjoint from every case. A union whose cases don't all differ is rejected.
127- **Containers**: `(list A)`, `(unit A)`, `(set A)`, `(map K V)`, `(mop K V)`, `(qeu A)`, `(jar K V)`, `(jug K V)`.
128- **Gates**: `$-(A B)`. Type variables are capitals, generalized per `++`. A capital in a local `++` shares the enclosing `++`'s variable of the same name if the enclosing `++` also uses it; otherwise the local `++` introduces and generalizes its own variable. For example, `++ inner |= y=A y` uses the enclosing `A` when its enclosing gate has a sample `x=A`; if the enclosing `++` does not use `A`, `inner` generalizes its own `A`. Use another capital for a different type. A body can't force two distinct capitals to be equal; use the same capital wherever a type must be shared. A type variable used where an atom is required gains an atom constraint, so `++ clamp |= [lo=A hi=A n=A]`, whose body calls `gth` and `max`, works on any one aura. It can become nothing more specific: a body that makes `A` a `@ud` or a list is an error.
129- **Paths**: `/todos` is constant, `/` empty, and `/todo/[id=@ud]` typed by aura. `%/blog/(slug)` is an `qath`, checked against routes and static files.
130- **Generic molds**: `+$ result |$ [V E]` with cases below; `|$` goes only right after a `+$`'s name.
131- **Noun**: `*`, not a type variable; use a capital for a generic type. A value already known to have a more specific noun type, such as `@ud`, can be used where `*` is expected. **Empty**: `!!`.
132- **Opaque**: `poke`, `ask` and `qath`, built only by derived gates, `pure` and application-path literals. `(facts full diff …)` appears only in an API file's `paths`.
133- **Identity**: recursive molds go by name and module.
135`eq`'s arguments and map and set keys must contain no gates, even nested inside another value. The checker infers this gate-free constraint on type variables and retains it when generalizing a `++`, so a generic helper that calls `eq` stays generic over gate-free types. Printed types show the constraint as `$-([A A] ?) where A contains no gates`. An inferred atom constraint prints as `$-([A A] A) where A is an atom`. Multiple clauses are comma-separated, as `where A is an atom, B contains no gates`. Union-case matching treats gates as opaque and never inspects a gate to distinguish cases.
137## Tag resolution
139A tag literal or `~` gets a fresh type variable and a pending constraint: the variable must become a union with a matching case, or an atom type the term fits. A mold that isn't a union counts as a union of one case, for tags and for `|=%tag`: `[%go "x"]` fits `+$ go [%go url=@t]`, and `|=%typed` works against `$-(@t M)` when `M` is `[%typed text=@t]`. Once unification fixes the variable, each constraint is checked against the union's cases or, for `@tas`, `@ta`, `@` or `*`, makes the tag that constant atom. A literal that matches nothing is an error naming the union, such as "msg has no case %submitt".
141- A constraint records the literal's shape, such as `[@t %draft]`, and matches cases by shape and shared tag positions; a literal in a field position is checked against that field.
142- `|=%typed` is a case's constructor: against `$-(@t M)` it is `|=(t [%typed t])`, requiring `M` to have a case `[%typed @t]`. It works for cases tagged at their head; a bare tag is always a value.
143- A variable constrained by tag resolution, and any it mentions, doesn't generalize; it stays open until something in the module fixes it. An island's `main` fixes its messages, so islands resolve their own tags. The inferred gate-free constraint is retained when a type variable is generalized.
144- A constraint still open when its module is checked is an error asking for a `^-`; the checker never guesses. Library helpers can cast, as in `^- (list attr)`.
145- Wing access, changes and `?-` on a type not yet known wait the same way.
146- Patterns need none of this; they're checked against the subject's type.
148```jibb
149:: /ui/components/buttons.jibb, a library with no main
150++ add-button |= label=@t
151 button label %add :: error: nothing fixes the union of %add
152++ labelled |= [label=@t msg=M]
153 button label msg :: fine: the caller's union
154++ wide ^- (list attr)
155 ~[width+[%fill 1] pad+%s2] :: fine: the cast fixes the tags
156```
158## Literals by context
160Number and cell literals resolve like tags: each waits for its context and takes a default when its `++` is checked. A number takes `@ud` only if nothing fixes its aura and its `++` isn't generic over it, so `++ inc |= n=A (add n 1)` stays generic, while the sum in `"{(add 1 2)}"` is a `@ud`.
162| Literal | Can become | Default |
163| --- | --- | --- |
164| Tag `%a`, or `~` | A union's case or a constant atom | None: an error asking for a `^-` |
165| Number `5` | Any aura | `@ud` |
166| Cell `[x xs]` | A tuple, a record, a cons | A tuple |
168- In `{…}`, an atom prints by its aura: `@ud` as `1.000`, `@da` as a UTC date with sub-seconds, `@p` as a ship, and a bare `@` as `@ud`. `@t`, `@ta`, `@tas` and tapes splice as text; any other cell is an error asking for a printer. Printers are picked when compiling, so an atom whose aura its `++` is generic over, as `n` in `|= n=A` when `A` is constrained to atoms, is an error asking for an aura, here and in a `(…)` segment of an application path. For readers, `print-date` prints in the theme's zone, Urbit-style, to the minute, as `~2026.10.29..14.30`; numbers keep Hoon's form.
169- String literals are always cords, so an element needs `text`, as `(text "{n} items")`.
170- Where a list is expected, `[x xs]` is `x` in front of `xs`.
171- A typed segment prints and parses by aura, whether `[id=@ud]` in an agent's paths, a `_` route parameter typed by `load`'s sample, or a `(…)` segment of an application path; `@ta` and `@tas` are raw text.
173```jibb
174++ view |= [day=@da n=@ud]
175 column ~[gap+%s1] ~[
176 (text "Updated {day}") :: day printed as a date
177 (text "{n} items") :: n printed as @ud
178```
180## Patterns
182Arms are tried in order. Every case needs an arm or a catch-all, `*` or a bare name. An arm that earlier arms cover, such as a second `%add` or anything after `[%move * *]`, is an error, and a case that should fail says `!!`. A name in a pattern always binds anew, so compare values with `?- (eq a b)`. Patterns take terms, numbers, strings without `{…}`, ships, loobeans and `~`, not lists or pairs.
184| Pattern | Matches | Binds |
185| --- | --- | --- |
186| `%add` | The term | Nothing |
187| `%.y`, `%.n` | Yes, no | Nothing |
188| `[%add t]` | A cell headed by `%add` | `t`, the rest |
189| `[t %draft]` | A pair ending in `%draft` | `t` |
190| `[%move %left n]` | Tagged `%move`, then `%left` | `n` |
191| `~` | Null | Nothing |
192| `[~ x]` | A unit holding a value | `x` |
193| `1.000`, `"milk"`, `~zod` | That literal | Nothing |
194| `[a b]` | Any cell; `b` is the rest | `a`, `b` |
195| `*` | Anything | Nothing |
196| `x` | Anything | `x` |
198Tags can sit anywhere in a case, and a case can carry several:
200```jibb
201+$ step
202 [%move %left n=@ud]
203 [%move %right n=@ud]
204 [text=@t %draft]
205 [text=@t %sent at=@da]
206 %stop
208++ describe |= s=step
209 ?- s
210 [%move %left n] "left {n}"
211 [%move %right n] "right {n}"
212 [t %draft] t
213 [t %sent at] t
214 %stop "stop"
215```
217Leaving out `%stop` is an error naming it; to crash instead, write `%stop !!`.
219## The grammar in EBNF
221```
222(* Layout: a deeper line emits INDENT, a line at the same depth NEWLINE, and a
223 shallower one a DEDENT for each level it closes, then NEWLINE. Blank and
224 comment-only lines emit nothing. A bracket left open at the end of a line
225 takes the deeper lines below as its items and needs no closer. When a line leaves
226 several brackets open, the deeper lines go to the last one, or to the body
227 of the last rune, and the others close with it. *)
229module = { import NEWLINE } { definition NEWLINE } ;
230import = "/-" name | "/%" term | "/=" name pathlit ;
231definition = "+$" name [ generic ] ( mold | cases )
232 | "++" name ( expr | body ) ;
233generic = "|$" "[" typevar { typevar } "]" ;
234cases = INDENT mold { NEWLINE mold } DEDENT ; (* one case is that mold; more make a union *)
236body = INDENT { definition NEWLINE } expr DEDENT ;
237expr = rune | call ;
238call = operand { operand } ;
239operand = literal | qath | ref | change | pair | unit | group | list | tuple
240 | gate | ctor | pathlit | "!!" ;
241ref = wing ;
242wing = name { "." name } ;
243change = wing "(" item { item } ")" | wing "(" { item } open ; (* no space before "("; items named *)
244pair = name "+" operand ; (* [%name operand] *)
245unit = "`" operand ; (* [~ operand] *)
246group = "(" expr ")" | "(" { operand } open ;
247list = "~[" { operand } "]" | "~[" { operand } open ;
248tuple = "[" { item } "]" | "[" { item } open ;
249item = [ name "=" ] operand ;
250open = INDENT line { NEWLINE line } DEDENT ;
251line = [ name "=" ] expr ;
252qath = "%/" [ asegment { "/" asegment } ] ;
253asegment = knot | "(" expr ")" ; (* checked against the routes *)
254gate = "|=(" ( field | name ) operand ")" ; (* the one wide expression *)
255ctor = "|=" term ; (* a case's constructor *)
257rune = "|=" field ( expr | body )
258 | "^-" mold ( expr | body )
259 | "~&" expr body
260 | "?-" expr INDENT arm { NEWLINE arm } DEDENT ;
261arm = pattern ( expr | body ) ; (* every case covered; see Patterns *)
263mold = aura | "*" | "!!" | "?" | "~" | term | loobean | ref | typevar
264 | pathmold
265 | "(" ref mold { mold } ")" | "(" ref { mold } mlines
266 | "[" field { field } "]" | "[" { field } flines
267 | "$-(" mold mold ")" ;
268field = [ name "=" ] mold | "=" name ; (* =msg is msg=msg *)
269mlines = INDENT mold { NEWLINE mold } DEDENT ; (* a mold's ( left open *)
270flines = INDENT field { NEWLINE field } DEDENT ; (* a mold's [ left open *)
271pathmold = "/" [ segment { "/" segment } ] ;
272segment = knot | "[" name "=" aura "]" ;
274pattern = term | number | string | ship | loobean | "~" | "*" | name
275 | "[" pattern { pattern } "]" ;
277literal = number | string | ship | date | duration | knotlit
278 | term | loobean | "~" ;
279number = decimal | "0x" hex | "0v" base32 ;
280pathlit = "/" knot { "/" knot } ;
281term = "%" name | "%" decimal | "%&" | "%|" | "%$" ;
282loobean = "%.y" | "%.n" ;
283aura = "@" { lower } [ upper ] ;
284typevar = upper ;
285name = lower { lower | digit | "-" } ;
286knot = { lower | digit | "-" | "." | "~" | "_" } ;
287```
289## Example: a store and an island
291```jibb
292:: /ui/stores/cart.jibb
293+$ state [items=(list @t) open=?]
294+$ cart-action
295 [%add item=@t]
296 %toggle
297+$ poke cart-action
299++ main (store init update)
300++ init ^- state [~ %.n]
301++ update |= [a=cart-action s=state]
302 ?- a
303 [%add item] s(items=[item items.s])
304 %toggle s(open=(not open.s))
305```
307```jibb
308:: /ui/basket.jibb
309/= cart /ui/stores/cart
311++ main (program init update view subs)
313+$ model [cart=state.cart note=@t]
314+$ msg
315 [%noted text=@t]
316 %add
317 %toggle
318 [%got cart=state.cart]
319 [%acked err=(unit @t)]
321++ init |= ~
322 [[~ %.n] ""]
324++ update |= [=msg m=model]
325 ?- msg
326 [%noted t] [m(note=t) ~]
327 %add [m(note="") ~[[%poke (cart-action.cart [%add note.m]) |=%acked]]]
328 %toggle [m ~[[%poke (cart-action.cart %toggle) |=%acked]]]
329 [%got c] [m(cart=c) ~]
330 [%acked err] [m ~]
332++ subs |= m=model
333 ~[[%watch (state.cart |=%got)]]
335++ view |= m=model
336 column ~[gap+%s1] ~[
337 (text "{(lent items.cart.m)} items")
338 field "Item" note.m |=%noted
339 button "Add" %add
340```
342A store is written like an API file: `state`, `+$ poke` and `(store init update)`. Islands poke it with derived constructors and watch it with `(state.cart |=%got)`, whose first fact is the current state and each later fact the next. It runs in the browser, shared by the page's islands.