<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">tramaj</title><id>https://lucasdicioccio.github.io/tramaj/atom.xml</id><updated>2026-10-02T20:43:09Z</updated><entry><id>https://lucasdicioccio.github.io/tramaj/reference-language.html</id><title type="text">Tramaj — Language Reference</title><updated>2026-10-02T20:43:09Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;p&gt;&lt;em&gt;Generated from &lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/reference.md"&gt;&lt;code&gt;specs/reference.md&lt;/code&gt;&lt;/a&gt; — the repository is the canonical source, and may be ahead of this page.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="tramaj--language-reference"&gt;Tramaj — Language Reference&lt;/h2&gt;
&lt;p&gt;This is the reference for the language &lt;strong&gt;as implemented&lt;/strong&gt;. Where the earlier
drafts disagree with each other or with this document, this document wins;
&lt;code&gt;decisions.md&lt;/code&gt; records which reading of those drafts was taken and why, and
the drafts themselves are in &lt;a href="archive"&gt;&lt;code&gt;archive/&lt;/code&gt;&lt;/a&gt;. &lt;code&gt;node-json.md&lt;/code&gt; is
normative for the output wire format and is not repeated in full here.&lt;/p&gt;
&lt;p&gt;Two implementations are held to this document: &lt;code&gt;tramaj/&lt;/code&gt; (PureScript) and
&lt;code&gt;tramaj-hs/&lt;/code&gt; (Haskell). Anything below marked &lt;em&gt;implementation-defined&lt;/em&gt; is
where they are permitted to differ; everything else they must not.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="1-shape-of-the-language"&gt;1. Shape of the language&lt;/h3&gt;
&lt;p&gt;Tramaj is expression-oriented. There is one expression language, and
&lt;strong&gt;documents are values&lt;/strong&gt;: an element or a fragment is an ordinary expression
result that can be bound, passed to a lambda, returned from one, or stored in
an array.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  surface syntax
        │  parse + desugar
        ▼
    core AST  ──────────────┐
        │  evaluate         │  analyse (no evaluation)
        ▼                   ▼
      Value           imports / action keys / context holes
        │
        └── Node ──► normative JSON ──► host fold (HTML, YAML, HCL, UI, …)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The core AST is deliberately smaller than the surface. Conveniences are
lowered into it (§8) rather than given constructors of their own.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="2-core-ast"&gt;2. Core AST&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Program
  = DocumentProgram Expr
  | ExpressionProgram Expr

Expr
  = Path         root: String, fields: List&amp;lt;String&amp;gt;
  | FieldAccess  target: Expr, fields: List&amp;lt;String&amp;gt;
  | Call         function: Expr, arguments: List&amp;lt;Expr&amp;gt;
  | Lambda       parameters: List&amp;lt;String&amp;gt;, body: Expr
  | Let          name: String, value: Expr, body: Expr
  | StringLit    String
  | NumberLit    Number
  | BoolLit      Boolean
  | NullLit
  | ArrayLit     elements: List&amp;lt;Expr&amp;gt;
  | ObjectLit    fields: List&amp;lt;(String, Expr)&amp;gt;
  | Element      tag: String, attributes: List&amp;lt;Attribute&amp;gt;,
                 value: Expr, children: List&amp;lt;Expr&amp;gt;
  | Fragment     children: List&amp;lt;Expr&amp;gt;
  | Branch       condition: Expr, then: Expr, else: Expr
  | Map          collection: Expr, function: Expr
  | Filter       collection: Expr, function: Expr
  | Scan         collection: Expr, initial: Expr, function: Expr
  | Fold         collection: Expr, initial: Expr, function: Expr
  | Concat       left: Expr, right: Expr
  | Import       name: String, parameters: List&amp;lt;(String, ParamValue)&amp;gt;
  | AdaptActions target: Expr, adaptation: ActionAdaptation,
                 function: Optional&amp;lt;Expr&amp;gt;

ParamValue
  = PExpr        Expr                 -- any expression, evaluated at the import
  | PFromContext path: List&amp;lt;String&amp;gt;   -- read from the importing program's $ctx

Attribute
  = Attr         name: String, value: Expr
  | ActionAttr   event: String, key: String, payload: Expr

ActionAdaptation
  = Identity
  | Prefix       String
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Every static position is a &lt;code&gt;String&lt;/code&gt;, not an &lt;code&gt;Expr&lt;/code&gt;&lt;/strong&gt;: an import’s name, an
action’s event and key, an adaptation’s prefix, a &lt;code&gt;ctx(...)&lt;/code&gt; parameter’s path.
That is what makes §9’s analyses possible, and the grammar refuses anything
else in those positions — the restriction is enforced at parse time, not
checked later at evaluation time.&lt;/p&gt;
&lt;p&gt;There is no partial-import constructor, and partiality is not visible in the
syntax at all: an import is unsaturated when the library reads a parameter
nobody has supplied yet, which is a fact about two programs, not about this
one (§7).&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="3-values"&gt;3. Values&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Value
  = Null | Boolean | Number | String
  | Array   List&amp;lt;Value&amp;gt;
  | Object  Map&amp;lt;String, Value&amp;gt;
  | Node                        -- a document
  | Closure                     -- a lambda, with its captured environment
  | Builtin                     -- a builtin, referenced but not yet called
  | ImportResult                -- {rendered, vals}, once the library has run
  | Import                      -- an import wired up but not yet run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Arrays and objects hold &lt;code&gt;Value&lt;/code&gt;s, &lt;strong&gt;not&lt;/strong&gt; JSON — so a document can travel
inside a structure. That is what makes the JSX-children pattern work without a
separate “template value” category.&lt;/p&gt;
&lt;p&gt;JSON is required only at the boundaries where a document would be meaningless:
an attribute value, an action payload, an element’s value slot, and an
expression program’s result. A closure, a builtin, an import result, an
import that has not run, or a document reaching one of those positions is an
error, not a silent serialization.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="4-node-output"&gt;4. Node (output)&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Node
  = Text      value: Value,  annotations: Annotations
  | Element   tag: String, attributes: List&amp;lt;NodeAttribute&amp;gt;,
              value: Value, children: List&amp;lt;Node&amp;gt;, annotations: Annotations
  | Fragment  children: List&amp;lt;Node&amp;gt;, annotations: Annotations

NodeAttribute
  = Attribute name: String, value: Value
  | Action    event: String, key: String, payload: Value

Annotations = Map&amp;lt;String, JSON&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The text leaf carries a &lt;strong&gt;&lt;code&gt;Value&lt;/code&gt;&lt;/strong&gt;, so a scalar child keeps its type:
&lt;code&gt;.td($ctx.count)&lt;/code&gt; yields the number &lt;code&gt;3&lt;/code&gt;, not &lt;code&gt;&amp;quot;3&amp;quot;&lt;/code&gt;. How that becomes
characters is the host’s decision.
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Element&lt;/code&gt; carries a &lt;strong&gt;value slot&lt;/strong&gt; alongside its children, for targets that
attach a body value to a tagged node. It is &lt;code&gt;null&lt;/code&gt; unless set.
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attributes&lt;/code&gt; is an ordered list, so an element may carry &lt;strong&gt;any number&lt;/strong&gt; of
attributes and actions, and duplicate names are preserved, not merged.
&lt;/li&gt;
&lt;li&gt;Annotations have no core meaning. They are the extension point for types,
domains, provenance and host metadata, and every transformation must carry
them through unchanged.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Serialization — including the strict decoding rules — is specified in
&lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="5-surface-syntax"&gt;5. Surface syntax&lt;/h3&gt;
&lt;p&gt;Three leader characters: &lt;code&gt;.&lt;/code&gt; builds a document, &lt;code&gt;$&lt;/code&gt; reads a name, &lt;code&gt;@&lt;/code&gt; defines
one.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@count=cardinality($ctx.items)          -- bindings, one per line
@panel=(title, kids) =&amp;gt; .section(.h2($title), $kids)

.div(class: &amp;quot;panel&amp;quot;,                    -- attribute position first
     action(&amp;quot;on-click&amp;quot;, &amp;quot;save&amp;quot;, {&amp;quot;id&amp;quot;: $ctx.id}),
     value($ctx.raw),
     .h1(&amp;quot;Title&amp;quot;),                      -- then children
     .( .p(&amp;quot;a&amp;quot;), .p(&amp;quot;b&amp;quot;) ),             -- a fragment: tagless element
     map($ctx.items, (i) =&amp;gt; .li($i.name)),
     branch(.p(&amp;quot;none&amp;quot;), gt($count, 0), .p(&amp;quot;some&amp;quot;)))
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;Form&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$name&lt;/code&gt;, &lt;code&gt;$name.a.b&lt;/code&gt;&lt;/td&gt;&lt;td&gt;read a binding, then static field access&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;name(args)&lt;/code&gt;, &lt;code&gt;$name(args)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a call; the two spellings are identical&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$lib.vals.fn(1)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;the callee may be a dotted path&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;f(x).rendered&lt;/code&gt;&lt;/td&gt;&lt;td&gt;field access on a call's result&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;"text"&lt;/code&gt;, &lt;code&gt;"n: `$x`"&lt;/code&gt;&lt;/td&gt;&lt;td&gt;string literal, backtick interpolation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;123&lt;/code&gt;, &lt;code&gt;-1.5&lt;/code&gt;, &lt;code&gt;1e5&lt;/code&gt;, &lt;code&gt;1_000_000&lt;/code&gt;&lt;/td&gt;&lt;td&gt;number literal; see &lt;em&gt;Number literals&lt;/em&gt; below&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;&lt;td&gt;keyword literals&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;[a, b]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;array literal&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;{"k": v}&lt;/code&gt;, &lt;code&gt;{k: v}&lt;/code&gt;, &lt;code&gt;{foo}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;object literal; keys may be bare; &lt;code&gt;{foo}&lt;/code&gt; is shorthand for &lt;code&gt;{"foo": $foo}&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;(x, y) =&amp;gt; expr&lt;/code&gt;&lt;/td&gt;&lt;td&gt;lambda&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;({a, b: c}, y) =&amp;gt; expr&lt;/code&gt;&lt;/td&gt;&lt;td&gt;lambda with an object pattern parameter; see &lt;em&gt;Binding patterns&lt;/em&gt; in §8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@{a, b: c}=expr&lt;/code&gt;&lt;/td&gt;&lt;td&gt;binding with an object pattern; see §8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;(expr)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;grouping&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;-- text&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a comment, to the end of the line&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;a &amp;lt;&amp;gt; b&lt;/code&gt;&lt;/td&gt;&lt;td&gt;concat — the only infix operator, left-associative, lowest precedence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.tag(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;element&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;fragment&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;value(expr)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;element value slot; at most one, attribute position&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;action("event", "key", payload)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;action; attribute position, any number&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;map/filter(coll, fn)&lt;/code&gt;, &lt;code&gt;scan/fold(coll, init, fn)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;array primitives&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;branch(fallback, p1, v1, …)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;conditional&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;import("name", {k: expr, j: ctx(path)})&lt;/code&gt;&lt;/td&gt;&lt;td&gt;import; a parameter may also be left out and supplied later&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;adapt-actions(node, prefix("ns:") \| identity [, fn])&lt;/code&gt;&lt;/td&gt;&lt;td&gt;action adaptation&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Names — bindings, path segments, tags, bare keys — start with a letter and
may contain letters, digits, &lt;code&gt;_&lt;/code&gt; and internal &lt;code&gt;-&lt;/code&gt;. &lt;em&gt;Internal&lt;/em&gt; is enforced,
not merely advised: a hyphen belongs to the name only when another name
character follows it, so a name never ends in one and &lt;code&gt;$x-- note&lt;/code&gt; reads as
&lt;code&gt;x&lt;/code&gt; followed by a comment. Whitespace is insignificant except as a
separator, with one exception: a field-access &lt;code&gt;.&lt;/code&gt; must directly follow the
&lt;code&gt;)&lt;/code&gt; it applies to, so &lt;code&gt;f()&lt;/code&gt; on one line and &lt;code&gt;.div(...)&lt;/code&gt; on the next are two
separate things.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Comments.&lt;/strong&gt; &lt;code&gt;--&lt;/code&gt; begins a comment that runs to the end of the line. There
is no block form and no nesting, so a comment cannot be left unterminated
and a second &lt;code&gt;--&lt;/code&gt; inside one is simply more comment. A comment is legal
wherever a space is — above the bindings, trailing one, between an element’s
arguments, after the root — and is discarded together with the whitespace
around it, so it reaches neither the core AST nor §9’s analyses. Inside a
string literal &lt;code&gt;--&lt;/code&gt; is ordinary text: a string body is read character by
character and never passes through this rule.&lt;/p&gt;
&lt;p&gt;Being whitespace, a comment does not join the lines it sits between:
&lt;code&gt;f() -- note&lt;/code&gt; followed by &lt;code&gt;.div(...)&lt;/code&gt; on the next line is two separate
things, exactly as the uncommented version is.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Number literals.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;number = [&amp;quot;-&amp;quot;] digits [&amp;quot;.&amp;quot; digits] [(&amp;quot;e&amp;quot; | &amp;quot;E&amp;quot;) [&amp;quot;+&amp;quot; | &amp;quot;-&amp;quot;] digits]
digits = digit { [&amp;quot;_&amp;quot;] digit }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;-&lt;/code&gt; is part of the literal, not an operator (there is no arithmetic,
§11), so it must touch the first digit: &lt;code&gt;- 1&lt;/code&gt; is a parse error, and so is
a leading &lt;code&gt;+&lt;/code&gt;. &lt;code&gt;--1&lt;/code&gt; is a comment, like any other &lt;code&gt;--&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;_&lt;/code&gt; separates digits for readability and means nothing: &lt;code&gt;1_000_000&lt;/code&gt; is
&lt;code&gt;1000000&lt;/code&gt;. It is allowed only &lt;em&gt;between two digits&lt;/em&gt; — &lt;code&gt;1_&lt;/code&gt;, &lt;code&gt;1__0&lt;/code&gt;, &lt;code&gt;1_.5&lt;/code&gt;,
&lt;code&gt;1._5&lt;/code&gt; and &lt;code&gt;1e_5&lt;/code&gt; are all parse errors — and may appear in the integer,
fraction and exponent parts alike (&lt;code&gt;0.000_001&lt;/code&gt;, &lt;code&gt;1e1_0&lt;/code&gt;).
&lt;/li&gt;
&lt;li&gt;Both parts around a &lt;code&gt;.&lt;/code&gt; need at least one digit: &lt;code&gt;.5&lt;/code&gt; and &lt;code&gt;5.&lt;/code&gt; are not
numbers.
&lt;/li&gt;
&lt;li&gt;The literal denotes the double nearest to its decimal value (round to
nearest, ties to even). A literal too large for a double (&lt;code&gt;1e400&lt;/code&gt;) is a
parse error rather than an infinity; one too small rounds to zero.
&lt;/li&gt;
&lt;li&gt;There is no negative zero: a literal that denotes zero is &lt;code&gt;0&lt;/code&gt; whatever its
sign, so &lt;code&gt;-0&lt;/code&gt; and &lt;code&gt;-1e-400&lt;/code&gt; both evaluate to &lt;code&gt;0&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Attribute position before children.&lt;/strong&gt; Attributes, &lt;code&gt;action(...)&lt;/code&gt; and
&lt;code&gt;value(...)&lt;/code&gt; must all precede any child; a child first is a parse error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static positions reject interpolation.&lt;/strong&gt; A backtick inside an import name,
action event/key, adaptation prefix or object key is a parse error rather than
a literal backtick — someone writing &lt;code&gt;import(&amp;quot;lib-`$x`&amp;quot;)&lt;/code&gt; means
interpolation, and that position cannot be computed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A malformed special form is a parse error.&lt;/strong&gt; Name recognition backtracks;
the shape does not. &lt;code&gt;action(&amp;quot;on-click&amp;quot;, $computed, {})&lt;/code&gt; fails to parse rather
than quietly becoming a call to an unbound function named &lt;code&gt;action&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="program-kind"&gt;Program kind&lt;/h4&gt;
&lt;p&gt;A program is bindings followed by a root expression. &lt;code&gt;parseProgram&lt;/code&gt; reports
&lt;code&gt;DocumentProgram&lt;/code&gt; if the root is written as a document (&lt;code&gt;.tag(...)&lt;/code&gt; or
&lt;code&gt;.(...)&lt;/code&gt;) and &lt;code&gt;ExpressionProgram&lt;/code&gt; otherwise. That is a syntactic classification;
what evaluation actually produces is decided at runtime (§6).&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="6-evaluation"&gt;6. Evaluation&lt;/h3&gt;
&lt;p&gt;Evaluation is eager and deterministic, with exactly one exception (&lt;code&gt;Branch&lt;/code&gt;).
Aside from binding order, no evaluation order is prescribed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bindings.&lt;/strong&gt; &lt;code&gt;@name=expr&lt;/code&gt; lines lower to nested &lt;code&gt;Let&lt;/code&gt;s, so a binding may
reference &lt;code&gt;$ctx&lt;/code&gt; and any earlier binding, never a later one. A binding’s value
is inserted only &lt;em&gt;after&lt;/em&gt; it is evaluated, which is why &lt;strong&gt;there is no
recursion&lt;/strong&gt;: a lambda cannot call itself by its own binding name.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Closures&lt;/strong&gt; capture the environment where the lambda is evaluated. Builtins
are ordinary values in the initial environment, so one can be passed by
reference: &lt;code&gt;map($ctx.flags, $not)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Branch&lt;/strong&gt; evaluates its condition, then &lt;em&gt;only&lt;/em&gt; the selected arm. Errors in an
unreached arm never surface. This holds in every position — there is no
separate eager form. The condition must be a boolean.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Elements&lt;/strong&gt; evaluate attributes, then the value slot, then children, in
source order.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Children coercion&lt;/strong&gt; is the one rule the document side adds:&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;child value&lt;/th&gt;&lt;th&gt;contributes&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;a document&lt;/td&gt;&lt;td&gt;itself, as one child&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;an array&lt;/td&gt;&lt;td&gt;each element, recursively — this is how &lt;code&gt;map(...)&lt;/code&gt; repeats children&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;anything else JSON-able&lt;/td&gt;&lt;td&gt;one text node carrying that value unconverted&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;a closure / builtin / import result / unrun import&lt;/td&gt;&lt;td&gt;an error&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;So an array in child position is a &lt;em&gt;sibling sequence&lt;/em&gt;, not one value. An array
wanted as data belongs in an attribute or the value slot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concat&lt;/strong&gt; (&lt;code&gt;a &amp;lt;&amp;gt; b&lt;/code&gt;) is a monoid over three types, with no coercion:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;String × String → String      &amp;quot;&amp;quot;   identity
Array  × Array  → Array       []
Object × Object → Object      {}   right-biased on key collision
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mixed types are an error. Object key order is not semantically significant.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;str&lt;/code&gt;, and string interpolation.&lt;/strong&gt; &lt;code&gt;&amp;quot;n: `$x`&amp;quot;&lt;/code&gt; lowers to &lt;code&gt;Concat&lt;/code&gt; over
&lt;code&gt;str(...)&lt;/code&gt;, so &lt;code&gt;str&lt;/code&gt; decides what interpolation puts in the output. Its
rendering is normative:&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;value&lt;/th&gt;&lt;th&gt;renders as&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;string&lt;/td&gt;&lt;td&gt;itself, raw&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;null&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;""&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;boolean&lt;/td&gt;&lt;td&gt;&lt;code&gt;true&lt;/code&gt; / &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;number&lt;/td&gt;&lt;td&gt;exactly as ECMAScript's &lt;code&gt;Number::toString&lt;/code&gt; — &lt;code&gt;3&lt;/code&gt;, &lt;code&gt;1.5&lt;/code&gt;, &lt;code&gt;0.05&lt;/code&gt;, &lt;code&gt;100000000000&lt;/code&gt;, &lt;code&gt;1e+21&lt;/code&gt;, &lt;code&gt;1e-7&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;array / object&lt;/td&gt;&lt;td&gt;compact JSON, &lt;strong&gt;keys sorted&lt;/strong&gt;, numbers by the same rule, strings JSON-quoted&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Keys are sorted because key order is not semantically significant, so it must
not be observable here either. Both implementations must produce these
character for character.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="7-imports"&gt;7. Imports&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import(&amp;quot;deployment&amp;quot;, {
  name:     &amp;quot;web&amp;quot;,              -- (a) an expression, evaluated here
  replicas: ctx(spec.replicas)  -- (b) this program's $ctx.spec.replicas
})                              -- (c) anything not listed: supplied later
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three ways to supply a parameter, and the third is not writing it down.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(a)&lt;/strong&gt; Any expression. It is evaluated where the import is written, like
every other argument in the language.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(b)&lt;/strong&gt; &lt;code&gt;ctx(path)&lt;/code&gt; reads the &lt;em&gt;importing&lt;/em&gt; program’s own context, also where
the import is written. It means exactly what &lt;code&gt;$ctx.spec.replicas&lt;/code&gt; means, and
a path the context lacks is the same &lt;code&gt;PathNotFound&lt;/code&gt; — the two forms are
interchangeable at runtime and the evaluator makes no distinction between
them.&lt;/p&gt;
&lt;p&gt;The distinction is entirely static. &lt;code&gt;ctx(path)&lt;/code&gt; puts the path in a static
position in the AST, where §9’s &lt;code&gt;contextHoles&lt;/code&gt; can enumerate it directly;
the same read spelled &lt;code&gt;$ctx.spec.replicas&lt;/code&gt; is an ordinary path buried in an
arbitrary expression, indistinguishable from every other read. Writing
&lt;code&gt;ctx(...)&lt;/code&gt; is how an author says &lt;em&gt;this is a hole — count it&lt;/em&gt;, and it costs
the ability to compute the value, which is the point.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(c)&lt;/strong&gt; A parameter the import does not mention is supplied later, by calling
the import with an object of more parameters. Merging is right-biased, like
&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; on objects, so a later call overrides an earlier value for the same
name.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@panel=import(&amp;quot;panel&amp;quot;, {})
@half=$panel({&amp;quot;name&amp;quot;: &amp;quot;web&amp;quot;})
$half({&amp;quot;replicas&amp;quot;: 2}).rendered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An import runs &lt;strong&gt;when a field is read off it&lt;/strong&gt; — &lt;code&gt;.rendered&lt;/code&gt; or &lt;code&gt;.vals&lt;/code&gt; —
never where it is written. Everything before that is accumulation, which is
what lets one wired-up import serve a whole &lt;code&gt;map&lt;/code&gt;, each iteration adding its
own parameter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@row=import(&amp;quot;row&amp;quot;, {kind: ctx(row-kind)})
.ul(map($ctx.items, (item) =&amp;gt; $row({&amp;quot;title&amp;quot;: $item.title}).rendered))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A run import exposes &lt;code&gt;.rendered&lt;/code&gt; (whatever its root evaluated to — a
document or an ordinary value) and &lt;code&gt;.vals&lt;/code&gt; (its own top-level bindings).
Using the import itself where a value is expected is an error: it has not
run, so it has nothing to serialize.&lt;/p&gt;
&lt;p&gt;A library is evaluated against its parameters as its own fresh &lt;code&gt;$ctx&lt;/code&gt; — a
library’s &lt;code&gt;$ctx&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; its parameter object, which is why a library reads
&lt;code&gt;$ctx.title&lt;/code&gt; for the parameter &lt;code&gt;title&lt;/code&gt;, and why a &lt;code&gt;ctx(...)&lt;/code&gt; hole inside a
library is a hole in what its importer must pass. Import names are resolved
by a host-supplied table; where a library came from is not the language’s
business. Re-entering a library already being evaluated is reported as a
cycle, not run.&lt;/p&gt;
&lt;p&gt;Nothing checks that the parameters are &lt;em&gt;enough&lt;/em&gt; before running a library,
because nothing in the import knows what enough would be. A library that
reads a path nobody supplied fails with its own &lt;code&gt;PathNotFound&lt;/code&gt;, wrapped in
an &lt;code&gt;InLibrary&lt;/code&gt; naming the library that raised it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import(&amp;quot;panel&amp;quot;, {&amp;quot;name&amp;quot;: &amp;quot;web&amp;quot;}).rendered
  -- InLibrary &amp;quot;panel&amp;quot; (PathNotFound [&amp;quot;ctx&amp;quot;, &amp;quot;replicas&amp;quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The static counterpart is §9’s &lt;code&gt;unsuppliedParams&lt;/code&gt;, which answers the same
question without running anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Known limitation:&lt;/strong&gt; an import or &lt;code&gt;adapt-actions&lt;/code&gt; result cannot be called
directly — &lt;code&gt;import(&amp;quot;x&amp;quot;, {...})({...})&lt;/code&gt; is a parse error. Bind it first, as the
examples above do. Only field access is available as a postfix on a call.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="8-desugaring"&gt;8. Desugaring&lt;/h3&gt;
&lt;p&gt;The parser lowers each of these; none has a core constructor.&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;surface&lt;/th&gt;&lt;th&gt;core&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@a=1&lt;/code&gt; &lt;code&gt;@b=$a&lt;/code&gt; &lt;code&gt;root&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Let "a" 1 (Let "b" $a root)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;"n: &lt;/code&gt; + &lt;code&gt;`$x`&lt;/code&gt; + &lt;code&gt;!"&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Concat (Concat "n: " (str $x)) "!"&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;"a\nb"&lt;/code&gt;&lt;/td&gt;&lt;td&gt;one &lt;code&gt;StringLit&lt;/code&gt; with a real newline&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;{foo, bar: 1}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;ObjectLit [("foo", Path foo), ("bar", 1)]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;branch(f, p1, v1, p2, v2)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Branch p1 v1 (Branch p2 v2 f)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;a &amp;lt;&amp;gt; b &amp;lt;&amp;gt; c&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Concat (Concat a b) c&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.div()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Element "div" [] NullLit []&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@{a, b: c}=$x.y&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Let "a" $x.y.a (Let "c" $x.y.b …)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;({a}, n) =&amp;gt; e&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;Lambda ["#arg0", "n"] (Let "a" $#arg0.a e)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Escape sequences:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\n  \t  \r  \\  \&amp;quot;  \`  \0  \u{1F600}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;\u{...}&lt;/code&gt; is braced, so an astral code point needs no surrogate pair.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Binding patterns.&lt;/strong&gt; An &lt;code&gt;@&lt;/code&gt; binding or a lambda parameter may be an object
pattern instead of a name (decisions §17):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pattern ::= name | &amp;quot;{&amp;quot; field { &amp;quot;,&amp;quot; field } [&amp;quot;,&amp;quot;] &amp;quot;}&amp;quot;
field   ::= name | name &amp;quot;:&amp;quot; pattern
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;{title, kind: k, meta: {owner}}&lt;/code&gt; binds &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;k&lt;/code&gt; (the field &lt;code&gt;kind&lt;/code&gt;) and
&lt;code&gt;owner&lt;/code&gt;, reading each field the way &lt;code&gt;$x.title&lt;/code&gt; would: a missing field is
&lt;code&gt;PathNotFound&lt;/code&gt;, a non-object is &lt;code&gt;TypeMismatch&lt;/code&gt;, and extra fields are ignored.
A path source is read directly, so &lt;code&gt;@{a} = $ctx.item&lt;/code&gt; is &lt;code&gt;@a=$ctx.item.a&lt;/code&gt;; any
other source (a call, a literal) is evaluated once. A pattern parameter takes
one position, so arity is unchanged. Bindings are made left to right.&lt;/p&gt;
&lt;p&gt;These are parse errors: a name bound twice in one pattern, a default
(&lt;code&gt;{a = 1}&lt;/code&gt;), a rest element (&lt;code&gt;{...r}&lt;/code&gt;), an array pattern (&lt;code&gt;@[a, b]&lt;/code&gt;), an empty
pattern (&lt;code&gt;{}&lt;/code&gt;) and an annotation on a pattern (&lt;code&gt;@{a} : T = e&lt;/code&gt;). The parser
invents hidden names for the lowering (they start with &lt;code&gt;#&lt;/code&gt;, which no surface
name can); they never appear as a symbol’s &lt;code&gt;&amp;quot;binding&amp;quot;&lt;/code&gt; or in a library’s
&lt;code&gt;.vals&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Deferred:&lt;/em&gt; array patterns, defaults and rest (see decisions §17 for why).&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="9-static-analysis"&gt;9. Static analysis&lt;/h3&gt;
&lt;p&gt;All of these answer from the AST alone — no context, no library evaluation,
no host code. The three the laws ask for each have a deep variant that
follows imports through a library table and cuts cycles.&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;function&lt;/th&gt;&lt;th&gt;answers&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;staticImportNames&lt;/code&gt; / &lt;code&gt;transitiveImportNames&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which libraries this program depends on&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;staticActionKeys&lt;/code&gt; / &lt;code&gt;deepActionKeys&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which action keys it can emit&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;contextHoles&lt;/code&gt; / &lt;code&gt;deepContextHoles&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which context paths it declares as holes with &lt;code&gt;ctx(...)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;contextReads&lt;/code&gt;&lt;/td&gt;&lt;td&gt;every path it reads from its own context, &lt;code&gt;$ctx.a&lt;/code&gt; and &lt;code&gt;ctx(a)&lt;/code&gt; alike&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;unsuppliedParams&lt;/code&gt;&lt;/td&gt;&lt;td&gt;per import, the paths its library reads that the import does not supply&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;contextReads&lt;/code&gt; of a library is the shape of the parameter object it expects,
since a library’s context is its parameters. &lt;code&gt;unsuppliedParams&lt;/code&gt; is that set
minus what each import supplies — the parameters still to be saturated by a
later call (§7), computed without running anything. It attributes a read to a
parameter by the read’s first segment, and stops at the library’s own reads
rather than following that library’s imports, whose parameters it supplies
itself.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;staticActionKeys&lt;/code&gt; &lt;strong&gt;applies&lt;/strong&gt; adaptation rather than ignoring it, using the
same function the evaluator does: &lt;code&gt;adapt-actions(x, prefix(&amp;quot;user:&amp;quot;))&lt;/code&gt; over
&lt;code&gt;{save, delete}&lt;/code&gt; yields &lt;code&gt;{user:save, user:delete}&lt;/code&gt;. That is the payoff of
restricting adaptation to identity-or-prefix.&lt;/p&gt;
&lt;p&gt;These are over-approximations: keys under a &lt;code&gt;Branch&lt;/code&gt; arm that a given context
will never select are still reported, and so is a parameter a library reads
only in such an arm. A name that is imported but missing from the table is
reported too — an unresolvable dependency is what a caller wants to hear
about — though it contributes no unsupplied parameters, since what it needs
is unknowable rather than nothing.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="10-actions-and-adaptation"&gt;10. Actions and adaptation&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;action(&amp;quot;on-click&amp;quot;, &amp;quot;deploy&amp;quot;, {&amp;quot;deployment&amp;quot;: $ctx.name})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The event and key are literals; only the payload is computed. The language
assigns meaning to neither — the event &lt;em&gt;vocabulary&lt;/em&gt; is the host’s, and what is
fixed is the position, not the words allowed in it.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adapt-actions(node, prefix(&amp;quot;deployment:&amp;quot;))
adapt-actions(node, identity)
adapt-actions(node, prefix(&amp;quot;ns:&amp;quot;), fn)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Prefixes every action key in the subtree, reaching through imported programs
and supplied fragments. Adaptations compose the obvious way — &lt;code&gt;a:&lt;/code&gt; then &lt;code&gt;b:&lt;/code&gt;
gives &lt;code&gt;b:a:key&lt;/code&gt; — with no “already adapted” state. Applied to an import that
has not run, the adaptation is queued and runs on its result.&lt;/p&gt;
&lt;p&gt;The optional &lt;code&gt;fn&lt;/code&gt; sees each &lt;em&gt;already-prefixed&lt;/em&gt; action and may change only its
event type and payload; a &lt;code&gt;key&lt;/code&gt; it returns is ignored. Letting it win would
put the action vocabulary back beyond static reach, which is the whole point
of the restriction.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="11-builtins"&gt;11. Builtins&lt;/h3&gt;
&lt;p&gt;The vocabulary is fixed, not user-extensible.&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;builtin&lt;/th&gt;&lt;th&gt;notes&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cardinality(x)&lt;/code&gt;, &lt;code&gt;count(x)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;array or object size&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;str(x)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;§6's rendering; what interpolation uses&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;not(b)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;and(…)&lt;/code&gt;, &lt;code&gt;or(…)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;variadic, including zero arguments (&lt;code&gt;and()&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;or()&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;eq(a, b)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;deep equality; no cross-type coercion, so &lt;code&gt;eq(1, "1")&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;lt&lt;/code&gt;, &lt;code&gt;lte&lt;/code&gt;, &lt;code&gt;gt&lt;/code&gt;, &lt;code&gt;gte&lt;/code&gt;&lt;/td&gt;&lt;td&gt;numbers only&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;has(container, key)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;tolerant: a missing key, out-of-range index or wrong-shaped container answers &lt;code&gt;false&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;lookup(container, key, fallback)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;dynamic access; the fallback is mandatory, so this never errors&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;map&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;scan&lt;/code&gt;, &lt;code&gt;fold&lt;/code&gt;&lt;/td&gt;&lt;td&gt;also core constructors — see below&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;concat(…)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;variadic array join; every argument must be an array&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;append(arr, item)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;adds one element; an array item is added whole, not spliced&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;scan&lt;/code&gt; is &lt;code&gt;scanl&lt;/code&gt;, not &lt;code&gt;scanl1&lt;/code&gt;: its output starts with the seed and is one
longer than the input. &lt;code&gt;fold&lt;/code&gt; takes the same &lt;code&gt;(acc, item)&lt;/code&gt; step and returns
only the final accumulator.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There is no arithmetic.&lt;/strong&gt; No &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, and no numeric builtins beyond
the comparisons above — a template compares and selects, it does not compute.
Numbers reach a program through &lt;code&gt;$ctx&lt;/code&gt;, a literal, or a host-supplied library.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;map&lt;/code&gt;/&lt;code&gt;filter&lt;/code&gt;/&lt;code&gt;scan&lt;/code&gt;/&lt;code&gt;fold&lt;/code&gt; are core constructors rather than builtins
because their function argument needs a fresh binding per element. &lt;code&gt;branch&lt;/code&gt; is
a core constructor because it must leave an arm unevaluated, which no builtin
can do.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="12-errors"&gt;12. Errors&lt;/h3&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;error&lt;/th&gt;&lt;th&gt;when&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;UnboundName&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a name that is not bound and not a builtin&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;PathNotFound&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a field the value does not have; carries the path as written&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;TypeMismatch&lt;/code&gt;&lt;/td&gt;&lt;td&gt;wrong type, wrong arity, a non-callable callee, a value that cannot cross a JSON boundary&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ConcatMismatch&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; over two different types&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;UnknownLibrary&lt;/code&gt;&lt;/td&gt;&lt;td&gt;an import name the host table does not resolve&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ImportCycle&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a library re-entered while already being evaluated&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;InLibrary&lt;/code&gt;&lt;/td&gt;&lt;td&gt;wraps whatever an imported library failed with, naming that library; nests through a chain of imports&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h3 id="13-implementation-defined"&gt;13. Implementation-defined&lt;/h3&gt;
&lt;p&gt;Programs must not depend on any of these.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Object key order&lt;/strong&gt;, in values and in the serialized &lt;code&gt;Node&lt;/code&gt;. &lt;code&gt;str&lt;/code&gt; is the
exception: it sorts, so it is deterministic.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluation order&lt;/strong&gt;, beyond binding order and &lt;code&gt;Branch&lt;/code&gt;’s laziness.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Error message text.&lt;/strong&gt; The error &lt;em&gt;kinds&lt;/em&gt; above are stable; their prose is
not.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number precision.&lt;/strong&gt; Numbers are doubles. Beyond 2^53 integers are not
exact, and &lt;code&gt;str&lt;/code&gt; renders whatever double survived.
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3 id="14-open"&gt;14. Open&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Array patterns, defaults and rest in binding patterns (§8).
&lt;/li&gt;
&lt;li&gt;Calling a call’s result directly (§7).
&lt;/li&gt;
&lt;li&gt;Types, domains and constraints. The AST is built to accept them: node
annotations are where derived type/domain information goes, and the
semantics avoid equating “unknown” with &lt;code&gt;null&lt;/code&gt;, so a constraint-aware
evaluator can reuse this AST rather than forking the language. The symbolic
and constraint half of that is specified in &lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;
and implemented in both hosts; nominal types are specified in
&lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt; — still a draft, not frozen, but implemented in
both hosts through roadmap-to-v4 Phase 15 (tooling included: both the CLI
and the browser playground surface the &lt;code&gt;&amp;quot;types&amp;quot;&lt;/code&gt;/&lt;code&gt;&amp;quot;type-constraints&amp;quot;&lt;/code&gt;
envelope fields). v4 shipped as a &lt;strong&gt;minor&lt;/strong&gt; bump — &lt;code&gt;type X = ...&lt;/code&gt;
declarations and &lt;code&gt;@x : T = e&lt;/code&gt; annotations both fit inside the existing
&lt;code&gt;Let&lt;/code&gt;/&lt;code&gt;Emit&lt;/code&gt; chain, so &lt;code&gt;Program&lt;/code&gt;’s own shape (&lt;code&gt;DocumentProgram | ExpressionProgram&lt;/code&gt;) never changed and no host that pattern-matches it is
affected (roadmap-to-v4 Phase 15’s version decision).
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/reference-language.html" rel="alternate"/><summary type="text">Generated from [`specs/reference.md`](https://github.com/lucasdicioccio/tramaj/blob/main/specs/reference.md) — the repository is the canonical source.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/reference-v4-types.html</id><title type="text">Tramaj v4 — Types (draft)</title><updated>2026-10-02T20:43:09Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;p&gt;&lt;em&gt;Generated from &lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/v4-types.md"&gt;&lt;code&gt;specs/v4-types.md&lt;/code&gt;&lt;/a&gt; — the repository is the canonical source, and may be ahead of this page.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="tramaj-v4--types-draft"&gt;Tramaj v4 — Types (draft)&lt;/h2&gt;
&lt;p&gt;Status: &lt;strong&gt;draft, not frozen, implemented.&lt;/strong&gt; Split out of the v3 design so
that &lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt; could be frozen without waiting on the
type questions. The design itself is still not settled — the gate in
roadmap-to-v4’s Part II was crossed by explicit choice (“no host yet, proceed
anyway”), not by a host actually hurt by a plain-string &lt;code&gt;has-type&lt;/code&gt; argument,
so the six items in §11 remain open and this document remains a draft that
could still change under real use. What follows §11 is nonetheless
implemented, byte-identically, in both hosts (roadmap-to-v4 Phases 8-14):
parsing, resolution and canonical identity, type parameters through imports,
annotation erasure, &lt;code&gt;!type-constraint&lt;/code&gt;, the &lt;code&gt;&amp;quot;types&amp;quot;&lt;/code&gt;/&lt;code&gt;&amp;quot;type-constraints&amp;quot;&lt;/code&gt;
output lists, and the static analyses in §9.&lt;/p&gt;
&lt;p&gt;v3 is complete without this document. A constraint argument may be any JSON
value, so a v3 program can already say &lt;code&gt;!constraint(&amp;quot;has-type&amp;quot;, $d, &amp;quot;Deployment&amp;quot;)&lt;/code&gt; and a host may give that whatever meaning it likes. What v4
adds is everything that string cannot do: &lt;strong&gt;resolution&lt;/strong&gt;, so a name means a
declaration rather than itself; &lt;strong&gt;parameterisation&lt;/strong&gt;, so a library can export
a type it does not fully know; &lt;strong&gt;an algebra&lt;/strong&gt;, so types can be built rather
than only named; and &lt;strong&gt;identity&lt;/strong&gt;, so two spellings of the same type are the
same type.&lt;/p&gt;
&lt;p&gt;The thesis is unchanged from v3 §0 — &lt;em&gt;Tramaj fixes positions and identity; the
host owns vocabulary and meaning.&lt;/em&gt; So v4 still contains &lt;strong&gt;no typechecker&lt;/strong&gt;. It
resolves references, normalises type expressions, collects declarations and
constraints, and refuses to leave a hole unfilled. It checks nothing against
anything. The phase order is the point:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;parse  -&amp;gt;  analyse  -&amp;gt;  evaluate  -&amp;gt;  someone else checks
           ^ types live entirely here      ^ a-priori accepted, may be rejected
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A program that reaches evaluation has a complete, resolved, hole-free set of
types. Whether those types are &lt;em&gt;true&lt;/em&gt; of the values is a question for a
checker that runs on the concrete output, and Tramaj never asks it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="0-two-realms"&gt;0. Two realms&lt;/h3&gt;
&lt;p&gt;v3 gave values a leader for “supplied from outside”: &lt;code&gt;?&lt;/code&gt;. Types get their own,
&lt;code&gt;%&lt;/code&gt;, and the two realms stay separate.&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;values&lt;/th&gt;&lt;th&gt;types&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;leader&lt;/td&gt;&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;supplied by&lt;/td&gt;&lt;td&gt;the &lt;strong&gt;host&lt;/strong&gt;, after evaluation&lt;/td&gt;&lt;td&gt;the &lt;strong&gt;caller&lt;/strong&gt;, before evaluation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;identity&lt;/td&gt;&lt;td&gt;allocation site + key (v3 §1.4)&lt;/td&gt;&lt;td&gt;the normal form (§3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;may remain open&lt;/td&gt;&lt;td&gt;yes — that is the output&lt;/td&gt;&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt; — unfilled is an error (§4)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;constraints&lt;/td&gt;&lt;td&gt;&lt;code&gt;!constraint(...)&lt;/code&gt;, evaluated&lt;/td&gt;&lt;td&gt;&lt;code&gt;!type-constraint(...)&lt;/code&gt;, static (§5)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;lives until&lt;/td&gt;&lt;td&gt;the host resolves it&lt;/td&gt;&lt;td&gt;erased before evaluation (§7)&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;The asymmetry in row three is the whole reason they are separate. A symbol is
&lt;em&gt;meant&lt;/em&gt; to survive into the output — an unresolved symbol is the deliverable.
A type is not: it is scaffolding for a checker, and scaffolding with a gap in
it is a bug, not a product.&lt;/p&gt;
&lt;p&gt;Unifying the two — symbolic types constrained the way symbolic values are, so
that a host could &lt;em&gt;choose&lt;/em&gt; a type the way it chooses a value — is a research
direction, noted in §11 and deliberately not taken here.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="1-the-algebra"&gt;1. The algebra&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;TypeExpr
  = Prim     name: String                              -- string number bool null document
  | Array    element: TypeExpr                         -- [T]
  | Record   fields: Map&amp;lt;String, TypeExpr&amp;gt;             -- { a : T, b : U }
  | Union    arms:   Map&amp;lt;String, Optional&amp;lt;TypeExpr&amp;gt;&amp;gt;   -- | A T | B U | C
  | Ref      library: String, name: String,            -- a declaration, with its
             arguments: Map&amp;lt;String, TypeExpr&amp;gt;          --   type arguments
  | Var      path: List&amp;lt;String&amp;gt;                        -- %ctx.a.b  -- a hole
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Six constructors, closed. There is no function type, no type-level
abstraction, and no way for a type to depend on a value.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The primitives are exactly the value domain’s own shapes&lt;/strong&gt; and nothing else.
&lt;code&gt;string&lt;/code&gt;, &lt;code&gt;number&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;document&lt;/code&gt; are what ref §3 already
fixes, so Tramaj can be said to know them without learning anything new.
&lt;code&gt;Int&lt;/code&gt; is deliberately absent: numbers are doubles and their precision past
2^53 is implementation-defined (ref §13), so an integer type is a host-side
refinement of &lt;code&gt;number&lt;/code&gt;, not a language primitive. Building it in would oblige
both implementations to agree on a boundary the value domain does not have.&lt;/p&gt;
&lt;h4 id="11-declarations"&gt;1.1 Declarations&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;type Point   = { x : number, y : number }
type Shape   = | Circle { r : number } | Square { s : number }
type UserId  = string
type Message = { to : string, payload : %ctx.payload }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Declarations are nominal.&lt;/strong&gt; &lt;code&gt;type UserId = string&lt;/code&gt; does not make &lt;code&gt;UserId&lt;/code&gt;
and &lt;code&gt;string&lt;/code&gt; the same type; it declares a new one whose definition is
&lt;code&gt;string&lt;/code&gt;. Newtyping is therefore not a separate feature — it is what a
declaration &lt;em&gt;is&lt;/em&gt;, and &lt;code&gt;type OrderId = string&lt;/code&gt; is a different type from
&lt;code&gt;UserId&lt;/code&gt; for the reason a Haskeller expects.
&lt;/li&gt;
&lt;li&gt;Nominal identity is &lt;strong&gt;&lt;code&gt;(library, name)&lt;/code&gt; plus arguments&lt;/strong&gt; (§3), where
&lt;code&gt;library&lt;/code&gt; is the host table key, not the binding. &lt;code&gt;@a=import(&amp;quot;foo&amp;quot;,{})&lt;/code&gt; and
&lt;code&gt;@b=import(&amp;quot;foo&amp;quot;,{})&lt;/code&gt; give &lt;code&gt;$a.types.Card&lt;/code&gt; and &lt;code&gt;$b.types.Card&lt;/code&gt; the same
identity, because they &lt;em&gt;are&lt;/em&gt; the same type. Since import names are table
keys, aliasing is impossible.
&lt;/li&gt;
&lt;li&gt;Records and unions are &lt;strong&gt;structural&lt;/strong&gt; where they appear anonymously, and get
nominal identity only by being the definition of a declaration. &lt;code&gt;{x: number}&lt;/code&gt; written in two annotations is one type; &lt;code&gt;Point&lt;/code&gt; and &lt;code&gt;Vec2&lt;/code&gt; with
identical bodies are two.
&lt;/li&gt;
&lt;li&gt;A union arm may carry a payload or not. &lt;code&gt;| Dev | Staging | Prod&lt;/code&gt; is an
enum — the case that matters most, because a hole of that type is a
variable with a finite domain, which is precisely what a solver can search
and a form renderer can render.
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="12-what-tramaj-does-not-say-about-a-union"&gt;1.2 What Tramaj does not say about a union&lt;/h4&gt;
&lt;p&gt;Nothing about how a value inhabits an arm. Tramaj declares the arms; whether
the host reads a &lt;code&gt;&amp;quot;tag&amp;quot;&lt;/code&gt; field, a wrapper object, or a discriminating shape is
the host’s convention, on the same footing as what an action key means. This
is the one place the thesis has real teeth: the language could mandate a
discriminator field, but it could not enforce one — the values come from
&lt;code&gt;$ctx&lt;/code&gt; or from a solver — so mandating it would buy an opinion without the
means to back it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="2-type-parameters-are-import-parameters"&gt;2. Type parameters are import parameters&lt;/h3&gt;
&lt;p&gt;This is the load-bearing decision. A library is parameterised over types the
same way it is parameterised over values: through its context.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- library &amp;quot;message&amp;quot;
type Envelope = { to : string, payload : %ctx.payload }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;%ctx.payload&lt;/code&gt; is a &lt;strong&gt;type variable&lt;/strong&gt;: the type named &lt;code&gt;payload&lt;/code&gt; in this
library’s context. The importer supplies it in the ordinary params record,
marked with &lt;code&gt;%&lt;/code&gt; because a type is not a value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@msg = import(&amp;quot;message&amp;quot;, {payload: %Json})
@m : $msg.types.Envelope = ?(&amp;quot;m&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything the import machinery already does applies unchanged:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Partial application&lt;/strong&gt; (ref §7c). &lt;code&gt;@msg = import(&amp;quot;message&amp;quot;, {})&lt;/code&gt; is a
partial import whose &lt;code&gt;Envelope&lt;/code&gt; is a &lt;em&gt;partial&lt;/em&gt; type. Saturating the import
saturates the type. “Three partially-applied arguments for one and two for
the other” falls out of a mechanism that already exists, rather than from a
type-level arity discipline.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deferral.&lt;/strong&gt; &lt;code&gt;import(&amp;quot;inner&amp;quot;, {payload: %ctx.payload})&lt;/code&gt; forwards &lt;em&gt;this&lt;/em&gt;
library’s type variable to the inner one — the exact type-level reading of
&lt;code&gt;ctx(path)&lt;/code&gt;, and the answer to §6’s question.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;unsuppliedParams&lt;/code&gt;&lt;/strong&gt; (ref §9) already computes what is missing; §9 below
adds the type-side split.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So there is no new parameterisation mechanism, no type-level lambda, no arity
to check, and no second notion of “partially applied”. There is also,
consequently, &lt;strong&gt;no higher-kinded anything&lt;/strong&gt; — a library is not a type, it
cannot be passed, and &lt;code&gt;%ctx.f&lt;/code&gt; cannot be applied to arguments. Tramaj is
about constructing values; this is the ceiling and it is a deliberate one.&lt;/p&gt;
&lt;h4 id="21-the-two-channels-share-one-record"&gt;2.1 The two channels share one record&lt;/h4&gt;
&lt;p&gt;Syntactically there is one params record; semantically there are two channels,
split statically by the &lt;code&gt;%&lt;/code&gt;. An entry written &lt;code&gt;%T&lt;/code&gt; is a type argument, and
&lt;code&gt;typeParams&lt;/code&gt; (§9) — the &lt;code&gt;%ctx.*&lt;/code&gt; paths a library’s declarations mention — says
which keys those are. It is an error for the same key to be read as both, so
the two namespaces do not overlap in practice even though they share a
record.&lt;/p&gt;
&lt;p&gt;The alternative, a second record on &lt;code&gt;import&lt;/code&gt;, was rejected: it would duplicate
partial application, deferral and the unsupplied analysis at the type level,
and change the arity of the one form every program uses.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="3-identity-is-a-canonical-string"&gt;3. Identity is a canonical string&lt;/h3&gt;
&lt;p&gt;Two types are the same type iff their &lt;strong&gt;canonical ids&lt;/strong&gt; are equal — that is,
type equality is string equality, and no unifier, subsumption order or
occurs-check exists anywhere in the language.&lt;/p&gt;
&lt;p&gt;The normal form resolves every &lt;code&gt;Ref&lt;/code&gt; to its &lt;code&gt;(library, name)&lt;/code&gt; and every
argument recursively, sorts record fields, union arms and argument maps by
name, and &lt;strong&gt;stops at declaration boundaries&lt;/strong&gt; — a &lt;code&gt;Ref&lt;/code&gt; is rendered as its
name and arguments, never as its expansion. That last clause is what makes
&lt;code&gt;type Tree = | Leaf | Node { l : Tree, r : Tree }&lt;/code&gt; terminate: recursion is
compared nominally, structure only within a body.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;id     ::= prim | &amp;quot;[&amp;quot; id &amp;quot;]&amp;quot; | record | union | ref | var
ref    ::= library &amp;quot;:&amp;quot; name  [ &amp;quot;[&amp;quot; name &amp;quot;=&amp;quot; id (&amp;quot;,&amp;quot; name &amp;quot;=&amp;quot; id)* &amp;quot;]&amp;quot; ]
record ::= &amp;quot;{&amp;quot; name &amp;quot;:&amp;quot; id (&amp;quot;,&amp;quot; name &amp;quot;:&amp;quot; id)* &amp;quot;}&amp;quot;
union  ::= &amp;quot;|&amp;quot; name [ &amp;quot; &amp;quot; id ] (&amp;quot;|&amp;quot; name [ &amp;quot; &amp;quot; id ])*
var    ::= &amp;quot;%&amp;quot; path
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;number
[string]
{x:number,y:number}
message:Envelope[payload=json:Value]
list:T[elem=message:Envelope[payload=json:Value]]
message:Envelope[payload=%ctx.p]        -- partial
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;list:T[elem=number]&lt;/code&gt; written in two libraries a thousand lines apart is one
string, hence one type. That is the whole of the equality story, and it is why
type arguments need no allocation the way symbols do: a symbol’s two
instantiations are genuinely two variables, whereas &lt;code&gt;List[Int]&lt;/code&gt; and
&lt;code&gt;List[Int]&lt;/code&gt; are genuinely one type.&lt;/p&gt;
&lt;p&gt;Canonicalisation must be &lt;strong&gt;injective&lt;/strong&gt;, for the reason v3 §1.4 gives about
allocation keys: distinct types must not collide in one string. Sorting is
therefore total and the grammar above is unambiguous.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="4-partial-types"&gt;4. Partial types&lt;/h3&gt;
&lt;p&gt;A type is &lt;strong&gt;partial&lt;/strong&gt; if its normal form contains a &lt;code&gt;Var&lt;/code&gt;. Partial types are
entirely legal while they are being built — that is what a parameterised
library exports — and are an &lt;strong&gt;error in the root program&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;PartialType  the root's type &amp;lt;id&amp;gt; still contains the variable &amp;lt;path&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the exact type-level analogue of ref §7’s rule that an import may be
partial but must be saturated before it runs, and it is reported the same
way — statically, by analysis, before anything is evaluated. It is what makes
&lt;code&gt;%&lt;/code&gt; safe: unlike &lt;code&gt;?&lt;/code&gt;, a &lt;code&gt;%&lt;/code&gt; cannot leak into the output, because a program
with one left over does not run.&lt;/p&gt;
&lt;p&gt;The consequence worth stating: no partial order over types is needed. Two
partial types are compared by the same string equality as any others, holes
included; nothing ever asks whether one type is &lt;em&gt;more&lt;/em&gt; applied than another,
because nothing may ship a partial type to a host that would care.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="5-constraints-on-types"&gt;5. Constraints on types&lt;/h3&gt;
&lt;p&gt;A template may constrain a type it does not define:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Envelope = { to : string, payload : %ctx.payload }

!type-constraint(&amp;quot;has-default&amp;quot;, %ctx.payload)
!type-constraint(&amp;quot;serializable&amp;quot;, %ctx.payload, &amp;quot;json&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;!type-constraint&lt;/code&gt; is the type realm’s sibling of v3’s &lt;code&gt;!constraint&lt;/code&gt;, and it
means exactly what that means: &lt;em&gt;this fact is asserted; someone downstream
discharges it.&lt;/em&gt; Tramaj checks nothing — it does not know what &lt;code&gt;has-default&lt;/code&gt;
is, any more than it knows what an action key means. It takes a static name
and a list of arguments, each of which is a type expression (&lt;code&gt;%&lt;/code&gt;-marked, §2)
or a literal scalar.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It is variadic and its arity is unbounded&lt;/strong&gt;, for the reason v3 §2.1 gives:
the language fixes no signature because it knows no names, so arity belongs to
the host’s vocabulary alongside the name. A zero-argument
&lt;code&gt;!type-constraint(&amp;quot;closed-world&amp;quot;)&lt;/code&gt; is legal, and so is an n-ary relation over
many types at once — the type-level counterpart of a global constraint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!type-constraint(&amp;quot;disjoint&amp;quot;, %ctx.a, %ctx.b, %ctx.c)
!type-constraint(&amp;quot;coercible-to&amp;quot;, %ctx.payload, %Json, &amp;quot;lossy&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Argument order is significant and preserved, and is part of the canonical
rendering the deduplication below compares on.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;statement = &amp;quot;@&amp;quot; name &amp;quot;=&amp;quot; expr           -- ref
          | &amp;quot;!&amp;quot; expr                    -- v3 §2.2, a value constraint
          | &amp;quot;!&amp;quot; &amp;quot;type-constraint&amp;quot; &amp;quot;(&amp;quot; ... &amp;quot;)&amp;quot;   -- v4, a type constraint
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instantiation follows substitution. When &lt;code&gt;message&lt;/code&gt; is imported with
&lt;code&gt;{payload: %Json}&lt;/code&gt;, the collected constraint is
&lt;code&gt;has-default(json:Value)&lt;/code&gt; — the variable is gone, and the host is asked a
question about a concrete type. Constraints are deduplicated by their
canonical rendering, as v3 §4 deduplicates value constraints and for the same
reason.&lt;/p&gt;
&lt;h4 id="51-one-leader-two-phases-told-apart-by-the-form-name"&gt;5.1 One leader, two phases, told apart by the form name&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;!&lt;/code&gt; means &lt;em&gt;emit a fact&lt;/em&gt;, and that is true of both forms. What differs is the
realm, and the realm is legible in the form name rather than in the leader —
which is the same trick &lt;code&gt;ctx(path)&lt;/code&gt; plays in ref §7, where a static form sits
in the middle of an ordinary expression and is enumerable precisely because
the form name is a fixed word.&lt;/p&gt;
&lt;p&gt;The consequences are worth stating, because they are not the ones a reader
would assume from the leader alone:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;!type-constraint&lt;/code&gt; is resolved, not evaluated.&lt;/strong&gt; Its arguments are type
expressions in the static grammar. The analyser collects it by walking the
AST; the evaluator never sees one, exactly as it never sees the type in an
annotation (§7).
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is a statement, not a declaration.&lt;/strong&gt; It lives in the statement chain
with the bindings and the value constraints, so the declaration block of §1
stays purely declarative. It may be written anywhere a statement may.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Its position affects nothing but source order.&lt;/strong&gt; Since it is erased before
evaluation, it cannot observe or affect a binding, and moving it up or down
the statement list changes only where it lands in &lt;code&gt;&amp;quot;type-constraints&amp;quot;&lt;/code&gt;
before deduplication.
&lt;/li&gt;
&lt;li&gt;In the core AST it is &lt;code&gt;TypeEmit name: String, arguments: List&amp;lt;TypeExpr | Scalar&amp;gt;&lt;/code&gt;, a sibling of v3’s &lt;code&gt;Emit&lt;/code&gt; that the evaluator’s statement fold skips.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The one thing this costs is that &lt;code&gt;!&lt;/code&gt; no longer implies “evaluated”. That was
never load-bearing — &lt;code&gt;!&lt;/code&gt; implies “this contributes to the output envelope”,
which remains exactly true — and the form name is a static position, so a
reader and an analyser can both tell which realm a statement is in by looking
at one word.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="6-supplying-versus-asserting"&gt;6. Supplying versus asserting&lt;/h3&gt;
&lt;p&gt;How does a user force a type variable to be &lt;em&gt;replaced&lt;/em&gt; rather than
symbolically equated? The two operations are already different things, and
only one of them is Tramaj’s:&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;written&lt;/th&gt;&lt;th&gt;what Tramaj does&lt;/th&gt;&lt;th&gt;when&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;supply&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;import("m", {payload: %Json})&lt;/code&gt;&lt;/td&gt;&lt;td&gt;substitutes; the variable is gone&lt;/td&gt;&lt;td&gt;analysis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;forward&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;import("m", {payload: %ctx.payload})&lt;/code&gt;&lt;/td&gt;&lt;td&gt;substitutes one variable for another; still a hole&lt;/td&gt;&lt;td&gt;analysis&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;assert&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;!type-constraint("eq", %ctx.payload, %Json)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;collects the fact; the hole remains a hole&lt;/td&gt;&lt;td&gt;never&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Substitution is structural and Tramaj performs it. Assertion is a fact and
Tramaj only carries it. Tramaj will &lt;strong&gt;never&lt;/strong&gt; solve an assertion into a
substitution — that would be a unifier, and a unifier is the beginning of the
checker both documents refuse to build. If you want the type replaced, supply
it; the &lt;code&gt;PartialType&lt;/code&gt; error of §4 is what guarantees you cannot forget to.&lt;/p&gt;
&lt;p&gt;The parallel to &lt;code&gt;ctx(path)&lt;/code&gt; is exact: forwarding is deferral, supplying is the
literal. The syntax already distinguishes them, because &lt;code&gt;%ctx.p&lt;/code&gt; and &lt;code&gt;%T&lt;/code&gt; are
different expressions.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="7-annotations-and-how-types-reach-the-value-realm"&gt;7. Annotations, and how types reach the value realm&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@d : Deployment          = ?(&amp;quot;d&amp;quot;)
@n : string              = $ctx.name
@m : $msg.types.Envelope = ?(&amp;quot;m&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;@x : T = e&lt;/code&gt; is &lt;strong&gt;sugar&lt;/strong&gt; for the binding plus a v3 emission:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;@x = e
!constraint(&amp;quot;has-type&amp;quot;, $x, {&amp;quot;$type&amp;quot;: &amp;quot;&amp;lt;canonical id of T&amp;gt;&amp;quot;})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The type is &lt;strong&gt;erased at analysis time&lt;/strong&gt; into the opaque tag v3 §5.3 already
reserved. By the time the evaluator runs there is no type left in the
program — only an inert object with a &lt;code&gt;&amp;quot;$type&amp;quot;&lt;/code&gt; key, which is data like any
other. So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Value&lt;/code&gt; gains no constructor. &lt;code&gt;str&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;eq&lt;/code&gt; and the whole &lt;code&gt;NotConcrete&lt;/code&gt;
table of v3 §1.5 are untouched.
&lt;/li&gt;
&lt;li&gt;A type reference is still not a value, and cannot be computed, branched on,
or built at runtime.
&lt;/li&gt;
&lt;li&gt;The host looks the id up in the &lt;code&gt;&amp;quot;types&amp;quot;&lt;/code&gt; table (§8) and has the definition
in hand without parsing Tramaj source.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An annotation whose type is partial is the &lt;code&gt;PartialType&lt;/code&gt; error of §4 — which
is why erasure is safe: nothing ambiguous can be erased.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;has-type&lt;/code&gt; on a symbol is the useful case: it tells the host the domain of a
hole it is about to fill, which for an enum union is a finite one. A
&lt;code&gt;has-type&lt;/code&gt; on a concrete value is a claim a checker can verify against the
output. Tramaj distinguishes neither; both are emitted the same way.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="8-output"&gt;8. Output&lt;/h3&gt;
&lt;p&gt;The v3 §5.2 envelope gains two lists, and the value domain gains the second
tagged shape v3 reserved.&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;format&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;tramaj/symbolic/1&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;document&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;root&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;...&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;...&amp;quot;&lt;/span&gt; &lt;span class="fu"&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span id="5"&gt;&lt;a href="#5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;symbols&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="6"&gt;&lt;a href="#6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;#0:&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;m&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;origin&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;alloc&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;site&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="dv"&gt;0&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;m&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;},&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;binding&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;m&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id="7"&gt;&lt;a href="#7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="8"&gt;&lt;a href="#8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;constraints&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="9"&gt;&lt;a href="#9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;has-type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="10"&gt;&lt;a href="#10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;arguments&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;$sym&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;#0:&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;m&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="11"&gt;&lt;a href="#11" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;                   &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;$type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;message:Envelope[payload=json:Value]&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id="12"&gt;&lt;a href="#12" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="13"&gt;&lt;a href="#13" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;types&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="14"&gt;&lt;a href="#14" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;message:Envelope[payload=json:Value]&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="15"&gt;&lt;a href="#15" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;definition&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;record&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="16"&gt;&lt;a href="#16" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;                    &lt;span class="dt"&gt;&amp;quot;fields&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;payload&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;ref&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;json:Value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="17"&gt;&lt;a href="#17" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;                               &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;to&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;prim&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;string&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}}&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="18"&gt;&lt;a href="#18" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;json:Value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;definition&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;...&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;...&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}}&lt;/span&gt;&lt;/span&gt;
&lt;span id="19"&gt;&lt;a href="#19" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="20"&gt;&lt;a href="#20" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;type-constraints&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="21"&gt;&lt;a href="#21" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;has-default&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;arguments&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;$type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;json:Value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id="22"&gt;&lt;a href="#22" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span id="23"&gt;&lt;a href="#23" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;quot;types&amp;quot;&lt;/code&gt; is the &lt;strong&gt;transitive closure&lt;/strong&gt; of every type the program
references: a record’s field types, a union’s payloads, and theirs. The
closure is what a host actually needs to interpret a &lt;code&gt;has-type&lt;/code&gt;; the direct
set is merely cheaper to compute. Cycles are cut by the ids, since a &lt;code&gt;Ref&lt;/code&gt;
is never expanded (§3).
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A constraint goes in the list of its subject’s realm.&lt;/strong&gt; &lt;code&gt;has-type&lt;/code&gt; relates
a &lt;em&gt;value&lt;/em&gt; to a type, so it is a v3 constraint carrying an erased &lt;code&gt;$type&lt;/code&gt;
argument; &lt;code&gt;has-default&lt;/code&gt; relates a type to nothing else, so it is a type
constraint. That is the rule, not an accident of which syntax produced it.
&lt;/li&gt;
&lt;li&gt;Both lists are &lt;code&gt;[]&lt;/code&gt; for a program with no types, so a v3 consumer reading a
v4 envelope sees nothing new, and the format version does not change.
&lt;/li&gt;
&lt;li&gt;Concrete mode (v3 §5.1) emits plain JSON as before. Types are erased, so a
concrete-mode run of a typed program is byte-identical to the same program
with the annotations deleted — which is the honest meaning of erasure.
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3 id="9-static-analysis"&gt;9. Static analysis&lt;/h3&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;function&lt;/th&gt;&lt;th&gt;answers&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;typeDeclarations&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which types a program declares&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;typeReferences&lt;/code&gt; / &lt;code&gt;deepTypeReferences&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which it refers to, normalised&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;typeParams&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which &lt;code&gt;%ctx.*&lt;/code&gt; variables its declarations read — its type-level parameter list&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;unsuppliedTypeParams&lt;/code&gt;&lt;/td&gt;&lt;td&gt;per import, the type params its library needs that the import does not supply&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;typeConstraints&lt;/code&gt; / &lt;code&gt;deepTypeConstraints&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which type-level facts it asserts&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;typeParams&lt;/code&gt; is to types what &lt;code&gt;contextReads&lt;/code&gt; is to values, and
&lt;code&gt;unsuppliedTypeParams&lt;/code&gt; is the type half of ref §9’s &lt;code&gt;unsuppliedParams&lt;/code&gt; — the
same subtraction, over the keys the &lt;code&gt;%&lt;/code&gt; marks.&lt;/p&gt;
&lt;p&gt;Resolution follows imports through the library table with the same cycle
cutting ref §9’s deep analyses use, and &lt;strong&gt;no library is evaluated&lt;/strong&gt;: the table
holds parsed programs, so all of this is an AST walk. Unresolvable references
are reported rather than passed through, on the same footing as an import name
the table does not resolve.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$lib.types.X&lt;/code&gt; is not a runtime field read. It is resolved statically, which
means &lt;code&gt;lib&lt;/code&gt; must be bound &lt;em&gt;directly&lt;/em&gt; to an &lt;code&gt;import(...)&lt;/code&gt; in the enclosing
binding chain; an import reached through a lambda, an array, or a later
saturating call cannot be resolved, and that is an analysis error.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="10-errors"&gt;10. Errors&lt;/h3&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;error&lt;/th&gt;&lt;th&gt;when&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;UnresolvedType&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a name resolves to no declaration and no primitive&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;PartialType&lt;/code&gt;&lt;/td&gt;&lt;td&gt;the root ships a type still containing a &lt;code&gt;%&lt;/code&gt; variable (§4)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;TypeParamCollision&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a params key is read both as &lt;code&gt;$ctx.k&lt;/code&gt; and as &lt;code&gt;%ctx.k&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NotStaticallyResolvable&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;$lib.types.X&lt;/code&gt; where &lt;code&gt;lib&lt;/code&gt; is not bound directly to an import&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;TypeCycle&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a declaration's &lt;em&gt;arguments&lt;/em&gt; cycle (a recursive body does not)&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;All five are analysis errors. No new evaluation error exists, because by
evaluation time there are no types.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="11-open"&gt;11. Open&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;alias X = T&lt;/code&gt;&lt;/strong&gt;, transparent rather than nominal. Nominal is the right
default and gives newtyping for free (§1.1); transparency is occasionally
what you want for a host schema that is structural. Cheap to add, easy to
regret, so: not until something needs it.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application sugar.&lt;/strong&gt; &lt;code&gt;list:T[elem=number]&lt;/code&gt; is spelled today as an import
plus a field read, which is verbose for what a Haskeller writes &lt;code&gt;List Int&lt;/code&gt;.
A sugar &lt;code&gt;%list.T[elem: number]&lt;/code&gt; desugaring to a fresh wiring would be safe
— identity is structural, so a sugared and a spelled-out application are
the same type — but it is sugar, and should follow a real complaint.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;Program&lt;/code&gt; change.&lt;/strong&gt; Adding a declaration block breaks
&lt;code&gt;DocumentProgram Expr | ExpressionProgram Expr&lt;/code&gt; and every host that
pattern-matches it. This remains the only breaking AST change across v3 and
v4, and it is worth confirming exported types are wanted before paying it.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type-directed projection.&lt;/strong&gt; With declarations in hand, &lt;code&gt;$d.replicas&lt;/code&gt;
could carry a resolved field type into the constraint that mentions it. v3
deliberately computes nothing for a projection (v3 §1.6), and moving that
into the language is the first step toward the typechecker both documents
refuse to build.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unifying the realms.&lt;/strong&gt; A &lt;em&gt;symbolic&lt;/em&gt; type — a &lt;code&gt;?&lt;/code&gt;-style hole in the type
realm, constrained rather than supplied, that a host resolves alongside the
symbols — would collapse §0’s table into one mechanism. It also collapses
the erasure of §7, since a type that survives to the host cannot be erased
before evaluation, and it reopens the question of which realm is resolved
first. Research, not v4.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Row polymorphism&lt;/strong&gt;, for “a record with at least these fields”. This is
what &lt;code&gt;!type-constraint&lt;/code&gt; is likely to be abused into expressing, and if that
abuse becomes common the honest fix is a former, not a constraint name.
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/reference-v4-types.html" rel="alternate"/><summary type="text">Generated from [`specs/v4-types.md`](https://github.com/lucasdicioccio/tramaj/blob/main/specs/v4-types.md) — the repository is the canonical source.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/reference-v3-symbols.html</id><title type="text">Tramaj v3 — Symbolic values and constraints</title><updated>2026-10-02T20:43:09Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;p&gt;&lt;em&gt;Generated from &lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/v3-symbols.md"&gt;&lt;code&gt;specs/v3-symbols.md&lt;/code&gt;&lt;/a&gt; — the repository is the canonical source, and may be ahead of this page.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="tramaj-v3--symbolic-values-and-constraints"&gt;Tramaj v3 — Symbolic values and constraints&lt;/h2&gt;
&lt;p&gt;This document is &lt;strong&gt;normative&lt;/strong&gt; for the symbolic extension. It builds on
&lt;code&gt;reference.md&lt;/code&gt;, which remains normative for everything else; section numbers
written as &lt;em&gt;(ref §n)&lt;/em&gt; point there. &lt;code&gt;node-json.md&lt;/code&gt; is unchanged and remains
normative for the &lt;code&gt;Node&lt;/code&gt; encoding, which v3 wraps rather than alters.&lt;/p&gt;
&lt;p&gt;Types are &lt;strong&gt;not&lt;/strong&gt; in v3. Nominal types, type declarations, imported type
namespaces and typed annotations are drafted separately in
&lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt;. v3 is complete and implementable without them,
and §9 says how far it gets on its own.&lt;/p&gt;
&lt;p&gt;Status: frozen as a design. Not implemented in either &lt;code&gt;tramaj/&lt;/code&gt; or
&lt;code&gt;tramaj-hs/&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="0-what-v3-adds-and-the-rule-it-follows"&gt;0. What v3 adds, and the rule it follows&lt;/h3&gt;
&lt;p&gt;Three things: an expression &lt;code&gt;?&lt;/code&gt; that introduces a &lt;strong&gt;symbol&lt;/strong&gt;, a statement &lt;code&gt;!&lt;/code&gt;
that emits a &lt;strong&gt;constraint&lt;/strong&gt;, and an output mode that carries both.&lt;/p&gt;
&lt;p&gt;Everything follows one rule the language already applies to actions and import
names:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tramaj fixes positions and identity. The host owns vocabulary and meaning.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;An action’s event and key are static positions; what &lt;code&gt;&amp;quot;on-click&amp;quot;&lt;/code&gt; means is the
host’s business (ref §10). v3 says the same of symbolic variables and
constraints:&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;the language guarantees&lt;/th&gt;&lt;th&gt;the host decides&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;a symbolic variable exists, and &lt;em&gt;which&lt;/em&gt; one it is&lt;/td&gt;&lt;td&gt;what it stands for&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;a constraint was emitted, with these arguments&lt;/td&gt;&lt;td&gt;what &lt;code&gt;"gte"&lt;/code&gt; means, and how to solve it&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;the set of constraint kinds a program can emit, statically&lt;/td&gt;&lt;td&gt;whether it supports them&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Tramaj therefore solves nothing and checks nothing. It evaluates as far as it
can and serializes what it could not resolve, so that a host fold may run a
solver, render a form, ask a user, or refuse a template whose constraint
vocabulary it does not implement.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="1-symbols"&gt;1. Symbols&lt;/h3&gt;
&lt;h4 id="11-the-value"&gt;1.1 The value&lt;/h4&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Value
  = ... as in ref §3 ...
  | Symbol  id: SymbolId, path: List&amp;lt;String&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A &lt;code&gt;Symbol&lt;/code&gt; with an empty &lt;code&gt;path&lt;/code&gt; is an &lt;strong&gt;allocation&lt;/strong&gt;; one with a non-empty
path is a &lt;strong&gt;projection&lt;/strong&gt; of an allocation (§1.6). A symbol is opaque to the
language: nothing inspects it, and nothing but §1.6’s projection derives a new
one from it.&lt;/p&gt;
&lt;h4 id="12-key--allocation"&gt;1.2 &lt;code&gt;?(key)&lt;/code&gt; — allocation&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@primary = ?(&amp;quot;primary&amp;quot;)
@ports   = map($ctx.services, (s) =&amp;gt; ?($s.name))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;?(expr)&lt;/code&gt; allocates. The argument is an ordinary expression, evaluated where
it is written, and MUST evaluate to a concrete JSON value; a symbol used as a
key is &lt;code&gt;NotConcrete&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is &lt;strong&gt;no keyless form.&lt;/strong&gt; A symbol always says what distinguishes it from
its neighbours, so the question “did this mean one variable or many?” is
always answered in the source:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;map($ctx.services, (s) =&amp;gt; ?($s.name))   -- one symbol per service
map($ctx.services, (s) =&amp;gt; ?(&amp;quot;port&amp;quot;))    -- one symbol, shared by every service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both are legal and mean what they say. A constant key is a constant symbol.
What v3 refuses is the version where the author writes nothing and the
language guesses.&lt;/p&gt;
&lt;h4 id="13-ctxpath--demand"&gt;1.3 &lt;code&gt;?ctx.path&lt;/code&gt; — demand&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;@replicas = ?ctx.replicas
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;?ctx.a.b&lt;/code&gt; evaluates to &lt;code&gt;$ctx.a.b&lt;/code&gt; and declares &lt;em&gt;this is a value I intend to
discuss symbolically&lt;/em&gt;. The path MUST be rooted at &lt;code&gt;ctx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;At runtime it means exactly what &lt;code&gt;$ctx.a.b&lt;/code&gt; means.&lt;/strong&gt; Whatever the caller
supplied — a number, a symbol, an object — is what it produces. The
distinction is entirely static, and it is the same trade &lt;code&gt;ctx(path)&lt;/code&gt; already
makes for import parameters (ref §7): a path in a static position is a demand
an analyzer can enumerate, where the same read buried in an arbitrary
expression is not.&lt;/p&gt;
&lt;p&gt;An unsupplied demand:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;in the root program&lt;/strong&gt;, allocates the symbol &lt;code&gt;#&lt;/code&gt; + the path (§1.4). The
root’s caller is the host, the host declined to supply, and the root runs
once, so it may allocate.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;in a library&lt;/strong&gt;, is &lt;code&gt;PathNotFound&lt;/code&gt;, wrapped in &lt;code&gt;InLibrary&lt;/code&gt; naming the
library — the same error an ordinary unsupplied read already produces
(ref §7).
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="14-identity"&gt;1.4 Identity&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Only a root program may allocate. A library MUST NOT&lt;/strong&gt;, because the root
runs exactly once and a library does not.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;AllocationInLibrary&lt;/code&gt; is raised when a program containing &lt;code&gt;?(k)&lt;/code&gt; is loaded as
a library. The check is &lt;strong&gt;lexical&lt;/strong&gt;: it is about where the &lt;code&gt;?&lt;/code&gt; is written, not
where it is evaluated, so a closure created in the root and applied inside a
library still allocates in the root, which is where its &lt;code&gt;?&lt;/code&gt; is written.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;SymbolId&lt;/code&gt; is a string, formed as follows and identical in every conforming
implementation:&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;form&lt;/th&gt;&lt;th&gt;id&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;?(k)&lt;/code&gt;, the &lt;em&gt;n&lt;/em&gt;-th &lt;code&gt;?&lt;/code&gt; in the program's source&lt;/td&gt;&lt;td&gt;&lt;code&gt;#&lt;/code&gt; &lt;em&gt;n&lt;/em&gt; &lt;code&gt;:&lt;/code&gt; canon(&lt;em&gt;k&lt;/em&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;?ctx.a.b&lt;/code&gt;, unsupplied at the root&lt;/td&gt;&lt;td&gt;&lt;code&gt;#ctx.a.b&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;n&lt;/em&gt; is assigned by the parser: the &lt;code&gt;?(k)&lt;/code&gt; occurrences of one program are
numbered &lt;code&gt;0, 1, 2, …&lt;/code&gt; in source order. It belongs to the core AST for the
same reason a binding’s name does — it &lt;em&gt;is&lt;/em&gt; the variable’s identity, not
source trivia.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;canon&lt;/strong&gt; is compact JSON with object keys sorted, numbers rendered by
ref §6’s &lt;code&gt;str&lt;/code&gt; rule, and strings JSON-quoted. It agrees with &lt;code&gt;str&lt;/code&gt; on arrays
and objects and differs at the top level for strings, where &lt;code&gt;str&lt;/code&gt; renders
raw. The difference is required: canon MUST be injective, and &lt;code&gt;str(3)&lt;/code&gt; and
&lt;code&gt;str(&amp;quot;3&amp;quot;)&lt;/code&gt; are the same characters.
&lt;/li&gt;
&lt;li&gt;The two forms cannot collide: an allocation id continues with a digit, a
demand id with &lt;code&gt;ctx&lt;/code&gt;. A path segment cannot contain &lt;code&gt;.&lt;/code&gt; (ref §5), so the
demand form is unambiguous.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Identity is a function of the program text and of one value the author chose.
It does not depend on evaluation order, so two implementations agree on symbol
ids by construction rather than by agreeing on a traversal.&lt;/p&gt;
&lt;p&gt;The site is part of the id, as well as the key, so that two independent
&lt;code&gt;?(&amp;quot;replicas&amp;quot;)&lt;/code&gt; in different places are different variables. A key names a
distinction, not a variable.&lt;/p&gt;
&lt;h4 id="15-what-may-be-done-with-a-symbol"&gt;1.5 What may be done with a symbol&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Control flow and structure must be concrete. Only data may be symbolic.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A symbol is data. It may be bound, passed to a lambda, returned from one,
stored in an array or object, and placed in an attribute, an action payload,
an element value slot or a text child. Everything that would require the
language to &lt;em&gt;know&lt;/em&gt; something about it is an error:&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;expression&lt;/th&gt;&lt;th&gt;result&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$s&lt;/code&gt;, &lt;code&gt;[$s]&lt;/code&gt;, &lt;code&gt;{k: $s}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;fine — a symbol is a value&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.p($s)&lt;/code&gt;, &lt;code&gt;.div(class: $s)&lt;/code&gt;, &lt;code&gt;action(e, k, {r: $s})&lt;/code&gt;&lt;/td&gt;&lt;td&gt;fine — §5.2 carries it&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$s.field&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a projection (§1.6) — &lt;strong&gt;the one exception&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;branch($s, …)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;NotConcrete&lt;/code&gt; — no arm can be selected&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;map($s, f)&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt;, &lt;code&gt;scan&lt;/code&gt;, &lt;code&gt;fold&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;NotConcrete&lt;/code&gt; — the spine has no length&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;eq&lt;/code&gt;, &lt;code&gt;lt&lt;/code&gt;, &lt;code&gt;lte&lt;/code&gt;, &lt;code&gt;gt&lt;/code&gt;, &lt;code&gt;gte&lt;/code&gt; on anything &lt;em&gt;containing&lt;/em&gt; a symbol&lt;/td&gt;&lt;td&gt;&lt;code&gt;NotConcrete&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;str($s)&lt;/code&gt;, and so any interpolation of one&lt;/td&gt;&lt;td&gt;&lt;code&gt;NotConcrete&lt;/code&gt; — §1.8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cardinality($s)&lt;/code&gt;, &lt;code&gt;has($s, k)&lt;/code&gt;, &lt;code&gt;lookup($s, k, d)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;NotConcrete&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$s &amp;lt;&amp;gt; x&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;NotConcrete&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;has&lt;/code&gt; and &lt;code&gt;lookup&lt;/code&gt; are called out because both are deliberately &lt;em&gt;tolerant&lt;/em&gt;
today — &lt;code&gt;has&lt;/code&gt; answers &lt;code&gt;false&lt;/code&gt; for a wrong-shaped container and &lt;code&gt;lookup&lt;/code&gt; never
errors. Neither may answer about a symbol: a tolerant &lt;code&gt;false&lt;/code&gt; would be a lie,
and &lt;code&gt;lookup&lt;/code&gt;’s fallback would silently discard the symbol.&lt;/p&gt;
&lt;p&gt;Consequence worth stating: v3 can constrain the elements of a list whose
length is known, and cannot constrain a list whose length is symbolic. That is
the ceiling, and it is where a real symbolic evaluator would have to begin.
Declining to go there is what keeps &lt;code&gt;map&lt;/code&gt; free of any &lt;code&gt;if value is Symbol&lt;/code&gt;
branch — no evaluator rule in ref §6 changes, and no builtin learns a new
case beyond refusing.&lt;/p&gt;
&lt;h4 id="16-projection"&gt;1.6 Projection&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;$s.field&lt;/code&gt; reads nothing. It produces a symbol with the same &lt;code&gt;id&lt;/code&gt; and the path
extended:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Symbol &amp;quot;#0:\&amp;quot;d\&amp;quot;&amp;quot; []            -- $d
Symbol &amp;quot;#0:\&amp;quot;d\&amp;quot;&amp;quot; [&amp;quot;replicas&amp;quot;]  -- $d.replicas
Symbol &amp;quot;#0:\&amp;quot;d\&amp;quot;&amp;quot; [&amp;quot;a&amp;quot;,&amp;quot;b&amp;quot;]     -- $d.a.b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A projection is never rejected. &lt;code&gt;PathNotFound&lt;/code&gt; is for a concrete value that
demonstrably lacks a field; a symbol demonstrably lacks nothing, and whether
the thing it stands for has that field is a question for whoever owns its
meaning.&lt;/p&gt;
&lt;p&gt;Projections do not appear in the symbol table (§5.2); they are references to
the allocation they project from.&lt;/p&gt;
&lt;h4 id="17-symbols-in-structures"&gt;1.7 Symbols in structures&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A concrete structure containing symbols is not special.&lt;/strong&gt; &lt;code&gt;[?(&amp;quot;a&amp;quot;), ?(&amp;quot;b&amp;quot;)]&lt;/code&gt;
is an ordinary two-element array that happens to hold symbols. The spine is
concrete, so everything structural works: &lt;code&gt;cardinality&lt;/code&gt; counts, &lt;code&gt;map&lt;/code&gt;
iterates, a field read returns the symbol it finds, &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt; merges. Only the
operations that &lt;em&gt;inspect&lt;/em&gt; a value refuse.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@ports = map($ctx.services, (s) =&amp;gt; ?($s.name))
!map($ports, (p) =&amp;gt; constraint(&amp;quot;gte&amp;quot;, $p, 1024))
.ul(map($ports, (p) =&amp;gt; .li($p)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is legal end to end.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A symbol that is itself structured is the ceiling.&lt;/strong&gt; &lt;code&gt;$d.replicas&lt;/code&gt; projects
fine; &lt;code&gt;map($d.items, f)&lt;/code&gt; is &lt;code&gt;NotConcrete&lt;/code&gt;, because the length is unknown. A
symbol may &lt;em&gt;have&lt;/em&gt; structure as far as its host-side meaning is concerned; the
language cannot walk it.&lt;/p&gt;
&lt;p&gt;Two cases fall out of the same rule: a symbol cannot be an object key, because
keys are static (ref §5) and nothing computes one; and a symbol inside an
import’s parameters is an ordinary value being passed, so it crosses the
boundary with its identity intact, which is §2.4’s mechanism.&lt;/p&gt;
&lt;h4 id="18-symbols-in-text"&gt;1.8 Symbols in text&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;str&lt;/code&gt; refuses symbols, so &lt;code&gt;&amp;quot;Deploy `$r` replicas&amp;quot;&lt;/code&gt; is &lt;code&gt;NotConcrete&lt;/code&gt;. The
form to write instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.p(&amp;quot;Deploy &amp;quot;, $r, &amp;quot; replicas&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three children — two carrying strings, one carrying the symbol. This is what a
form renderer needs anyway: it must put an input where the symbol is, and it
cannot if the symbol has been flattened into the middle of a string. The
concrete case is unaffected: if &lt;code&gt;$r&lt;/code&gt; is a number, the same element produces
the same three text children.&lt;/p&gt;
&lt;p&gt;Reconstructing a string from those pieces, if a downstream consumer wants one,
is that consumer’s job. It has them in order and knows what its target does
with them, which the language does not.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="2-constraints"&gt;2. Constraints&lt;/h3&gt;
&lt;h4 id="21-constraint"&gt;2.1 &lt;code&gt;constraint(...)&lt;/code&gt;&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;constraint(&amp;quot;gte&amp;quot;, $d.replicas, 1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A special form, not a builtin. The name MUST be a string literal with no
interpolation, like an action’s event and key (ref §5) — that is what makes
§7’s &lt;code&gt;constraintKinds&lt;/code&gt; computable. The remaining arguments are ordinary
expressions, evaluated where they are written.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Value
  = ...
  | Constraint  name: String, arguments: List&amp;lt;Value&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The form is variadic and its arity is unbounded.&lt;/strong&gt; &lt;code&gt;constraint(name)&lt;/code&gt; with
no arguments is legal, and so is any number beyond. The language fixes no
signature for any name, because the language knows no names: arity is part of
the host’s vocabulary exactly as the name is, and a host that receives a
&lt;code&gt;&amp;quot;gte&amp;quot;&lt;/code&gt; with four arguments rejects it the same way it rejects a kind it does
not implement. Nothing in §7 depends on arity — &lt;code&gt;constraintKinds&lt;/code&gt; reports
names, not signatures.&lt;/p&gt;
&lt;p&gt;This matters for &lt;strong&gt;global constraints&lt;/strong&gt; in the MiniZinc sense, which are the
reason to have a constraint list at all rather than a pile of binary
comparisons. They are n-ary over whole collections, and they mix collections
with scalars:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;!constraint(&amp;quot;all-different&amp;quot;, $replicas)
!constraint(&amp;quot;cumulative&amp;quot;, $starts, $durations, $demands, $capacity)
!constraint(&amp;quot;global-cardinality&amp;quot;, $zones, [&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;], [$lo, $hi])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An argument may be an array (§1.7), so a global taking several arrays of
symbols is expressible without any special form of its own; combined with
unbounded arity, the whole family is reachable. &lt;strong&gt;Argument order is
significant and preserved&lt;/strong&gt; — position is how a global distinguishes its
starts from its durations — and it is part of the canonical rendering §4
deduplicates on.&lt;/p&gt;
&lt;p&gt;An argument may be any JSON value or any symbol. Anything else — a closure, a
builtin, a node, an import — is the &lt;code&gt;TypeMismatch&lt;/code&gt; the JSON boundary already
raises, through the same code path.&lt;/p&gt;
&lt;p&gt;Note what this does &lt;strong&gt;not&lt;/strong&gt; do: there is no restricted constraint-expression
grammar and no quoting of argument syntax. &lt;code&gt;constraint(&amp;quot;gt&amp;quot;, $x, $ctx.threshold)&lt;/code&gt; stores &lt;code&gt;10&lt;/code&gt; when the threshold is concrete and stores a
symbol when it is symbolic. Quoting the syntax instead would make every
constraint produced by a loop identical, since &lt;code&gt;$item.replicas&lt;/code&gt; is the same
three words on every iteration.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;Constraint&lt;/code&gt; is &lt;strong&gt;not data&lt;/strong&gt;. It MUST NOT cross a JSON boundary: not an
attribute, a payload, an element value slot, a text child, or an expression
program’s root. The only construct that may consume one is &lt;code&gt;!&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id="22---emission"&gt;2.2 &lt;code&gt;!&lt;/code&gt; — emission&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;!&lt;/code&gt; is a fourth leader character, joining &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;$&lt;/code&gt; and &lt;code&gt;@&lt;/code&gt;. A program is a
sequence of &lt;strong&gt;statements&lt;/strong&gt; followed by a root expression:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;statement = &amp;quot;@&amp;quot; name &amp;quot;=&amp;quot; expr        -- binding, as today
          | &amp;quot;!&amp;quot; expr                 -- emission
program   = statement* expr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;!&lt;/code&gt; is a statement leader only. It may not appear inside an expression.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@d   = ?(&amp;quot;d&amp;quot;)
@min = 1

!constraint(&amp;quot;gte&amp;quot;, $d.replicas, $min)
!constraint(&amp;quot;lte&amp;quot;, $d.replicas, 10)
!map($ctx.zones, (z) =&amp;gt; constraint(&amp;quot;allowed-zone&amp;quot;, $d.zone, $z))

.Deployment(name: $d.name, replicas: $d.replicas)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;!expr&lt;/code&gt; evaluates &lt;code&gt;expr&lt;/code&gt; and collects from it by exactly the coercion table
children already use (ref §6):&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;value&lt;/th&gt;&lt;th&gt;contributes&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;a constraint&lt;/td&gt;&lt;td&gt;itself, one&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;an array&lt;/td&gt;&lt;td&gt;each element, recursively&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;anything else&lt;/td&gt;&lt;td&gt;&lt;code&gt;TypeMismatch&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;which is why &lt;code&gt;!map(...)&lt;/code&gt; reads naturally: an array &lt;em&gt;is&lt;/em&gt; a sequence, in the
constraint set as much as in a child list.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A statement sees earlier bindings and not later ones, exactly as a binding
does.
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;!branch(c, …)&lt;/code&gt; collects only from the selected arm. &lt;code&gt;Branch&lt;/code&gt; is lazy in
every position (ref §6) and the unselected arm’s constraints do not exist.
&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;Constraint&lt;/code&gt; value never reached by a &lt;code&gt;!&lt;/code&gt; is discarded. That is the honest
consequence of constraints being values, and the price of the
&lt;code&gt;@cs = map(...)&lt;/code&gt; / &lt;code&gt;!$cs&lt;/code&gt; split that makes them worth having.
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="23-constraints-from-libraries"&gt;2.3 Constraints from libraries&lt;/h4&gt;
&lt;p&gt;A library’s emissions are collected into the same flat list as the root’s. A
library is where most constraints will be written, since it is the thing that
knows what its own parameters mean.&lt;/p&gt;
&lt;p&gt;A library runs when a field is read off its import (ref §7), so its
constraints are collected at that point, not where the import is written.&lt;/p&gt;
&lt;h4 id="24-who-supplies-a-symbol"&gt;2.4 Who supplies a symbol&lt;/h4&gt;
&lt;p&gt;Nothing bubbles at runtime. A library declares a demand; its caller decides
what to put there:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@a = ?(&amp;quot;a&amp;quot;)
@b = ?(&amp;quot;b&amp;quot;)
import(&amp;quot;deployment&amp;quot;, {replicas: $a})      -- this one
import(&amp;quot;deployment&amp;quot;, {replicas: $a})      -- and this one share a variable
import(&amp;quot;deployment&amp;quot;, {replicas: $b})      -- this one has its own
import(&amp;quot;deployment&amp;quot;, {replicas: 3})       -- and this one is concrete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Symbols are ordinary values, so passing one preserves its identity; no
mechanism beyond that is required. The &lt;em&gt;demand&lt;/em&gt; bubbles statically instead,
through &lt;code&gt;deepSymbolDemands&lt;/code&gt; (§7), which tells a root author how many symbols
to allocate and where to pass them without running anything.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="3-core-ast"&gt;3. Core AST&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Expr
  = ... as in ref §2 ...
  | Alloc      site: Int, key: Expr                 -- ?(k)
  | Demand     path: List&amp;lt;String&amp;gt;                   -- ?ctx.a.b
  | Constrain  name: String, arguments: List&amp;lt;Expr&amp;gt;  -- constraint(...)
  | Emit       constraint: Expr, body: Expr         -- !expr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Emit&lt;/code&gt; nests into the chain that bindings already lower to, so&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@a=1
!c
root
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is &lt;code&gt;Let &amp;quot;a&amp;quot; 1 (Emit c root)&lt;/code&gt;. The “earlier bindings only” rule therefore needs
no separate statement machinery, and &lt;code&gt;Program&lt;/code&gt; is unchanged from ref §2 —
still &lt;code&gt;DocumentProgram Expr | ExpressionProgram Expr&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Emit&lt;/code&gt; is representable anywhere an expression is, but the parser produces it
only at statement position.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="4-evaluation"&gt;4. Evaluation&lt;/h3&gt;
&lt;p&gt;Evaluation produces a value &lt;strong&gt;and&lt;/strong&gt; an emission record:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Emissions
  symbols     : List&amp;lt;SymbolEntry&amp;gt;   -- allocations, first-occurrence order, deduplicated by id
  constraints : List&amp;lt;Constraint&amp;gt;    -- emission order, deduplicated
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a monoidal output, not mutable state: it is deterministic, it is
threaded through evaluation rather than written to, and it gives &lt;code&gt;Branch&lt;/code&gt; the
right behaviour for free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evaluation order is prescribed.&lt;/strong&gt; ref §13 leaves evaluation order
implementation-defined beyond binding order and &lt;code&gt;Branch&lt;/code&gt;. v3 removes that
freedom: every implementation MUST evaluate call arguments, array literal
elements, object literal fields and special-form arguments in &lt;strong&gt;source
order&lt;/strong&gt;, in addition to the orders ref §6 already fixes for elements.&lt;/p&gt;
&lt;p&gt;This is required, not stylistic. &lt;code&gt;laws.md&lt;/code&gt; demands that evaluation be
deterministic in the result, and in v3 the emission list is part of the
result. It costs nothing: no conforming program could depend on the freedom
being removed, since ref §13 already forbade depending on it. It also makes
error reporting deterministic, which is a free improvement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deduplication.&lt;/strong&gt; Two constraints with the same name and equal arguments are
one constraint, kept at the position of the first. Constraints are declarative
facts; asserting one twice says nothing more than asserting it once. This also
makes it unobservable whether an implementation runs a library once or twice
for two field reads on the same import, which ref §7 does not specify.&lt;/p&gt;
&lt;p&gt;Allocations are deduplicated by &lt;code&gt;SymbolId&lt;/code&gt; the same way, so &lt;code&gt;?(&amp;quot;port&amp;quot;)&lt;/code&gt;
reached from every element of a &lt;code&gt;map&lt;/code&gt; produces one symbol table entry.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="5-modes"&gt;5. Modes&lt;/h3&gt;
&lt;p&gt;An interpreter takes a &lt;strong&gt;mode&lt;/strong&gt;. It is a host parameter, not a property of the
program: a host declares what it can read, rather than discovering it from
whichever template it was handed. Which mode a template &lt;em&gt;needs&lt;/em&gt; is a static
question — &lt;code&gt;symbolSites&lt;/code&gt; and &lt;code&gt;deepSymbolDemands&lt;/code&gt; (§7) answer it without
running anything.&lt;/p&gt;
&lt;h4 id="51-concrete-mode"&gt;5.1 Concrete mode&lt;/h4&gt;
&lt;p&gt;Output is exactly what ref §1 describes: Node JSON for a document program, a
plain JSON value for an expression program. Byte for byte, unchanged. An
expression program still pipes into &lt;code&gt;jq&lt;/code&gt;; a Node-JSON host still receives Node
JSON and nothing else.&lt;/p&gt;
&lt;p&gt;In concrete mode a symbol cannot exist:&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;form&lt;/th&gt;&lt;th&gt;concrete mode&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;?(key)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;SymbolsUnavailable&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;?ctx.path&lt;/code&gt;, supplied&lt;/td&gt;&lt;td&gt;the value the caller supplied; no symbol involved&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;?ctx.path&lt;/code&gt;, unsupplied at the root&lt;/td&gt;&lt;td&gt;&lt;code&gt;SymbolsUnavailable&lt;/code&gt; — that is the same minting&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;!expr&lt;/code&gt;&lt;/td&gt;&lt;td&gt;evaluated; the constraints are discarded&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;{"$sym": …}&lt;/code&gt; arriving in the context&lt;/td&gt;&lt;td&gt;rejected by the decoder (§5.3)&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Two of those rows are load-bearing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A library written against &lt;code&gt;?ctx.path&lt;/code&gt; runs in both modes.&lt;/strong&gt; Only &lt;em&gt;minting&lt;/em&gt;
needs symbolic mode; reading a demand the caller satisfied concretely is an
ordinary read. That is what makes a constraint-annotated library dual-use
rather than symbolic-only, and it is the reason the demand form is what
libraries should be written with.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;!&lt;/code&gt; is allowed and discarded&lt;/strong&gt;, not refused. A constraint over values the
caller supplied concretely is a checkable fact about a document that is
otherwise perfectly correct; refusing would make every annotated library
unusable in the half of its settings that do not care. The host has said it
does not consume constraints, and &lt;code&gt;deepConstraintKinds&lt;/code&gt; (§7) is there for
the host that would rather refuse the template up front.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So concrete mode is the language of &lt;code&gt;reference.md&lt;/code&gt; exactly, plus one rule
about &lt;code&gt;!&lt;/code&gt; and two refusals.&lt;/p&gt;
&lt;h4 id="52-symbolic-mode"&gt;5.2 Symbolic mode&lt;/h4&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;format&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;tramaj/symbolic/1&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;document&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;root&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;element&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;tag&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;Deployment&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;...&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;...&amp;quot;&lt;/span&gt; &lt;span class="fu"&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span id="5"&gt;&lt;a href="#5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;symbols&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="6"&gt;&lt;a href="#6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;#0:&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;d&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="7"&gt;&lt;a href="#7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;origin&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;alloc&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;site&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="dv"&gt;0&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;d&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span id="8"&gt;&lt;a href="#8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;binding&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;d&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="9"&gt;&lt;a href="#9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;#2:&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;web&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="10"&gt;&lt;a href="#10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;origin&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;alloc&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;site&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="dv"&gt;2&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;web&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span id="11"&gt;&lt;a href="#11" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;binding&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="kw"&gt;null&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="12"&gt;&lt;a href="#12" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;id&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;#ctx.threshold&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="13"&gt;&lt;a href="#13" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;origin&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;demand&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;ctx&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;threshold&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span id="14"&gt;&lt;a href="#14" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;binding&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;t&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id="15"&gt;&lt;a href="#15" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="16"&gt;&lt;a href="#16" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;constraints&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="17"&gt;&lt;a href="#17" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;gte&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="18"&gt;&lt;a href="#18" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;arguments&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;$sym&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;#0:&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;d&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;replicas&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt; &lt;span class="dv"&gt;1&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="19"&gt;&lt;a href="#19" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;allowed-zone&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="20"&gt;&lt;a href="#20" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;arguments&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;$sym&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;#0:&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;d&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;zone&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;eu&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span id="21"&gt;&lt;a href="#21" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span id="22"&gt;&lt;a href="#22" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;quot;kind&amp;quot;&lt;/code&gt; is &lt;code&gt;&amp;quot;document&amp;quot;&lt;/code&gt; or &lt;code&gt;&amp;quot;expression&amp;quot;&lt;/code&gt;. &lt;code&gt;&amp;quot;root&amp;quot;&lt;/code&gt; is Node JSON or a plain
value accordingly, and is byte-identical to what concrete mode would have
produced whenever the program uses no symbols.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Within a mode the shape is stable.&lt;/strong&gt; &lt;code&gt;&amp;quot;symbols&amp;quot;&lt;/code&gt; and &lt;code&gt;&amp;quot;constraints&amp;quot;&lt;/code&gt; are
&lt;code&gt;[]&lt;/code&gt; for a program that uses none, so a template growing its first &lt;code&gt;?&lt;/code&gt; does
not change the shape its host decodes. The shape changes when the host
changes its mind, never when the template does.
&lt;/li&gt;
&lt;li&gt;Only allocations appear in &lt;code&gt;&amp;quot;symbols&amp;quot;&lt;/code&gt;, and every one of them was allocated
by the root program, so the table is exactly as long as the author’s
declarations.
&lt;/li&gt;
&lt;li&gt;A symbol the host seeded (§5.4) arrives as a value and is echoed wherever it
was used; the language has nothing to add about it, so it is not listed.
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;quot;binding&amp;quot;&lt;/code&gt; is the name the allocation was bound to, or &lt;code&gt;null&lt;/code&gt;. It is
provenance for humans. &lt;code&gt;&amp;quot;origin&amp;quot;&lt;/code&gt; is the structured form of &lt;code&gt;&amp;quot;id&amp;quot;&lt;/code&gt;, so a
host never has to parse the id.
&lt;/li&gt;
&lt;li&gt;Every field is REQUIRED. A decoder MUST reject an object missing any of
them, and MUST NOT infer one from a default — the discipline &lt;code&gt;node-json.md&lt;/code&gt;
already sets.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A host fold may then: run a solver over &lt;code&gt;&amp;quot;constraints&amp;quot;&lt;/code&gt;; render an input for
every symbol reachable in &lt;code&gt;&amp;quot;root&amp;quot;&lt;/code&gt;; ask a user and re-run with the answers
seeded into the context; or compare &lt;code&gt;constraints[].name&lt;/code&gt; against what it
implements and fail with an unsupported-kind error before doing anything else.&lt;/p&gt;
&lt;h4 id="53-telling-a-symbol-from-an-object"&gt;5.3 Telling a symbol from an object&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Value&lt;/code&gt; in symbolic mode is JSON extended with one tagged shape, in the style
&lt;code&gt;node-json.md&lt;/code&gt; already uses to discriminate nodes by &lt;code&gt;&amp;quot;type&amp;quot;&lt;/code&gt; and attributes
by &lt;code&gt;&amp;quot;kind&amp;quot;&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;$sym&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;#0:&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;d&lt;/span&gt;&lt;span class="ch"&gt;\&amp;quot;&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;path&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;replicas&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Both fields are required. The ambiguity to rule out is a template, or a
context, carrying a data object whose key is &lt;code&gt;&amp;quot;$sym&amp;quot;&lt;/code&gt;. Neither can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Object keys in Tramaj are always static.&lt;/strong&gt; An object key is a quoted
string literal or a bare identifier, and ref §5 forbids interpolation there,
so nothing computes one. &lt;code&gt;{&amp;quot;$sym&amp;quot;: 1}&lt;/code&gt; written in a program is a &lt;strong&gt;parse
error&lt;/strong&gt;, in the same family as &lt;code&gt;import(&amp;quot;lib-`$x`&amp;quot;)&lt;/code&gt;.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;quot;$sym&amp;quot;&lt;/code&gt; and &lt;code&gt;&amp;quot;$type&amp;quot;&lt;/code&gt; are reserved in the value domain in both modes&lt;/strong&gt;,
in programs and in contexts alike. A context carrying either key is rejected
by the decoder: in symbolic mode unless it is a well-formed symbol
reference, and in concrete mode always. One rule, so a program’s legality
never depends on how a host runs it and a context does not change meaning
when a host changes mode.
&lt;/li&gt;
&lt;li&gt;Objects are otherwise built only by &lt;code&gt;&amp;lt;&amp;gt;&lt;/code&gt;, which merges keys from objects
that came from one of those two sources, so the sources are exhaustive.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;$type&amp;quot;&lt;/code&gt; is reserved by v3 but unused by it; it is v4’s (&lt;code&gt;v4-types.md&lt;/code&gt;).
Reserving it now costs two key names and avoids breaking the grammar later.&lt;/p&gt;
&lt;p&gt;The cost is those two names. The benefit is that the tag sits where a reader
looks for it and plain JSON stays plain: &lt;code&gt;{&amp;quot;replicas&amp;quot;: 3}&lt;/code&gt; remains
&lt;code&gt;{&amp;quot;replicas&amp;quot;: 3}&lt;/code&gt;, rather than the
&lt;code&gt;{&amp;quot;k&amp;quot;:&amp;quot;obj&amp;quot;,&amp;quot;v&amp;quot;:{&amp;quot;replicas&amp;quot;:{&amp;quot;k&amp;quot;:&amp;quot;num&amp;quot;,&amp;quot;v&amp;quot;:3}}}&lt;/code&gt; that a fully tagged value
domain would cost every attribute of every document.&lt;/p&gt;
&lt;h4 id="54-seeding"&gt;5.4 Seeding&lt;/h4&gt;
&lt;p&gt;In symbolic mode the input context accepts the same &lt;code&gt;{&amp;quot;$sym&amp;quot;: …}&lt;/code&gt; shape the
output emits, decoded recursively. A host that already knows about a variable
— because a user is filling a form, or because a solver resolved half of them
and the template is being re-run — supplies it as an ordinary context value,
and &lt;code&gt;?ctx.path&lt;/code&gt; reads it with no allocation. This is what makes the
ask-a-user and re-solve loops work without the template changing.&lt;/p&gt;
&lt;h4 id="55-profiles"&gt;5.5 Profiles&lt;/h4&gt;
&lt;p&gt;An implementation MAY implement the &lt;strong&gt;core profile&lt;/strong&gt; only — the language of
&lt;code&gt;reference.md&lt;/code&gt;, concrete output, no symbolic extension — and remain
conforming. It MUST reject &lt;code&gt;?&lt;/code&gt; and &lt;code&gt;!&lt;/code&gt; &lt;strong&gt;at parse time&lt;/strong&gt; rather than ignore
them, so that a symbolic template fails on it instead of quietly losing its
holes and its constraints. §5.3’s reserved-key rule belongs to the core
profile too, so that the two grammars agree.&lt;/p&gt;
&lt;p&gt;An implementation of the &lt;strong&gt;symbolic profile&lt;/strong&gt; implements both modes.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="6-errors"&gt;6. Errors&lt;/h3&gt;
&lt;p&gt;Three kinds join ref §12’s table.&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;error&lt;/th&gt;&lt;th&gt;when&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NotConcrete&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a symbolic value where the language requires a concrete one — §1.5's table, and a symbol used as an allocation key&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;AllocationInLibrary&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a program containing &lt;code&gt;?(k)&lt;/code&gt; is loaded as a library (§1.4)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;SymbolsUnavailable&lt;/code&gt;&lt;/td&gt;&lt;td&gt;a symbol would have to be minted in concrete mode (§5.1)&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;TypeMismatch&lt;/code&gt; covers the rest: a constraint value crossing a JSON boundary,
and a &lt;code&gt;!&lt;/code&gt; over something that is neither a constraint nor an array of them. An
unsupplied &lt;code&gt;?ctx.path&lt;/code&gt; inside a library is the existing &lt;code&gt;PathNotFound&lt;/code&gt;,
&lt;code&gt;InLibrary&lt;/code&gt;-wrapped.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AllocationInLibrary&lt;/code&gt; has a static counterpart: a program with a non-empty
&lt;code&gt;symbolSites&lt;/code&gt; cannot serve as a library, and a host building its library table
SHOULD check that up front rather than discover it on whichever path happens
to reach the &lt;code&gt;?&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Error message text remains implementation-defined (ref §13); the kinds are
stable.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="7-static-analysis"&gt;7. Static analysis&lt;/h3&gt;
&lt;p&gt;Answered from the AST alone — no context, no library evaluation, no host code
— alongside ref §9’s existing entries.&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;function&lt;/th&gt;&lt;th&gt;answers&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;symbolSites&lt;/code&gt;&lt;/td&gt;&lt;td&gt;the &lt;code&gt;?(k)&lt;/code&gt; allocation sites this program contains&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;symbolDemands&lt;/code&gt; / &lt;code&gt;deepSymbolDemands&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which context paths it declares symbolic with &lt;code&gt;?ctx.…&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;constraintKinds&lt;/code&gt; / &lt;code&gt;deepConstraintKinds&lt;/code&gt;&lt;/td&gt;&lt;td&gt;which constraint names it can emit&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;The deep variants follow imports through the library table with the same
traversal and cycle cutting ref §9’s deep analyses already use.&lt;/p&gt;
&lt;p&gt;Two of these earn their keep immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;deepConstraintKinds&lt;/code&gt; lets a host decide whether it supports a template
before running it — possible only because a constraint’s name is a static
position.
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deepSymbolDemands&lt;/code&gt; is the counterpart of ref §9’s &lt;code&gt;unsuppliedParams&lt;/code&gt; for
this feature. Since only the root allocates, knowing which paths the
libraries beneath will discuss symbolically &lt;em&gt;is&lt;/em&gt; the whole planning problem,
and it is answered without running anything.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;symbolSites&lt;/code&gt; reports sites, not keys, which are computed. The number of
symbols is a runtime fact; the number of sites is not, and a site inside a
&lt;code&gt;map&lt;/code&gt; is exactly where a reader should look to see what distinguishes them.&lt;/p&gt;
&lt;p&gt;Like the existing analyses, these over-approximate: a kind emitted only under
a &lt;code&gt;Branch&lt;/code&gt; arm that no context will select is still reported.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="8-conformance"&gt;8. Conformance&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;decode(encode(n))&lt;/code&gt; on the envelope MUST preserve the semantic result,
extending &lt;code&gt;node-json.md&lt;/code&gt;’s round-trip invariant.
&lt;/li&gt;
&lt;li&gt;Symbol ids are exact strings. Two conforming implementations MUST produce
the same ids, the same symbol table order and the same constraint order for
the same program and context — which is what §4’s prescribed evaluation
order and §1.4’s structural identity exist to guarantee.
&lt;/li&gt;
&lt;li&gt;The shared corpus &lt;code&gt;reference.md&lt;/code&gt; §14 calls for is where that is proved. The
astral-escape divergence recorded in &lt;code&gt;limitations&lt;/code&gt; is the precedent for why
neither implementation’s own suite would find a disagreement on its own.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Suggested fixture families: allocation identity under &lt;code&gt;map&lt;/code&gt;; a constant key
under &lt;code&gt;map&lt;/code&gt;; demand supplied concretely vs. symbolically vs. not at all;
symbols in attributes, payloads, value slots and text children; &lt;code&gt;!&lt;/code&gt; over a
single constraint, an array, a nested array, and a &lt;code&gt;branch&lt;/code&gt;; constraint
deduplication; library-emitted constraints; concrete mode refusing a mint and
discarding an emission; every row of §1.5’s table.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id="9-not-in-v3"&gt;9. Not in v3&lt;/h3&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;left out&lt;/th&gt;&lt;th&gt;where it went&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;nominal types, declarations, &lt;code&gt;.types&lt;/code&gt;, typed annotations&lt;/td&gt;&lt;td&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;any solver, domain or propagation&lt;/td&gt;&lt;td&gt;the host's, per §0&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;a keyless &lt;code&gt;?&lt;/code&gt;&lt;/td&gt;&lt;td&gt;§1.2 — a symbol always says what distinguishes it&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;allocation inside libraries, and any call-frame identity scheme&lt;/td&gt;&lt;td&gt;§1.4 — the root allocates, so identity is a site and a key&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;symbolic strings, symbolic control flow&lt;/td&gt;&lt;td&gt;§1.5, §1.8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;an envelope around concrete output&lt;/td&gt;&lt;td&gt;§5.1 — an expression program's result must still pipe into &lt;code&gt;jq&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;inferring the mode from the program&lt;/td&gt;&lt;td&gt;§5 — a template growing its first &lt;code&gt;?&lt;/code&gt; would silently change the shape its host decodes&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;v3 can still express typing informally, because a constraint argument may be
any JSON value: &lt;code&gt;!constraint(&amp;quot;has-type&amp;quot;, $d, &amp;quot;Deployment&amp;quot;)&lt;/code&gt; is an ordinary
constraint carrying a string, and a host may give it whatever meaning it
likes. What v4 adds is nominal identity, library-exported declarations and a
namespace, so that two libraries’ &lt;code&gt;Card&lt;/code&gt; are distinguishable and a type
reference is resolved rather than spelled.&lt;/p&gt;
&lt;p&gt;Recorded as considered and declined, to be revisited only if practice asks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A key on the demand form&lt;/strong&gt;, &lt;code&gt;?ctx.threshold($s.name)&lt;/code&gt;. Seeding (§5.4)
covers the case a host drives and &lt;code&gt;?(key)&lt;/code&gt; the case a template drives; this
is worth it only if something needs both at once.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A &lt;code&gt;label&lt;/code&gt; field on the symbol table entry.&lt;/strong&gt;
&lt;code&gt;!constraint(&amp;quot;label&amp;quot;, $x, &amp;quot;Replicas&amp;quot;)&lt;/code&gt; already says it, and says it in the
host’s vocabulary rather than the language’s.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Symbolic strings.&lt;/strong&gt; §1.8 has the cost: the value domain would gain terms,
not just variables.
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/reference-v3-symbols.html" rel="alternate"/><summary type="text">Generated from [`specs/v3-symbols.md`](https://github.com/lucasdicioccio/tramaj/blob/main/specs/v3-symbols.md) — the repository is the canonical source.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/reference-node-json.html</id><title type="text">Tramaj — Normative Node JSON representation</title><updated>2026-10-02T20:43:09Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;p&gt;&lt;em&gt;Generated from &lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/node-json.md"&gt;&lt;code&gt;specs/node-json.md&lt;/code&gt;&lt;/a&gt; — the repository is the canonical source, and may be ahead of this page.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="tramaj--normative-node-json-representation"&gt;Tramaj — Normative Node JSON representation&lt;/h2&gt;
&lt;p&gt;The evaluated &lt;code&gt;Node&lt;/code&gt; AST is the portable interchange boundary between Tramaj
implementations and their hosts. This document defines its &lt;strong&gt;normative&lt;/strong&gt; JSON
representation.&lt;/p&gt;
&lt;p&gt;An implementation MAY use any internal representation of nodes, but a conforming
implementation MUST be able to produce and consume the representation below
without loss: &lt;code&gt;decode(encode(n))&lt;/code&gt; MUST yield a node equal to &lt;code&gt;n&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Object key ordering is not semantically significant and MUST NOT be relied upon.&lt;/p&gt;
&lt;h3 id="node-ast"&gt;Node AST&lt;/h3&gt;
&lt;pre&gt;&lt;code class="language-text"&gt;Annotations = Map&amp;lt;String, JSON&amp;gt;

Node
  = Text
      value: Value
      annotations: Annotations

  | Element
      tag: String
      attributes: List&amp;lt;NodeAttribute&amp;gt;
      value: Value
      children: List&amp;lt;Node&amp;gt;
      annotations: Annotations

  | Fragment
      children: List&amp;lt;Node&amp;gt;
      annotations: Annotations

NodeAttribute
  = Attribute
      name: String
      value: Value

  | Action
      event: String
      key: String
      payload: Value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Value&lt;/code&gt; is the ordinary JSON value domain — &lt;code&gt;null&lt;/code&gt;, boolean, number, string,
array, object. It is &lt;em&gt;not&lt;/em&gt; a &lt;code&gt;Node&lt;/code&gt;: a document tree never nests through an
attribute value, an element value slot, or an action payload.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Text&lt;/code&gt; carries a &lt;code&gt;Value&lt;/code&gt;, not a &lt;code&gt;String&lt;/code&gt;, so scalars survive evaluation without
implicit conversion: &lt;code&gt;.p($ctx.count)&lt;/code&gt; with &lt;code&gt;count = 3&lt;/code&gt; produces a text node whose
value is the number &lt;code&gt;3&lt;/code&gt;, not the string &lt;code&gt;&amp;quot;3&amp;quot;&lt;/code&gt;. A host that wants a string renders
one; the interchange format does not decide that for it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Element&lt;/code&gt; carries a &lt;code&gt;value&lt;/code&gt; slot alongside its children, for hosts whose target
format attaches a body value to a tagged node (a scalar leaf in YAML/HCL, a
configuration value, an editor model). It defaults to &lt;code&gt;null&lt;/code&gt; when a template does
not set one.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Fragment&lt;/code&gt; is an ordered sequence of siblings introducing no wrapper element. It
is a real node in this representation; a host MAY flatten fragments when folding
into its own target.&lt;/p&gt;
&lt;h3 id="json-encoding"&gt;JSON encoding&lt;/h3&gt;
&lt;p&gt;Every object carries a discriminator: &lt;code&gt;&amp;quot;type&amp;quot;&lt;/code&gt; for nodes, &lt;code&gt;&amp;quot;kind&amp;quot;&lt;/code&gt; for attributes.
Every field listed is REQUIRED — including &lt;code&gt;&amp;quot;annotations&amp;quot;&lt;/code&gt;, which is &lt;code&gt;{}&lt;/code&gt; when a
node has none, and &lt;code&gt;&amp;quot;value&amp;quot;&lt;/code&gt;, which is &lt;code&gt;null&lt;/code&gt; when an element has no value slot.
An encoder MUST emit all of them; a decoder MUST reject an object missing any.&lt;/p&gt;
&lt;h4 id="text"&gt;Text&lt;/h4&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="dv"&gt;3&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;annotations&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;hello&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;annotations&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{}}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h4 id="element"&gt;Element&lt;/h4&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;element&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;tag&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;button&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;attributes&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="5"&gt;&lt;a href="#5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;attribute&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;class&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;primary&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="6"&gt;&lt;a href="#6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;kind&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;action&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;event&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;on-click&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;key&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;deploy&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="7"&gt;&lt;a href="#7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;     &lt;span class="dt"&gt;&amp;quot;payload&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;deployment&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;web&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;}}&lt;/span&gt;&lt;/span&gt;
&lt;span id="8"&gt;&lt;a href="#8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="9"&gt;&lt;a href="#9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="kw"&gt;null&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="10"&gt;&lt;a href="#10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;children&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="11"&gt;&lt;a href="#11" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;Deploy&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;annotations&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{}}&lt;/span&gt;&lt;/span&gt;
&lt;span id="12"&gt;&lt;a href="#12" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="13"&gt;&lt;a href="#13" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;annotations&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{}&lt;/span&gt;&lt;/span&gt;
&lt;span id="14"&gt;&lt;a href="#14" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;attributes&lt;/code&gt; is an ordered list, not a map: an element may carry any number of
attributes and any number of actions, and their source order is preserved.
Ordinary attributes and actions share one list because both are attribute-position
constructs; a host reading only one kind filters on &lt;code&gt;&amp;quot;kind&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;An attribute &lt;code&gt;name&lt;/code&gt; may repeat. The representation does not deduplicate; a host
decides what a repeated name means for its target.&lt;/p&gt;
&lt;h4 id="fragment"&gt;Fragment&lt;/h4&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;fragment&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;children&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;one&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;annotations&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{}}&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="5"&gt;&lt;a href="#5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    &lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;value&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;two&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt; &lt;span class="dt"&gt;&amp;quot;annotations&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{}}&lt;/span&gt;&lt;/span&gt;
&lt;span id="6"&gt;&lt;a href="#6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="7"&gt;&lt;a href="#7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  &lt;span class="dt"&gt;&amp;quot;annotations&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="fu"&gt;{}&lt;/span&gt;&lt;/span&gt;
&lt;span id="8"&gt;&lt;a href="#8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3 id="annotations"&gt;Annotations&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;annotations&lt;/code&gt; is a JSON object mapping annotation keys to arbitrary JSON.&lt;/p&gt;
&lt;p&gt;The core language assigns no meaning to any key or value. Annotations are the
extension point for types, domains and constraints, provenance, host metadata, and
optimization hints.&lt;/p&gt;
&lt;p&gt;Unknown annotations MUST NOT affect core semantics, and implementations MUST
preserve them across every node-to-node transformation they perform —
&lt;code&gt;adapt-actions&lt;/code&gt; in particular copies a node’s annotations through unchanged.&lt;/p&gt;
&lt;h3 id="decoding"&gt;Decoding&lt;/h3&gt;
&lt;p&gt;A decoder MUST reject:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an object with no &lt;code&gt;&amp;quot;type&amp;quot;&lt;/code&gt;, or a &lt;code&gt;&amp;quot;type&amp;quot;&lt;/code&gt; outside &lt;code&gt;{text, element, fragment}&lt;/code&gt;;
&lt;/li&gt;
&lt;li&gt;an attribute with no &lt;code&gt;&amp;quot;kind&amp;quot;&lt;/code&gt;, or a &lt;code&gt;&amp;quot;kind&amp;quot;&lt;/code&gt; outside &lt;code&gt;{attribute, action}&lt;/code&gt;;
&lt;/li&gt;
&lt;li&gt;any object missing a field required for its discriminator;
&lt;/li&gt;
&lt;li&gt;a non-string &lt;code&gt;tag&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;event&lt;/code&gt;, or &lt;code&gt;key&lt;/code&gt;;
&lt;/li&gt;
&lt;li&gt;a non-array &lt;code&gt;attributes&lt;/code&gt; or &lt;code&gt;children&lt;/code&gt;, or a non-object &lt;code&gt;annotations&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A decoder MUST NOT infer a missing field from a default. Defaults are an encoder’s
job; on the wire the representation is explicit, so that a missing field is a bug
rather than a silent &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/reference-node-json.html" rel="alternate"/><summary type="text">Generated from [`specs/node-json.md`](https://github.com/lucasdicioccio/tramaj/blob/main/specs/node-json.md) — the repository is the canonical source.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/tramaj-compared-to-other-tools.html</id><title type="text">tramaj compared to other tools</title><updated>2026-10-02T01:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="tramaj-compared-to-other-tools"&gt;tramaj compared to other tools&lt;/h2&gt;
&lt;p&gt;tramaj sits between three families of tools: string template engines, JSX-style
embedded document syntaxes, and configuration languages. This page says
where it overlaps with each, where it differs, and when another tool is the
better choice. For the argument behind the design, see
&lt;a href="/tramaj/what-sets-tramaj-apart.html"&gt;What sets tramaj apart&lt;/a&gt;; for the guarantees,
see &lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="at-a-glance"&gt;At a glance&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;String templates&lt;/strong&gt; (Jinja, Handlebars, Liquid, Go templates): output is
text; inputs are discovered by running or grepping; untrusted source depends
on the sandbox.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSX and similar&lt;/strong&gt;: output is an element tree; the expression language is
the full host language; the program’s interface is whatever the host’s type
checker knows.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configuration languages&lt;/strong&gt; (Jsonnet, Dhall, CUE, Nickel): output is data;
the language is small, functional and restricted; the interface is types or
schemas.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tramaj&lt;/strong&gt;: output is a document tree; the language is small, functional and
shared by several runtimes; inputs are declared as literal &lt;code&gt;ctx(path)&lt;/code&gt; holes;
imports, context reads and action keys are recoverable by walking the AST.
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="against-string-template-engines"&gt;Against string template engines&lt;/h3&gt;
&lt;p&gt;Jinja, Handlebars, Liquid, Mustache and Go’s &lt;code&gt;text/template&lt;/code&gt; render text.
The template is a string with holes, and the output is a string. That is
simple and works everywhere, but a few things follow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Escaping is a concern of the author. A tree-valued output makes
well-formed markup the default instead of a discipline.
&lt;/li&gt;
&lt;li&gt;Composition goes through includes, partials, or blocks, which are
usually resolved by name at render time. In tramaj an &lt;code&gt;import&lt;/code&gt; is a value
you can bind, partially apply and pass around.
&lt;/li&gt;
&lt;li&gt;What a template reads is discoverable only by running it or by grepping.
tramaj &lt;code&gt;ctx(...)&lt;/code&gt; reads are literal paths, so a host can list them without
evaluating anything.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose a string engine when you produce plain text (emails, config files,
code) and want the most familiar syntax and the biggest ecosystem of
filters and editors.&lt;/p&gt;
&lt;h3 id="against-jsx-and-embedded-document-syntaxes"&gt;Against JSX and embedded document syntaxes&lt;/h3&gt;
&lt;p&gt;JSX, and its cousins in other languages, make documents values inside a
full programming language. tramaj shares that central idea. The difference
is the host: with JSX you get all of JavaScript, its tooling and its
libraries, and you also get a program that can do anything. tramaj keeps its
own small language so the same template runs the same on every runtime and
a tool can reason about it before it runs.&lt;/p&gt;
&lt;p&gt;Choose JSX when you are already in a JavaScript application, want the
component ecosystem, and trust the code that you compile. Choose tramaj when
templates are data that travel across languages, or when the source is
written by a model or a user you do not fully trust.&lt;/p&gt;
&lt;h3 id="against-configuration-languages"&gt;Against configuration languages&lt;/h3&gt;
&lt;p&gt;Jsonnet, Dhall, Nickel and CUE are the closest relatives in spirit: small,
functional, importable, restricted on purpose. They produce data, though,
and tramaj produces documents. Dhall brings a real type system and CUE
brings constraint unification; tramaj does not try to compete with either.
What tramaj adds is document composition, deferred and partially applied
imports, and a statically recoverable vocabulary of actions that a document
may attach to a node.&lt;/p&gt;
&lt;p&gt;Choose a configuration language when the output is configuration and you
want strong typing, schema validation or constraint solving. Choose tramaj
when the output is a rendered document with interactive actions.&lt;/p&gt;
&lt;h3 id="against-template-layers-on-top-of-configuration"&gt;Against template layers on top of configuration&lt;/h3&gt;
&lt;p&gt;Helm charts and similar tools put a text templating layer on top of YAML.
That mixes two syntaxes, so indentation and quoting become template
concerns. tramaj builds the tree directly, so there is no text stage where
structure can break.&lt;/p&gt;
&lt;h3 id="where-tramaj-is-not-the-right-tool"&gt;Where tramaj is not the right tool&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;You only need text output and your team already knows a string engine.
&lt;/li&gt;
&lt;li&gt;You want arbitrary computation, I/O or a large standard library inside the
template. tramaj is intentionally small and deterministic.
&lt;/li&gt;
&lt;li&gt;You need a rich type system or schema language over the data. Pair tramaj
with one rather than expecting it in the template language.
&lt;/li&gt;
&lt;li&gt;You need an established ecosystem of editor plugins, linters and
third-party filters. tramaj is young.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See &lt;a href="/tramaj/use-cases.html"&gt;Use cases&lt;/a&gt; for where the trade-offs pay off, and
&lt;a href="/tramaj/reference.html"&gt;Reference&lt;/a&gt; for the language itself.&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/tramaj-compared-to-other-tools.html" rel="alternate"/><summary type="text">How tramaj differs from string templates, JSX, and configuration languages, and when you should pick something else.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/integrate-tramaj.html</id><title type="text">Integrating tramaj into a host</title><updated>2026-09-25T12:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="integrating-tramaj-into-a-host"&gt;Integrating tramaj into a host&lt;/h2&gt;
&lt;p&gt;Every integration has the same two moving parts, regardless of language:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Evaluate&lt;/strong&gt; — parse a template and run it against a JSON context to get
a &lt;code&gt;Node&lt;/code&gt;: the normative, host-agnostic document tree
(&lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt;).
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fold&lt;/strong&gt; — walk that &lt;code&gt;Node&lt;/code&gt; and turn it into whatever your host actually
renders: real DOM, a UI framework’s virtual tree, another markup
language, a config file.
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Nothing about &lt;code&gt;Node&lt;/code&gt; or the evaluator knows about HTML, a browser, or any
particular UI framework (see &lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt;) — the fold is
always something &lt;em&gt;you&lt;/em&gt; write for your host, even where this repository
already ships one for you. Which of the six paths below applies depends
only on what language your host is written in.&lt;/p&gt;
&lt;p&gt;The implementations and how they relate: one specification and corpus, five
independently written ports, and the folds and CLIs layered on top of two of
them.&lt;/p&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/implementations.dot.png" alt="The specification and corpus feed five ports: tramaj (PureScript) with tramaj-halogen and tramaj-cli, tramaj-hs, tramaj-rs with tramaj-cli-rs, tramaj-js with tramaj-react, and tramaj-py" /&gt;&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;Host language&lt;/th&gt;&lt;th&gt;Get the &lt;code&gt;Node&lt;/code&gt;&lt;/th&gt;&lt;th&gt;Write the fold&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PureScript&lt;/td&gt;&lt;td&gt;depend on &lt;code&gt;tramaj-purs&lt;/code&gt; directly&lt;/td&gt;&lt;td&gt;reuse &lt;code&gt;tramaj-halogen&lt;/code&gt;, or write your own&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Haskell&lt;/td&gt;&lt;td&gt;depend on &lt;code&gt;tramaj-hs&lt;/code&gt; directly&lt;/td&gt;&lt;td&gt;write your own — none shipped yet&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Rust&lt;/td&gt;&lt;td&gt;depend on &lt;code&gt;tramaj-rs&lt;/code&gt; directly, or shell out to &lt;code&gt;tramaj-cli-rs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;write your own — none shipped yet&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;JavaScript / TypeScript&lt;/td&gt;&lt;td&gt;depend on &lt;code&gt;tramaj-js&lt;/code&gt; directly, or shell out to &lt;code&gt;tramaj-cli&lt;/code&gt;&lt;/td&gt;&lt;td&gt;reuse &lt;code&gt;tramaj-react&lt;/code&gt;, or write your own&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Python&lt;/td&gt;&lt;td&gt;depend on &lt;code&gt;tramaj-py&lt;/code&gt; directly, or shell out to &lt;code&gt;python -m tramaj&lt;/code&gt;&lt;/td&gt;&lt;td&gt;write your own — none shipped yet&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;anything else&lt;/td&gt;&lt;td&gt;shell out to &lt;code&gt;tramaj-cli&lt;/code&gt; or &lt;code&gt;tramaj-cli-rs&lt;/code&gt;, or reimplement the evaluator&lt;/td&gt;&lt;td&gt;write your own, over the JSON&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;h3 id="purescript--import-as-a-library-fold-with-tramaj-halogen"&gt;PureScript — import as a library, fold with &lt;code&gt;tramaj-halogen&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tramaj-purs&lt;/code&gt;, &lt;code&gt;tramaj-halogen&lt;/code&gt;, &lt;code&gt;tramaj-cli&lt;/code&gt;, and &lt;code&gt;playground&lt;/code&gt; are members of one
&lt;a href="https://github.com/purescript/spago"&gt;Spago&lt;/a&gt; workspace rooted at this
repository. A new PureScript package placed as a subdirectory of the
workspace, with &lt;code&gt;tramaj-purs&lt;/code&gt; (and &lt;code&gt;tramaj-halogen&lt;/code&gt; if you’re rendering through
Halogen) listed in its &lt;code&gt;spago.yaml&lt;/code&gt; dependencies, is picked up by Spago’s
workspace discovery with no registry publishing step needed.&lt;/p&gt;
&lt;p&gt;The core API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- Tramaj.Parser
parseProgram :: String -&amp;gt; Either ParseError Program

-- Tramaj.Eval
runProgram :: Mode -&amp;gt; LibraryTable -&amp;gt; Json -&amp;gt; Program -&amp;gt; Either EvalError Json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Mode&lt;/code&gt; is &lt;code&gt;Concrete&lt;/code&gt; or &lt;code&gt;Symbolic&lt;/code&gt; (&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;);
&lt;code&gt;LibraryTable&lt;/code&gt; supplies whatever the template’s &lt;code&gt;import(...)&lt;/code&gt; calls need.
&lt;code&gt;runProgram&lt;/code&gt; gives you the evaluated document as JSON directly, already
encoded per &lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt;. If you need the
&lt;code&gt;Node&lt;/code&gt; value itself rather than its JSON encoding — to fold it without a
round-trip through JSON — &lt;code&gt;evalProgramWithEmissions&lt;/code&gt; is the lower-level
entry point that stops one step earlier.&lt;/p&gt;
&lt;p&gt;If your host renders through &lt;a href="https://github.com/purescript-halogen/purescript-halogen"&gt;Halogen&lt;/a&gt;,
don’t write a fold at all — &lt;code&gt;tramaj-halogen&lt;/code&gt; already has one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- Tramaj.Halogen
foldToHalogen
  :: forall action slots m
   . (String -&amp;gt; String -&amp;gt; Json -&amp;gt; Maybe action)
  -&amp;gt; Node
  -&amp;gt; Array (ComponentHTML action slots m)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first argument is a &lt;code&gt;dispatch&lt;/code&gt; function: event type, action key, and
JSON payload in, an optional host action out. A read-only host supplies
&lt;code&gt;\_ _ _ -&amp;gt; Nothing&lt;/code&gt;. The result is an &lt;code&gt;Array&lt;/code&gt;, not a single element, because
a fragment contributes several siblings with no wrapper element. Call
&lt;code&gt;validateAttrNames&lt;/code&gt; on the &lt;code&gt;Node&lt;/code&gt; first — attribute names aren’t validated
by the fold itself.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/playground/src/Playground/Main.purs"&gt;&lt;code&gt;playground/src/Playground/Main.purs&lt;/code&gt;&lt;/a&gt;
is the canonical worked example of the whole pipeline —
&lt;code&gt;Tramaj.Parser&lt;/code&gt; → &lt;code&gt;Tramaj.Eval&lt;/code&gt; → &lt;code&gt;Tramaj.Halogen.foldToHalogen&lt;/code&gt; — including
&lt;code&gt;dispatchAction&lt;/code&gt;, which turns &lt;code&gt;action(...)&lt;/code&gt; clicks into real Halogen actions
for an &lt;em&gt;interactive&lt;/em&gt; host (a read-only host just passes &lt;code&gt;const Nothing&lt;/code&gt;
instead).&lt;/p&gt;
&lt;h3 id="haskell--the-library-exists-the-fold-doesnt-yet"&gt;Haskell — the library exists, the fold doesn’t yet&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tramaj-hs&lt;/code&gt; is an independent Haskell port, built on megaparsec and aeson,
that targets the same grammar as the PureScript &lt;code&gt;tramaj-purs&lt;/code&gt; and shares its
conformance corpus. It’s a normal Cabal library exposing &lt;code&gt;Tramaj.Ast&lt;/code&gt;,
&lt;code&gt;Tramaj.Parser&lt;/code&gt;, &lt;code&gt;Tramaj.Eval&lt;/code&gt;, &lt;code&gt;Tramaj.Node&lt;/code&gt;, &lt;code&gt;Tramaj.Analysis&lt;/code&gt;, and
&lt;code&gt;Tramaj.Types&lt;/code&gt; — so a Haskell host can depend on it directly (as a
&lt;code&gt;source-repository-package&lt;/code&gt; or path dependency; it isn’t published to
Hackage), the same way a PureScript host depends on &lt;code&gt;tramaj-purs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What it does &lt;em&gt;not&lt;/em&gt; have yet is a &lt;code&gt;tramaj-halogen&lt;/code&gt; equivalent — no fold to
Lucid, Blaze, or any other Haskell HTML/UI library ships in this
repository. A Haskell host gets the parser, the evaluator, and the &lt;code&gt;Node&lt;/code&gt;
JSON encoding (kept in lockstep with the PureScript &lt;code&gt;Tramaj.Node&lt;/code&gt;); writing
the fold to your actual rendering library is on you. The shape to follow is
&lt;code&gt;tramaj-halogen&lt;/code&gt;’s: case on &lt;code&gt;Text&lt;/code&gt;/&lt;code&gt;Element&lt;/code&gt;/&lt;code&gt;Fragment&lt;/code&gt;, recurse into
children, and decide what an &lt;code&gt;action(...)&lt;/code&gt; payload means in your host.&lt;/p&gt;
&lt;h3 id="rust--the-library-exists-plus-its-own-cli"&gt;Rust — the library exists, plus its own CLI&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tramaj-rs&lt;/code&gt; is an independent, hand-written Rust port of the same grammar
and evaluation semantics as the PureScript &lt;code&gt;tramaj-purs&lt;/code&gt; and Haskell &lt;code&gt;tramaj-hs&lt;/code&gt;,
held to the same shared conformance corpus (&lt;code&gt;corpus/cases/&lt;/code&gt; — full parity,
including &lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;’s symbols/constraints
and &lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt;’s nominal types, not just the
core language). It’s a normal Cargo library crate — &lt;code&gt;tramaj_rs::{ast, parser, eval, node, analysis, types}&lt;/code&gt; — depended on as a path or git dependency (not
published to crates.io yet), the same way a PureScript host depends on
&lt;code&gt;tramaj-purs&lt;/code&gt; or a Haskell host depends on &lt;code&gt;tramaj-hs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="co"&gt;// tramaj_rs::parser&lt;/span&gt;&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;pub&lt;/span&gt; &lt;span class="kw"&gt;fn&lt;/span&gt; parse_program(src&lt;span class="op"&gt;:&lt;/span&gt; &lt;span class="op"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dt"&gt;str&lt;/span&gt;) &lt;span class="op"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="dt"&gt;Result&lt;/span&gt;&lt;span class="op"&gt;&amp;lt;&lt;/span&gt;Program&lt;span class="op"&gt;,&lt;/span&gt; ParseError&lt;span class="op"&gt;&amp;gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="co"&gt;// tramaj_rs::eval&lt;/span&gt;&lt;/span&gt;
&lt;span id="5"&gt;&lt;a href="#5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;pub&lt;/span&gt; &lt;span class="kw"&gt;fn&lt;/span&gt; run_program(&lt;/span&gt;
&lt;span id="6"&gt;&lt;a href="#6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    mode&lt;span class="op"&gt;:&lt;/span&gt; Mode&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="7"&gt;&lt;a href="#7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    libs&lt;span class="op"&gt;:&lt;/span&gt; &lt;span class="op"&gt;&amp;amp;&lt;/span&gt;LibraryTable&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="8"&gt;&lt;a href="#8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    ctx&lt;span class="op"&gt;:&lt;/span&gt; &lt;span class="op"&gt;&amp;amp;&lt;/span&gt;&lt;span class="pp"&gt;serde_json::&lt;/span&gt;Value&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="9"&gt;&lt;a href="#9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;    program&lt;span class="op"&gt;:&lt;/span&gt; &lt;span class="op"&gt;&amp;amp;&lt;/span&gt;Program&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="10"&gt;&lt;a href="#10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;) &lt;span class="op"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="dt"&gt;Result&lt;/span&gt;&lt;span class="op"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pp"&gt;serde_json::&lt;/span&gt;Value&lt;span class="op"&gt;,&lt;/span&gt; EvalError&lt;span class="op"&gt;&amp;gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;run_program&lt;/code&gt; gives you the evaluated result as JSON directly, already
encoded per &lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt; when the program
produces a document, or the concrete/symbolic envelope from
&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt; §5 otherwise — the same shape
&lt;code&gt;runProgram&lt;/code&gt; returns on the PureScript and Haskell sides. As with Haskell,
there’s no &lt;code&gt;tramaj-halogen&lt;/code&gt; equivalent shipped yet: a Rust host gets the
parser, evaluator, &lt;code&gt;Node&lt;/code&gt; JSON codec, and static analysis, and writes its
own fold following the same shape (case on &lt;code&gt;Text&lt;/code&gt;/&lt;code&gt;Element&lt;/code&gt;/&lt;code&gt;Fragment&lt;/code&gt;,
recurse into children, decide what an &lt;code&gt;action(...)&lt;/code&gt; payload means in your
host).&lt;/p&gt;
&lt;p&gt;Unlike the Haskell port, Rust also has a &lt;code&gt;tramaj-cli&lt;/code&gt; equivalent —
&lt;code&gt;tramaj-cli-rs&lt;/code&gt; — mirroring &lt;code&gt;tramaj-cli&lt;/code&gt;’s command surface field-for-field:
the same &lt;code&gt;evaluate&lt;/code&gt;/&lt;code&gt;analyze &amp;lt;imports|actions|holes|unsupplied|constraints|symbols|types|card|all&amp;gt;&lt;/code&gt;
subcommands, the same &lt;code&gt;--lib name=path&lt;/code&gt;/&lt;code&gt;--mode concrete|symbolic&lt;/code&gt; flags,
and the same JSON output shapes. If you’d rather shell out than link a Rust
library into your host — including from a non-Rust host — &lt;code&gt;tramaj-cli-rs&lt;/code&gt; is
a drop-in alternative to &lt;code&gt;tramaj-cli&lt;/code&gt; with no Node.js runtime required.&lt;/p&gt;
&lt;h3 id="javascript--typescript--import-as-a-library-fold-with-tramaj-react"&gt;JavaScript / TypeScript — import as a library, fold with &lt;code&gt;tramaj-react&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tramaj-js&lt;/code&gt; is an independent, hand-written TypeScript port of the same
grammar and evaluation semantics as the PureScript &lt;code&gt;tramaj-purs&lt;/code&gt;, Haskell
&lt;code&gt;tramaj-hs&lt;/code&gt;, and Rust &lt;code&gt;tramaj-rs&lt;/code&gt;, held to the same shared conformance
corpus (&lt;code&gt;corpus/cases/&lt;/code&gt; — full parity, including
&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;’s symbols/constraints and
&lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt;’s nominal types, not just the
core language). It’s a normal npm package with no React or DOM dependency
— a plain Node or browser host can depend on it the same way a PureScript
host depends on &lt;code&gt;tramaj-purs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="co"&gt;// parseProgram, tryParseProgram&lt;/span&gt;&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;function&lt;/span&gt; &lt;span class="fu"&gt;parseProgram&lt;/span&gt;(src&lt;span class="op"&gt;:&lt;/span&gt; &lt;span class="dt"&gt;string&lt;/span&gt;)&lt;span class="op"&gt;:&lt;/span&gt; Program&lt;span class="op"&gt;;&lt;/span&gt; &lt;span class="co"&gt;// throws ParseError&lt;/span&gt;&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="co"&gt;// runProgram&lt;/span&gt;&lt;/span&gt;
&lt;span id="5"&gt;&lt;a href="#5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;function&lt;/span&gt; &lt;span class="fu"&gt;runProgram&lt;/span&gt;(&lt;/span&gt;
&lt;span id="6"&gt;&lt;a href="#6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  mode&lt;span class="op"&gt;:&lt;/span&gt; Mode&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="7"&gt;&lt;a href="#7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  libs&lt;span class="op"&gt;:&lt;/span&gt; LibraryTable&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="8"&gt;&lt;a href="#8" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  ctx&lt;span class="op"&gt;:&lt;/span&gt; Json&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="9"&gt;&lt;a href="#9" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  program&lt;span class="op"&gt;:&lt;/span&gt; Program&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="10"&gt;&lt;a href="#10" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;)&lt;span class="op"&gt;:&lt;/span&gt; Json&lt;span class="op"&gt;;&lt;/span&gt; &lt;span class="co"&gt;// throws EvalError&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Mode&lt;/code&gt; is &lt;code&gt;&amp;quot;concrete&amp;quot;&lt;/code&gt; or &lt;code&gt;&amp;quot;symbolic&amp;quot;&lt;/code&gt; (&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;);
&lt;code&gt;runProgram&lt;/code&gt; gives you the evaluated result as JSON directly, already
encoded per &lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt; when the program
produces a document, or the concrete/symbolic envelope from
&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt; §5 otherwise — the same shape
&lt;code&gt;runProgram&lt;/code&gt; returns on the PureScript, Haskell, and Rust sides. &lt;code&gt;evalProgram&lt;/code&gt;
is the lower-level entry point if you want the &lt;code&gt;Node&lt;/code&gt; value itself rather
than its JSON encoding, to fold it without a round-trip through JSON.&lt;/p&gt;
&lt;p&gt;If your host renders through React, don’t write a fold at all —
&lt;code&gt;tramaj-react&lt;/code&gt; already has one, the same shape as &lt;code&gt;tramaj-halogen&lt;/code&gt;’s:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;type&lt;/span&gt; Dispatch &lt;span class="op"&gt;=&lt;/span&gt; (&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  event&lt;span class="op"&gt;:&lt;/span&gt; &lt;span class="dt"&gt;string&lt;/span&gt;&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  key&lt;span class="op"&gt;:&lt;/span&gt; &lt;span class="dt"&gt;string&lt;/span&gt;&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;  payload&lt;span class="op"&gt;:&lt;/span&gt; Json&lt;span class="op"&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span id="5"&gt;&lt;a href="#5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;) &lt;span class="kw"&gt;=&amp;gt;&lt;/span&gt; MouseEventHandler&lt;span class="op"&gt;&amp;lt;&lt;/span&gt;&lt;span class="bu"&gt;Element&lt;/span&gt;&lt;span class="op"&gt;&amp;gt;&lt;/span&gt; &lt;span class="op"&gt;|&lt;/span&gt; &lt;span class="kw"&gt;undefined&lt;/span&gt; &lt;span class="op"&gt;|&lt;/span&gt; &lt;span class="kw"&gt;null&lt;/span&gt;&lt;span class="op"&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span id="6"&gt;&lt;a href="#6" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="7"&gt;&lt;a href="#7" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="kw"&gt;function&lt;/span&gt; &lt;span class="fu"&gt;foldToReact&lt;/span&gt;(dispatch&lt;span class="op"&gt;:&lt;/span&gt; Dispatch&lt;span class="op"&gt;,&lt;/span&gt; node&lt;span class="op"&gt;:&lt;/span&gt; &lt;span class="bu"&gt;Node&lt;/span&gt;)&lt;span class="op"&gt;:&lt;/span&gt; ReactNode[]&lt;span class="op"&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;dispatch&lt;/code&gt; decides which &lt;code&gt;action(...)&lt;/code&gt; entries become a real event handler
— it inspects the event type itself (&lt;code&gt;foldToReact&lt;/code&gt; doesn’t), and returns
&lt;code&gt;undefined&lt;/code&gt;/&lt;code&gt;null&lt;/code&gt; for ones it wants to ignore. Every action a &lt;code&gt;dispatch&lt;/code&gt;
call accepts is wired to &lt;code&gt;onClick&lt;/code&gt;. The result is an array, not a single
element, because a fragment contributes several siblings with no wrapper —
splice it into your own container, &lt;code&gt;&amp;lt;&amp;gt;{foldToReact(dispatch, node)}&amp;lt;/&amp;gt;&lt;/code&gt;.
Call &lt;code&gt;validateAttrNames&lt;/code&gt; on the &lt;code&gt;Node&lt;/code&gt; first, same caveat as
&lt;code&gt;tramaj-halogen&lt;/code&gt;: attribute names aren’t validated by the fold itself.&lt;/p&gt;
&lt;p&gt;If you’d rather not add a JS dependency at all — or your host isn’t Node —
&lt;code&gt;tramaj-cli&lt;/code&gt; (or &lt;code&gt;tramaj-cli-rs&lt;/code&gt;) as a subprocess is still the same option
open to any other language; see &lt;a href="/tramaj/howto-humans.html"&gt;Getting started&lt;/a&gt; for
the exact CLI invocations, and &lt;em&gt;Any other language&lt;/em&gt; below for the general
shape of folding its JSON output by hand.&lt;/p&gt;
&lt;h3 id="python--import-as-a-library-standard-library-only"&gt;Python — import as a library, standard library only&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tramaj-py&lt;/code&gt; is an independent, hand-written Python port of the same grammar
and evaluation semantics as the other four implementations, held to the
same shared conformance corpus (&lt;code&gt;corpus/cases/&lt;/code&gt; — full parity, including
&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;’s symbols/constraints and
&lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt;’s nominal types). It needs
nothing outside the standard library (Python 3.9+), so it drops into a data
pipeline, a notebook or a backend service without a Node or PureScript
toolchain as a runtime dependency. It’s a normal package — &lt;code&gt;tramaj&lt;/code&gt; with
&lt;code&gt;ast&lt;/code&gt;, &lt;code&gt;parser&lt;/code&gt;, &lt;code&gt;evaluator&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;analysis&lt;/code&gt;, &lt;code&gt;typesys&lt;/code&gt; and &lt;code&gt;jsonval&lt;/code&gt;
modules, laid out one-to-one like &lt;code&gt;tramaj-js&lt;/code&gt; — installed from the
repository’s &lt;code&gt;tramaj-py/&lt;/code&gt; directory (not published to PyPI yet):&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="im"&gt;from&lt;/span&gt; tramaj &lt;span class="im"&gt;import&lt;/span&gt; parse_program, run_program&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;program &lt;span class="op"&gt;=&lt;/span&gt; parse_program(&lt;span class="st"&gt;&amp;#39;.p($ctx.name)&amp;#39;&lt;/span&gt;)          &lt;span class="co"&gt;# raises ParseError&lt;/span&gt;&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;run_program(&lt;span class="st"&gt;&amp;quot;concrete&amp;quot;&lt;/span&gt;, {}, {&lt;span class="st"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="st"&gt;&amp;quot;web&amp;quot;&lt;/span&gt;}, program)  &lt;span class="co"&gt;# raises EvalError&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;run_program&lt;/code&gt; gives you the evaluated result as plain Python JSON values
(&lt;code&gt;dict&lt;/code&gt;/&lt;code&gt;list&lt;/code&gt;/&lt;code&gt;str&lt;/code&gt;/numbers/&lt;code&gt;bool&lt;/code&gt;/&lt;code&gt;None&lt;/code&gt;), already encoded per
&lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt; when the program produces a
document, or the concrete/symbolic envelope from
&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt; §5 otherwise — the same shape
&lt;code&gt;runProgram&lt;/code&gt; returns on every other side. A library table is a &lt;code&gt;dict&lt;/code&gt; from
import name to parsed program; the mode is &lt;code&gt;&amp;quot;concrete&amp;quot;&lt;/code&gt; or &lt;code&gt;&amp;quot;symbolic&amp;quot;&lt;/code&gt;.
&lt;code&gt;eval_program&lt;/code&gt; is the lower-level entry point if you want the &lt;code&gt;Node&lt;/code&gt; value
itself rather than its JSON encoding. Numbers keep the language’s double
semantics: an exact integer comes back as an &lt;code&gt;int&lt;/code&gt;, and &lt;code&gt;str&lt;/code&gt; renders as
ECMAScript’s &lt;code&gt;Number::toString&lt;/code&gt; does, so interpolated text matches the
other implementations byte-for-byte.&lt;/p&gt;
&lt;p&gt;As with Haskell and Rust, no fold is shipped: a Python host writes its own
following the same shape (case on &lt;code&gt;text&lt;/code&gt;/&lt;code&gt;element&lt;/code&gt;/&lt;code&gt;fragment&lt;/code&gt;, recurse into
children, decide what an &lt;code&gt;action(...)&lt;/code&gt; payload means in your host). Python
also has its own &lt;code&gt;tramaj-cli&lt;/code&gt; equivalent, &lt;code&gt;python -m tramaj&lt;/code&gt;, mirroring
&lt;code&gt;tramaj-cli-rs&lt;/code&gt;’s &lt;code&gt;evaluate&lt;/code&gt;/&lt;code&gt;analyze&lt;/code&gt; subcommands, flags and JSON output
shapes, so a non-Python host with a Python interpreter around can shell out
to it instead of to Node.&lt;/p&gt;
&lt;h3 id="any-other-language"&gt;Any other language&lt;/h3&gt;
&lt;p&gt;For a host in a language with no PureScript, Haskell, Rust, JavaScript, or
Python interop story — Go, Java, whatever — there are two options:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(a) Full reimplementation.&lt;/strong&gt; Port the parser and evaluator to your
language, the way &lt;code&gt;tramaj-hs&lt;/code&gt; ports the PureScript original. Worth it only
if you need to &lt;em&gt;evaluate&lt;/em&gt; templates natively (e.g. embedding evaluation in
a process that can’t shell out, or needs it to run in-process for
performance). &lt;a href="/tramaj/reference-language.html"&gt;&lt;code&gt;reference.md&lt;/code&gt;&lt;/a&gt; is the full
grammar and evaluation semantics to implement against, and the conformance
corpus (&lt;code&gt;corpus/cases/&lt;/code&gt; in the repository — template + context + expected
output triples) is what the five existing implementations are held to; a
new implementation should be checked against the same corpus.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(b) Partial implementation, starting from &lt;code&gt;tramaj-cli&lt;/code&gt;’s output.&lt;/strong&gt; For
most hosts this is the pragmatic choice: don’t reimplement the parser or
evaluator at all. Shell out to &lt;code&gt;tramaj-cli&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;tramaj-cli&lt;/span&gt; template.tramaj context.json&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;which prints the evaluated &lt;code&gt;Node&lt;/code&gt; as JSON on stdout, exactly per
&lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt;. Your host only needs to write
the &lt;em&gt;fold&lt;/em&gt; — parse that JSON, case on whether each node is text, an
element, or a fragment, recurse into children, and decide what each
element’s &lt;code&gt;action(...)&lt;/code&gt; entries mean in your host — the same shape as
&lt;code&gt;tramaj-halogen&lt;/code&gt;’s fold, just written against decoded JSON instead of a
native &lt;code&gt;Node&lt;/code&gt; value. &lt;code&gt;tramaj-cli&lt;/code&gt;’s &lt;code&gt;analyze&lt;/code&gt; subcommands
(&lt;code&gt;imports&lt;/code&gt;/&lt;code&gt;actions&lt;/code&gt;/&lt;code&gt;holes&lt;/code&gt;/&lt;code&gt;unsupplied&lt;/code&gt;/&lt;code&gt;constraints&lt;/code&gt;/&lt;code&gt;symbols&lt;/code&gt;/&lt;code&gt;types&lt;/code&gt;)
are available the same way, without a context file, if your host wants the
static-analysis facts from &lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt; rather than a
rendered document.&lt;/p&gt;
&lt;p&gt;This is also the fastest way to prototype an integration in any language
before deciding whether a full reimplementation is ever worth it.&lt;/p&gt;
&lt;p&gt;See the &lt;a href="/tramaj/roadmap.html"&gt;Roadmap&lt;/a&gt; for what a new implementation needs to
cover and where to start if you want to build one; Rust, JavaScript and
Python each have one now (&lt;code&gt;tramaj-rs&lt;/code&gt;, &lt;code&gt;tramaj-js&lt;/code&gt; and &lt;code&gt;tramaj-py&lt;/code&gt;, above).&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/integrate-tramaj.html" rel="alternate"/><summary type="text">Six ways to embed tramaj in a host, depending on your language: import the library, or work from tramaj-cli's JSON output.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/roadmap.html</id><title type="text">Roadmap</title><updated>2026-09-25T12:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="roadmap"&gt;Roadmap&lt;/h2&gt;
&lt;p&gt;No package in this repository has had a stable release yet, and the
language itself is still settling — see the
&lt;a href="https://github.com/lucasdicioccio/tramaj#roadmap-to-a-stable-release"&gt;repository README&lt;/a&gt;
for where the language grammar stands. This page is about something
narrower: which &lt;em&gt;implementations&lt;/em&gt; of tramaj exist, and which are wanted
next.&lt;/p&gt;
&lt;h3 id="where-things-stand-today"&gt;Where things stand today&lt;/h3&gt;
&lt;p&gt;Five independent implementations target the same grammar and are checked
against a shared, language-neutral
&lt;a href="https://github.com/lucasdicioccio/tramaj/tree/main/corpus"&gt;conformance corpus&lt;/a&gt; —
template/context/expected-output triples that both must reproduce
byte-for-byte, per case, in both &lt;code&gt;concrete&lt;/code&gt; and &lt;code&gt;symbolic&lt;/code&gt; evaluation modes.
That’s a deliberately strong bar: a case belongs in the corpus only if
independently written implementations can run it identically.&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;Implementation&lt;/th&gt;&lt;th&gt;Language&lt;/th&gt;&lt;th&gt;Status&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;tramaj&lt;/code&gt;&lt;/td&gt;&lt;td&gt;PureScript&lt;/td&gt;&lt;td&gt;reference implementation, checked against the corpus; &lt;code&gt;tramaj-halogen&lt;/code&gt; folds to Halogen, &lt;code&gt;tramaj-cli&lt;/code&gt; is its CLI&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;tramaj-hs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Haskell&lt;/td&gt;&lt;td&gt;independent port, checked against the same corpus&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;tramaj-rs&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Rust&lt;/td&gt;&lt;td&gt;independent port, checked against the same corpus; &lt;code&gt;tramaj-cli-rs&lt;/code&gt; is its CLI&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;tramaj-js&lt;/code&gt;&lt;/td&gt;&lt;td&gt;JavaScript / TypeScript&lt;/td&gt;&lt;td&gt;independent port, checked against the same corpus; &lt;code&gt;tramaj-react&lt;/code&gt; folds to React&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;tramaj-py&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Python&lt;/td&gt;&lt;td&gt;independent port on the standard library alone, checked against the same corpus; &lt;code&gt;python -m tramaj&lt;/code&gt; is its CLI&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/conformance.dot.png" alt="Every corpus case is run in both modes by each implementation, and every output must equal the expected output byte for byte" /&gt;&lt;/p&gt;
&lt;p&gt;None is published to a package registry yet (see the README’s
“Publishing” section) — all are used today as workspace, source or git
dependencies. Each has its own &lt;a href="/tramaj/topics/changelog.html"&gt;changelog&lt;/a&gt; page and
Atom feed. See &lt;a href="/tramaj/integrate-tramaj.html"&gt;Integrating tramaj&lt;/a&gt; for what’s
actually usable from each right now, including the fold each one does or
doesn’t ship.&lt;/p&gt;
&lt;h3 id="the-native-implementation-wish-list-is-done"&gt;The native-implementation wish list is done&lt;/h3&gt;
&lt;p&gt;Earlier versions of this page asked for native JavaScript, Python and Rust
implementations, so that hosts in those ecosystems would get what
PureScript and Haskell hosts already had: an in-process parser and
evaluator, with no subprocess and no JSON round-trip just to get a &lt;code&gt;Node&lt;/code&gt;.
All three exist now — &lt;code&gt;tramaj-js&lt;/code&gt;, &lt;code&gt;tramaj-py&lt;/code&gt; and &lt;code&gt;tramaj-rs&lt;/code&gt; — and each
passes the full corpus in both modes, v3 and v4 extensions included.&lt;/p&gt;
&lt;h3 id="desired-next-folds-and-releases"&gt;Desired next: folds, and releases&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Folds.&lt;/strong&gt; Only PureScript (&lt;code&gt;tramaj-halogen&lt;/code&gt;) and JavaScript
(&lt;code&gt;tramaj-react&lt;/code&gt;) ship a fold to a rendering target. A DOM fold for JS, a
fold to a common Python templating output, and a fold for at least one
Haskell or Rust UI or markup library are the obvious next pieces.
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Registry releases.&lt;/strong&gt; The crates.io, npm and PyPI packages are ready to
publish but not published; the PureScript registry and Hackage are
deliberately deferred until the API has settled. Releases happen when
there are major changes, and each package’s version tracks the language
version it implements in its major digit.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a wish list, not a schedule: there’s no committed timeline or
assigned owner for any of it.&lt;/p&gt;
&lt;h3 id="what-done-looks-like-for-a-new-implementation"&gt;What “done” looks like for a new implementation&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tramaj-hs&lt;/code&gt;, &lt;code&gt;tramaj-rs&lt;/code&gt;, &lt;code&gt;tramaj-js&lt;/code&gt; and &lt;code&gt;tramaj-py&lt;/code&gt; are the templates to
follow, since they were added &lt;em&gt;after&lt;/em&gt; the language had settled on a
normative spec and output format (&lt;code&gt;tramaj-py&lt;/code&gt; is the smallest: one module
per concern, standard library only). A new implementation is considered on
par with the existing ones once it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;implements the grammar and evaluation rules in
&lt;a href="/tramaj/reference-language.html"&gt;&lt;code&gt;reference.md&lt;/code&gt;&lt;/a&gt;, including the static
restrictions that make imports, action keys, and &lt;code&gt;ctx(...)&lt;/code&gt; paths
analyzable without evaluation (see &lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt;);
&lt;/li&gt;
&lt;li&gt;encodes and decodes the &lt;code&gt;Node&lt;/code&gt; AST exactly per
&lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt;, including the round-trip
guarantee (&lt;code&gt;decode(encode(n)) == n&lt;/code&gt;);
&lt;/li&gt;
&lt;li&gt;passes every case in the
&lt;a href="https://github.com/lucasdicioccio/tramaj/tree/main/corpus"&gt;conformance corpus&lt;/a&gt;,
in both &lt;code&gt;concrete&lt;/code&gt; and &lt;code&gt;symbolic&lt;/code&gt; mode, byte-for-byte against
&lt;code&gt;expected.json&lt;/code&gt;;
&lt;/li&gt;
&lt;li&gt;optionally, but ideally: a fold analogous to &lt;code&gt;tramaj-halogen&lt;/code&gt;’s
&lt;code&gt;foldToHalogen&lt;/code&gt; for at least one popular rendering target in that
ecosystem (e.g. a DOM fold for JS, a common Python templating output for
Python).
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The v3 (&lt;code&gt;?&lt;/code&gt;/&lt;code&gt;!&lt;/code&gt;, &lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;) and v4
(&lt;code&gt;%&lt;/code&gt;, &lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt;) extensions are both
shipped and part of the same conformance bar — a new implementation isn’t
considered complete while targeting only the v2 core.&lt;/p&gt;
&lt;h3 id="contributing-an-implementation"&gt;Contributing an implementation&lt;/h3&gt;
&lt;p&gt;If you’re taking on one of these, the corpus is the fastest way to find out
what you got wrong: it’s meant to be run against a from-scratch
implementation just as it’s run against the five that exist, and a case
that byte-for-byte matches all of them today is a strong signal your
evaluation semantics agree with theirs. Open an issue on the
&lt;a href="https://github.com/lucasdicioccio/tramaj"&gt;repository&lt;/a&gt; before investing
heavily in one — coordinating on which language is being worked on avoids
duplicate effort.&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/roadmap.html" rel="alternate"/><summary type="text">Five implementations exist today: PureScript, Haskell, Rust, JavaScript/TypeScript and Python, all held to one conformance corpus. What is wanted next is folds and registry releases.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj-halogen.html</id><title type="text">Changelog: tramaj-halogen</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-halogen"&gt;Changelog: tramaj-halogen&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-halogen&lt;/code&gt;, the Halogen host: folds an evaluated Node into Halogen.HTML. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/purescript.html"&gt;&lt;span class="hashtag" data-hashtag="purescript"&gt;#purescript&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-halogen.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-halogen"&gt;#tramaj-halogen&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v(none)"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-halogen&lt;/code&gt; (none)&lt;span class="changelog-date"&gt;unreleased&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Consumed as a git dependency; no registry release yet.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; `Tramaj.Halogen.foldToHalogen`, wiring each `action(...)` to the host&amp;#39;s own Action type.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj-halogen.html" rel="alternate"/><summary type="text">Release history of `tramaj-halogen`, the Halogen host: folds an evaluated Node into Halogen.HTML.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj.html</id><title type="text">Changelog: tramaj-purs</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-purs"&gt;Changelog: tramaj-purs&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-purs&lt;/code&gt;, the reference PureScript implementation: parser, evaluator, Node codec and analyses. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/purescript.html"&gt;&lt;span class="hashtag" data-hashtag="purescript"&gt;#purescript&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-purs.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-purs"&gt;#tramaj-purs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v(none)"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-purs&lt;/code&gt; (none)&lt;span class="changelog-date"&gt;unreleased&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Consumed as a git dependency; no registry release yet.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Reference implementation of the language: parser, evaluator (concrete and symbolic modes), normative Node JSON codec and static analyses.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Conformance corpus under `corpus/cases`, shared with every other implementation.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Object destructuring patterns in `@` bindings and lambda parameters (decisions section 17): a parser lowering to plain `Let`/`Lambda`, checked by shared corpus cases 100-118.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj.html" rel="alternate"/><summary type="text">Release history of `tramaj-purs`, the reference PureScript implementation: parser, evaluator, Node codec and analyses.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj-hs.html</id><title type="text">Changelog: tramaj-hs</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-hs"&gt;Changelog: tramaj-hs&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-hs&lt;/code&gt;, the Haskell implementation, also the engine behind Kitchen-Sink’s templating sections. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/haskell.html"&gt;&lt;span class="hashtag" data-hashtag="haskell"&gt;#haskell&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-hs.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-hs"&gt;#tramaj-hs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v0.3.0.0"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-hs&lt;/code&gt; 0.3.0.0&lt;span class="changelog-date"&gt;date not recorded&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Implements v2 of the language: a rewrite of the semantic core, not an extension of it.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-changed"&gt;changed&lt;/span&gt; Documents are expressions: `Element` and `Fragment` are ordinary `Expr` constructors; the separate template-phase AST and `JsonProgram` are gone.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; `Tramaj.Node`, the normative interchange format, with an encoder and a strict decoder.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; `Tramaj.Analysis`: static import names, action keys, context holes, context reads and unsupplied params, with deep variants.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-changed"&gt;changed&lt;/span&gt; Imports take their parameters three ways (expression, `ctx(path)`, or later call); `partial-import` is gone.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-changed"&gt;changed&lt;/span&gt; `a &amp;lt;&amp;gt; b` concatenation, lazy `branch`, `adapt-actions`, `.(a, b)` fragments, string escapes, object shorthand, builtins as values.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-fixed"&gt;fixed&lt;/span&gt; Malformed `action(...)`/`value(...)` in an element argument no longer backtracks into a meaningless `Call`; a backtick in a static string position is reported.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section class="changelog-version" id="v0.2.0.0"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-hs&lt;/code&gt; 0.2.0.0&lt;span class="changelog-date"&gt;date not recorded&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Adds a JSON-producing mode for hosts that want the data half of the language on its own.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; `Tramaj.Ast.JsonProgram`, `Tramaj.Parser.parseJsonProgram` and `Tramaj.Eval.evalJsonProgram`.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;section class="changelog-version" id="v0.1.0.0"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-hs&lt;/code&gt; 0.1.0.0&lt;span class="changelog-date"&gt;date not recorded&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Initial release, extracted from the repository it was written in.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Ports the PureScript package&amp;#39;s `Tramaj.Ast` / `Tramaj.Parser` / `Tramaj.Eval` to megaparsec + aeson.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj-hs.html" rel="alternate"/><summary type="text">Release history of `tramaj-hs`, the Haskell implementation, also the engine behind Kitchen-Sink's templating sections.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj-rs.html</id><title type="text">Changelog: tramaj-rs</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-rs"&gt;Changelog: tramaj-rs&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-rs&lt;/code&gt;, the Rust implementation of the parser, evaluator and analyses. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/rust.html"&gt;&lt;span class="hashtag" data-hashtag="rust"&gt;#rust&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-rs.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-rs"&gt;#tramaj-rs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v0.1.0"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-rs&lt;/code&gt; 0.1.0&lt;span class="changelog-date"&gt;unreleased&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Hand-written port kept in agreement with the other implementations through the shared conformance corpus.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Parser, AST, evaluator and static analyses, plus a program-card analysis.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Serde-based Node JSON codec.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Object destructuring patterns in `@` bindings and lambda parameters (decisions section 17): a parser lowering to plain `Let`/`Lambda`, checked by shared corpus cases 100-118.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj-rs.html" rel="alternate"/><summary type="text">Release history of `tramaj-rs`, the Rust implementation of the parser, evaluator and analyses.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj-py.html</id><title type="text">Changelog: tramaj-py</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-py"&gt;Changelog: tramaj-py&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-py&lt;/code&gt;, the Python implementation, standard library only. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/python.html"&gt;&lt;span class="hashtag" data-hashtag="python"&gt;#python&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-py.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-py"&gt;#tramaj-py&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v0.1.0"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-py&lt;/code&gt; 0.1.0&lt;span class="changelog-date"&gt;unreleased&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Python port: parser, evaluator (concrete and symbolic modes), Node JSON codec, static analyses and a python -m tramaj CLI. Standard library only, Python 3.9+.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Hand-written port checked against `corpus/cases` by `tests/test_corpus.py`: every case passes in both modes.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Object destructuring patterns in `@` bindings and lambda parameters (decisions section 17): a parser lowering to plain `Let`/`Lambda`, checked by shared corpus cases 100-118.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj-py.html" rel="alternate"/><summary type="text">Release history of `tramaj-py`, the Python implementation, standard library only.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj-react.html</id><title type="text">Changelog: tramaj-react</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-react"&gt;Changelog: tramaj-react&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-react&lt;/code&gt;, the React host: folds a tramaj-js Node into React elements. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/javascript.html"&gt;&lt;span class="hashtag" data-hashtag="javascript"&gt;#javascript&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-react.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-react"&gt;#tramaj-react&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v0.1.0"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-react&lt;/code&gt; 0.1.0&lt;span class="changelog-date"&gt;unreleased&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;The React counterpart of tramaj-halogen, and the only package here that depends on react.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; `foldToReact` and `validateAttrNames`.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj-react.html" rel="alternate"/><summary type="text">Release history of `tramaj-react`, the React host: folds a tramaj-js Node into React elements.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj-js.html</id><title type="text">Changelog: tramaj-js</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-js"&gt;Changelog: tramaj-js&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-js&lt;/code&gt;, the TypeScript implementation for Node.js and browsers. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/javascript.html"&gt;&lt;span class="hashtag" data-hashtag="javascript"&gt;#javascript&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-js.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-js"&gt;#tramaj-js&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v0.1.0"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-js&lt;/code&gt; 0.1.0&lt;span class="changelog-date"&gt;unreleased&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;TypeScript port: parser, evaluator (concrete and symbolic modes), Node JSON codec and static analyses.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Hand-written port checked against `corpus/cases` by `test/corpus.test.ts`.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Object destructuring patterns in `@` bindings and lambda parameters (decisions section 17): a parser lowering to plain `Let`/`Lambda`, checked by shared corpus cases 100-118.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj-js.html" rel="alternate"/><summary type="text">Release history of `tramaj-js`, the TypeScript implementation for Node.js and browsers.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj-cli.html</id><title type="text">Changelog: tramaj-cli</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-cli"&gt;Changelog: tramaj-cli&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-cli&lt;/code&gt;, the Node.js command-line interface built on the PureScript package. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/purescript.html"&gt;&lt;span class="hashtag" data-hashtag="purescript"&gt;#purescript&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-cli.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-cli"&gt;#tramaj-cli&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v(none)"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-cli&lt;/code&gt; (none)&lt;span class="changelog-date"&gt;unreleased&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Bundled with spago; no registry release yet.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Template file + JSON context file to evaluated Node JSON on stdout, with `--lib name=file` libraries and `--watch`.&lt;/li&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; `analyze imports|actions|holes|unsupplied|constraints` subcommands over the static analyses.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj-cli.html" rel="alternate"/><summary type="text">Release history of `tramaj-cli`, the Node.js command-line interface built on the PureScript package.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/changelog-tramaj-cli-rs.html</id><title type="text">Changelog: tramaj-cli-rs</title><updated>2026-09-25T08:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="changelog-tramaj-cli-rs"&gt;Changelog: tramaj-cli-rs&lt;/h2&gt;
&lt;p&gt;Release history of &lt;code&gt;tramaj-cli-rs&lt;/code&gt;, the standalone Rust command-line interface over tramaj-rs. Releases happen when there are
major changes rather than on a schedule; each entry below is one version.
Every implementation has its own page like this one, and each page is
tagged so the site builds one Atom feed per implementation:
&lt;a href="/tramaj/hashtags/changelog.html"&gt;&lt;span class="hashtag" data-hashtag="changelog"&gt;#changelog&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/rust.html"&gt;&lt;span class="hashtag" data-hashtag="rust"&gt;#rust&lt;/span&gt;&lt;/a&gt; &lt;a href="/tramaj/hashtags/tramaj-cli-rs.html"&gt;&lt;span class="hashtag" data-hashtag="tramaj-cli-rs"&gt;#tramaj-cli-rs&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;&lt;section class="main-section"&gt;&lt;div class="changelog"&gt;&lt;section class="changelog-version" id="v0.1.0"&gt;&lt;h2 class="changelog-heading"&gt;&lt;code&gt;tramaj-cli-rs&lt;/code&gt; 0.1.0&lt;span class="changelog-date"&gt;unreleased&lt;/span&gt;&lt;/h2&gt;&lt;p class="changelog-summary"&gt;Rust port of tramaj-cli&amp;#39;s Main.purs over the tramaj-rs crate.&lt;/p&gt;&lt;ul class="changelog-changes"&gt;&lt;li&gt;&lt;span class="changelog-kind changelog-kind-added"&gt;added&lt;/span&gt; Template + JSON context evaluation and the `analyze` subcommands, as a single static binary.&lt;/li&gt;&lt;/ul&gt;&lt;/section&gt;&lt;/div&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/changelog-tramaj-cli-rs.html" rel="alternate"/><summary type="text">Release history of `tramaj-cli-rs`, the standalone Rust command-line interface over tramaj-rs.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/what-sets-tramaj-apart.html</id><title type="text">What sets tramaj apart</title><updated>2026-09-11T01:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="what-sets-tramaj-apart"&gt;What sets tramaj apart&lt;/h2&gt;
&lt;p&gt;Plenty of languages already do pieces of what tramaj does. JSX makes
documents a value inside an expression language. Dhall and Nickel are
typed, functional, importable configuration languages. Jsonnet generates
structured data from a small functional core. CUE builds a schema out of
constraints. Starlark restricts a language deliberately so tooling can
reason about it. None of that is new by itself.&lt;/p&gt;
&lt;p&gt;What’s distinctive about tramaj is a specific combination: &lt;strong&gt;a document is
an ordinary value in a small functional language, and the language is
restricted in exactly the places needed to make a program’s external
interface — what it imports, what context it reads, what actions it can
emit — recoverable by walking the AST, without evaluating it.&lt;/strong&gt; Most
template systems optimize for how easily they generate output. tramaj
optimizes for how much a host can know about a program before it runs.&lt;/p&gt;
&lt;h3 id="a-document-is-just-a-value"&gt;A document is just a value&lt;/h3&gt;
&lt;p&gt;There’s one expression grammar. A document (an &lt;code&gt;Element&lt;/code&gt; or &lt;code&gt;Fragment&lt;/code&gt;) is
a &lt;code&gt;Value&lt;/code&gt;, exactly like a string or a number — it can be bound, passed to a
lambda, returned from a function, collected by &lt;code&gt;map&lt;/code&gt;, or handed to
&lt;code&gt;import&lt;/code&gt;. There’s no separate template phase sitting on top of a host
language, the way JSX sits on top of JavaScript.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@row = import(&amp;quot;row&amp;quot;, {kind: ctx(row-kind)})
.ul(
  map($ctx.items, (item) =&amp;gt; $row({&amp;quot;title&amp;quot;: $item.title}).rendered))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The array &lt;code&gt;map&lt;/code&gt; returns is already a sequence of document children — “build
a document” and “compute a value” are the same activity. See
&lt;a href="/tramaj/reference.html"&gt;Reference&lt;/a&gt; for the full grammar.&lt;/p&gt;
&lt;h3 id="ctx-is-a-declared-hole-not-just-a-read"&gt;&lt;code&gt;ctx(...)&lt;/code&gt; is a declared hole, not just a read&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;$ctx.title&lt;/code&gt; and &lt;code&gt;ctx(title)&lt;/code&gt; are equivalent at runtime. They are not
equivalent to static analysis. &lt;code&gt;$ctx.title&lt;/code&gt; is a dynamic read that could, in
principle, be buried behind arbitrary computation. &lt;code&gt;ctx(title)&lt;/code&gt; is a literal
path in the syntax — the author is declaring, in a form a tool can see
without running anything, “this is a hole the caller is expected to fill.”&lt;/p&gt;
&lt;p&gt;That costs something: a &lt;code&gt;ctx(...)&lt;/code&gt; path can’t be computed. It has to be
written down. tramaj takes that trade deliberately, because it’s what turns
“what does this template depend on” into a question a static pass can
answer over an unmodified module — not just the top-level program, but a
library sitting behind an &lt;code&gt;import&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="imports-accumulate-parameters-before-they-run"&gt;Imports accumulate parameters before they run&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@panel = import(&amp;quot;panel&amp;quot;, {})
@half = $panel({&amp;quot;name&amp;quot;: &amp;quot;web&amp;quot;})
$half({&amp;quot;replicas&amp;quot;: 2}).rendered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An import isn’t “load this module” — it’s a partially applied program.
Each call supplies more parameters; nothing executes until &lt;code&gt;.rendered&lt;/code&gt; or
&lt;code&gt;.vals&lt;/code&gt; is actually read. That gives a wired-once, specialized-per-item
pattern for free:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@row = import(&amp;quot;row&amp;quot;, {kind: ctx(row-kind)})
map($ctx.items, (item) =&amp;gt; $row({&amp;quot;title&amp;quot;: $item.title}).rendered)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;code&gt;contextReads&lt;/code&gt; and &lt;code&gt;unsuppliedParams&lt;/code&gt; are static analyses (see
&lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt;), a host can diff what a library &lt;em&gt;expects&lt;/em&gt;
against what an import &lt;em&gt;supplies&lt;/em&gt; — &lt;code&gt;title&lt;/code&gt; supplied, &lt;code&gt;description&lt;/code&gt; and
&lt;code&gt;id&lt;/code&gt; still open — without running the library. That’s close to an implicit
interface for modules, without introducing a conventional module type
system.&lt;/p&gt;
&lt;h3 id="actions-are-a-statically-discoverable-vocabulary-not-an-escape-hatch"&gt;Actions are a statically discoverable vocabulary, not an escape hatch&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;action(&amp;quot;on-click&amp;quot;, &amp;quot;deploy&amp;quot;, {&amp;quot;id&amp;quot;: $ctx.id})&lt;/code&gt; attaches an opaque,
structured action to a node. The event and the key are literal strings; only
the payload can be computed. &lt;code&gt;adapt-actions(node, prefix(&amp;quot;deployment:&amp;quot;))&lt;/code&gt;
can rewrite a subtree’s action keys under a fixed prefix, and the optional
transform hook is allowed to touch the event and payload but never the key.&lt;/p&gt;
&lt;p&gt;That restriction exists for one reason: so &lt;code&gt;staticActionKeys(program)&lt;/code&gt; stays
answerable by walking the AST. A host embedding an untrusted or
model-generated template can ask “what could this possibly do to my page or
my system” — the full set of action keys it might ever emit — before a
single line of it runs. See &lt;a href="/tramaj/use-cases.html"&gt;Use cases&lt;/a&gt; for what that
buys in practice, and &lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt; for the determinism
and portability guarantees underneath it.&lt;/p&gt;
&lt;h3 id="the-shape-this-adds-up-to"&gt;The shape this adds up to&lt;/h3&gt;
&lt;p&gt;Put the three together and a tramaj program has a discoverable interface
even though it’s an executable program:&lt;/p&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/static-interface.dot.png" alt="The interface of a program, recovered by walking its AST: context holes, imports and unsupplied params, action keys, all before any evaluation" /&gt;&lt;/p&gt;
&lt;p&gt;None of documents-as-values, static analysis, or restricted languages is
new on its own — JSX, Dhall, CUE, and Starlark each stake out real territory
here, and tramaj doesn’t compete with Dhall’s type system or CUE’s
unification. What’s specific to tramaj is treating the combination —
&lt;em&gt;document composition&lt;/em&gt; + &lt;em&gt;deferred, parameterized imports&lt;/em&gt; + &lt;em&gt;a statically
recoverable dependency and action vocabulary&lt;/em&gt; — as the actual point,
instead of a byproduct of “yet another way to generate markup.” That’s why
&lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt; calls out cheap static analysis as an
invariant, not a feature: it’s the thing the rest of the design exists to
protect.&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/what-sets-tramaj-apart.html" rel="alternate"/><summary type="text">Not a new templating syntax — a document language whose imports, context reads, and emitted actions stay inspectable without running the program.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/howto-agents.html</id><title type="text">tramaj for coding agents</title><updated>2026-09-10T01:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="tramaj-for-coding-agents"&gt;tramaj for coding agents&lt;/h2&gt;
&lt;p&gt;tramaj was designed to be as easy for a model to emit correctly as for a
person to write by hand — a small builtin set, one expression grammar, and
no template/expression phase split to keep track of. This page is a dense
cheatsheet plus a fetch strategy for getting the full grammar when the
summary below isn’t enough.&lt;/p&gt;
&lt;h3 id="fetch-the-canonical-source-first"&gt;Fetch the canonical source first&lt;/h3&gt;
&lt;p&gt;Don’t rely on prose alone for anything you’re about to emit — pull the
normative text:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every page on this site is also served as plain text at
&lt;code&gt;/text/&amp;lt;page-name&amp;gt;.cmark.text&lt;/code&gt; (e.g. &lt;code&gt;/text/reference-language.cmark.text&lt;/code&gt;)
— no HTML to strip.
&lt;/li&gt;
&lt;li&gt;The specs themselves are the ultimate source of truth, on GitHub:
&lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/reference.md"&gt;&lt;code&gt;reference.md&lt;/code&gt;&lt;/a&gt;,
&lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/node-json.md"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt;,
&lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/v3-symbols.md"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;,
&lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/v4-types.md"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt;.
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lucasdicioccio/tramaj/tree/main/corpus/cases"&gt;&lt;code&gt;corpus/cases/&lt;/code&gt;&lt;/a&gt;
in the repository is a conformance corpus: each case is a
&lt;code&gt;template.tramaj&lt;/code&gt; + &lt;code&gt;ctx.json&lt;/code&gt; + &lt;code&gt;expected.json&lt;/code&gt; triple (plus optional
&lt;code&gt;libs/*.tramaj&lt;/code&gt;) that both implementations must satisfy — treat these as
runnable, ground-truth examples rather than prose.
&lt;/li&gt;
&lt;li&gt;See also &lt;a href="/tramaj/raw/llms.txt"&gt;&lt;code&gt;llms.txt&lt;/code&gt;&lt;/a&gt; for a short machine-readable index of
the above (Kitchen-Sink serves arbitrary &lt;code&gt;.txt&lt;/code&gt; files under &lt;code&gt;/raw/&lt;/code&gt;, so
this isn’t at the conventional &lt;code&gt;/llms.txt&lt;/code&gt; root path).
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="leaders--what-each-sigil-means"&gt;Leaders — what each sigil means&lt;/h3&gt;
&lt;table&gt;
&lt;tr&gt;&lt;th&gt;Leader&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;.&lt;/code&gt;&lt;/td&gt;&lt;td&gt;document: &lt;code&gt;.tag(...)&lt;/code&gt; element, or &lt;code&gt;.(...)&lt;/code&gt; fragment&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;&lt;td&gt;read a binding or path&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;@&lt;/code&gt;&lt;/td&gt;&lt;td&gt;define a binding (&lt;code&gt;@name=expr&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;!&lt;/code&gt;&lt;/td&gt;&lt;td&gt;emit a constraint, statement position (&lt;code&gt;!constraint(name, ...)&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;&lt;td&gt;symbolic allocation/demand, v3 (&lt;code&gt;?(key)&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%&lt;/code&gt;&lt;/td&gt;&lt;td&gt;type hole, v4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--&lt;/code&gt;&lt;/td&gt;&lt;td&gt;line comment, to end of line&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;
&lt;h3 id="shape-of-a-program"&gt;Shape of a program&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;@n=cardinality($ctx.items)
.p(&amp;quot;there are `$n` item(s)&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An optional block of &lt;code&gt;@name=expr&lt;/code&gt; bindings, evaluated once each in order,
followed by one root expression. Backtick-quoted &lt;code&gt;`$expr`&lt;/code&gt; interpolates
into a string; a bare &lt;code&gt;$path&lt;/code&gt; can be a child directly. &lt;code&gt;a &amp;lt;&amp;gt; b&lt;/code&gt; concatenates
strings, arrays, and objects.&lt;/p&gt;
&lt;h3 id="documents-are-ordinary-values"&gt;Documents are ordinary values&lt;/h3&gt;
&lt;p&gt;There is no separate “template value” type — an element or fragment can be
bound, passed to a function, and returned from one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@kids=.(.p(&amp;quot;one&amp;quot;), .p(&amp;quot;two&amp;quot;))
@panel=(title, children) =&amp;gt; .section(.h2($title), $children)

.main($panel(&amp;quot;Deployment&amp;quot;, $kids))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="actions-are-opaque-to-the-language"&gt;Actions are opaque to the language&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;action(&amp;quot;on-click&amp;quot;, &amp;quot;select&amp;quot;, {&amp;quot;id&amp;quot;: $i.id})&lt;/code&gt; attaches structured data to an
element. The language does not interpret it — a host decides what the key
means. Action keys and adaptation prefixes must be literal strings, never
computed, which is what keeps “what actions can this template emit”
statically answerable.&lt;/p&gt;
&lt;h3 id="imports"&gt;Imports&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;import(...)&lt;/code&gt; pulls in another program as a library. Parameters can arrive
as an expression, as a &lt;code&gt;ctx(path)&lt;/code&gt; hole (a static marker that the &lt;em&gt;importing&lt;/em&gt;
program’s own context gets read at that point), or be omitted and supplied
later by calling the import. Imports are lazy — nothing runs until
&lt;code&gt;.rendered&lt;/code&gt; or &lt;code&gt;.vals&lt;/code&gt; is read on the result.&lt;/p&gt;
&lt;h3 id="v3--symbols-and-constraints-frozen-implemented"&gt;v3 — symbols and constraints (frozen, implemented)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;?(key)&lt;/code&gt; allocates an opaque symbol; only the root program may do this
(libraries raise &lt;code&gt;AllocationInLibrary&lt;/code&gt;). &lt;code&gt;!constraint(name, ...)&lt;/code&gt; emits a
constraint. Running in &lt;code&gt;Symbolic&lt;/code&gt; mode (a host choice, not something the
template declares) wraps the result in a &lt;code&gt;tramaj/symbolic/1&lt;/code&gt; envelope of
root value, symbols, and constraints instead of a finished document — use
this when the actual values should come from a solver rather than be
supplied up front. Full grammar: &lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="v4--types-draft-implemented"&gt;v4 — types (draft, implemented)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;@x : T = e&lt;/code&gt; desugars to &lt;code&gt;@x=e&lt;/code&gt; plus a &lt;code&gt;has-type&lt;/code&gt; constraint carrying &lt;code&gt;T&lt;/code&gt;’s
canonical id. Types are resolved and erased before evaluation — tramaj fixes
type &lt;em&gt;identity&lt;/em&gt; (canonical ids, string equality), a separate host checker
decides whether values actually satisfy it. Type parameters thread through
imports. Full grammar: &lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="static-analyses-available-without-evaluating"&gt;Static analyses available without evaluating&lt;/h3&gt;
&lt;p&gt;Because imports, action keys, adaptation prefixes, and &lt;code&gt;ctx(...)&lt;/code&gt; paths are
all literal strings in the syntax, these can all be answered by walking the
AST — no context, no evaluation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what a program imports, and with what parameters
&lt;/li&gt;
&lt;li&gt;what actions it can possibly emit
&lt;/li&gt;
&lt;li&gt;what context paths it reads (&lt;code&gt;ctx(...)&lt;/code&gt; holes)
&lt;/li&gt;
&lt;li&gt;which import parameters are still unsupplied
&lt;/li&gt;
&lt;li&gt;(v3/v4) what constraints and symbols/types it introduces
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/agent-loop.dot.png" alt="Self-check loop for an agent: draft, run analyze, compare the interface with what was asked, revise or hand over" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tramaj-cli&lt;/code&gt;’s &lt;code&gt;analyze&lt;/code&gt; subcommand exposes these directly — see
&lt;a href="/tramaj/howto-humans.html"&gt;Getting started&lt;/a&gt; for exact invocations. If you’re
generating a template and want to
self-check it before handing it to a host, running &lt;code&gt;analyze&lt;/code&gt; on it is
cheaper and more reliable than re-deriving these properties by re-reading
your own output.&lt;/p&gt;
&lt;h3 id="common-pitfalls"&gt;Common pitfalls&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Don’t compute an import name, action key, or &lt;code&gt;ctx(...)&lt;/code&gt; path from an
expression — these must be literal strings; the grammar doesn’t allow
otherwise.
&lt;/li&gt;
&lt;li&gt;Bindings are visible only &lt;em&gt;after&lt;/em&gt; their declaration, in order — not
mutually recursive.
&lt;/li&gt;
&lt;li&gt;A library (something loaded via &lt;code&gt;import&lt;/code&gt;) may not call &lt;code&gt;?(key)&lt;/code&gt; — only the
root program may allocate symbols.
&lt;/li&gt;
&lt;li&gt;Evaluation order is unconstrained outside of binding precedence: don’t
write templates that rely on side-effect ordering, because there isn’t
one — evaluation is meant to be referentially transparent.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/howto-agents.html" rel="alternate"/><summary type="text">A dense syntax reference and fetch strategy for an LLM or agent emitting tramaj templates.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/playground.html</id><title type="text">tramaj playground</title><updated>2026-09-10T01:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;p&gt;This is the same playground built from the
&lt;a href="https://github.com/lucasdicioccio/tramaj"&gt;tramaj&lt;/a&gt; repository’s PureScript
sources, published here as a static bundle so it can be tried without a
local build. It parses and evaluates tramaj templates in the browser —
nothing is sent to a server. Tabs act as importable libraries for each
other; toggle “Symbolic mode” to see the v3 envelope instead of a finished
document.&lt;/p&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/playground-flow.dot.png" alt="Editor tabs and a context are parsed and evaluated in the browser; Symbolic mode decides between a finished document and the v3 envelope" /&gt;&lt;/p&gt;
&lt;p&gt;See &lt;a href="/tramaj/howto-humans.html"&gt;Getting started&lt;/a&gt; for running the same language
from the command line, or &lt;a href="/tramaj/use-cases.html"&gt;Use cases&lt;/a&gt; for where it fits.&lt;/p&gt;
&lt;iframe class="playground-frame" src="/tramaj/tramaj-playground.html" title="tramaj playground"&gt;&lt;/iframe&gt;
&lt;p&gt;If the embedded frame does not load, open the
&lt;a href="/tramaj/tramaj-playground.html"&gt;playground&lt;/a&gt; directly.&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/playground.html" rel="alternate"/><summary type="text">An in-browser playground for tramaj — edit a template and a JSON context, see it evaluated live.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/howto-humans.html</id><title type="text">Getting started with tramaj</title><updated>2026-09-10T01:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="getting-started-with-tramaj"&gt;Getting started with tramaj&lt;/h2&gt;
&lt;p&gt;This is a practical walkthrough. For the language’s core ideas, see the
overview on the &lt;a href="/tramaj/"&gt;home page&lt;/a&gt;; for the full grammar, see the
&lt;a href="/tramaj/reference.html"&gt;Reference&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A template can be run in four places, and each section below covers one:&lt;/p&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/template-workflow.dot.png" alt="The same template can go to the playground, to tramaj-cli evaluate, to tramaj-cli analyze (no context needed), or into an embedding host" /&gt;&lt;/p&gt;
&lt;h3 id="the-fastest-way-in-the-playground"&gt;The fastest way in: the playground&lt;/h3&gt;
&lt;p&gt;Before installing anything, try the &lt;a href="/tramaj/playground.html"&gt;playground&lt;/a&gt; — it runs
entirely in the browser, needs no setup, and shows the same panels described
below (evaluated AST, rendered HTML, action log).&lt;/p&gt;
&lt;h3 id="installing-the-toolchain"&gt;Installing the toolchain&lt;/h3&gt;
&lt;p&gt;tramaj’s PureScript packages are one &lt;a href="https://github.com/purescript/spago"&gt;Spago&lt;/a&gt;
workspace. You need &lt;code&gt;purs&lt;/code&gt; and &lt;code&gt;spago&lt;/code&gt; on your &lt;code&gt;PATH&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;npm&lt;/span&gt; install &lt;span class="at"&gt;-g&lt;/span&gt; purescript spago&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Anything that bundles a browser or Node bundle — the playground and the CLI —
also needs &lt;code&gt;esbuild&lt;/code&gt;, which is a devDependency of the repo root:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;git&lt;/span&gt; clone https://github.com/lucasdicioccio/tramaj.git&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="bu"&gt;cd&lt;/span&gt; tramaj&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;npm&lt;/span&gt; install&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="bu"&gt;export&lt;/span&gt; &lt;span class="va"&gt;PATH&lt;/span&gt;&lt;span class="op"&gt;=&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;span class="va"&gt;$PWD&lt;/span&gt;&lt;span class="st"&gt;/node_modules/.bin:&lt;/span&gt;&lt;span class="va"&gt;$PATH&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3 id="writing-a-first-template"&gt;Writing a first template&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;hello.tramaj&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@n=cardinality($ctx.items)
.p(&amp;quot;there are `$n` item(s)&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and a context to evaluate it against, &lt;code&gt;hello.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="fu"&gt;{&lt;/span&gt;&lt;span class="dt"&gt;&amp;quot;items&amp;quot;&lt;/span&gt;&lt;span class="fu"&gt;:&lt;/span&gt; &lt;span class="ot"&gt;[&lt;/span&gt;&lt;span class="st"&gt;&amp;quot;a&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;b&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;,&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;c&amp;quot;&lt;/span&gt;&lt;span class="ot"&gt;]&lt;/span&gt;&lt;span class="fu"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;h3 id="running-it"&gt;Running it&lt;/h3&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;spago&lt;/span&gt; run &lt;span class="at"&gt;-p&lt;/span&gt; tramaj-cli &lt;span class="at"&gt;--args&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;hello.tramaj hello.json&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;This prints the evaluated &lt;code&gt;Node&lt;/code&gt; AST as JSON — a &lt;code&gt;p&lt;/code&gt; element wrapping the
text &lt;code&gt;there are 3 item(s)&lt;/code&gt;. See &lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt;
for what that JSON shape means, or fold it into real HTML by rendering it
through a host like &lt;code&gt;tramaj-halogen&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="splitting-a-template-into-libraries"&gt;Splitting a template into libraries&lt;/h3&gt;
&lt;p&gt;A template can &lt;code&gt;import(...)&lt;/code&gt; another template as a library. Pass each one
with &lt;code&gt;--lib name=path&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;spago&lt;/span&gt; run &lt;span class="at"&gt;-p&lt;/span&gt; tramaj-cli &lt;span class="at"&gt;--args&lt;/span&gt; &lt;span class="st"&gt;&amp;quot;--lib greeter=greeter.tramaj hello.tramaj hello.json&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Repeat &lt;code&gt;--lib&lt;/code&gt; for as many libraries as the template’s imports need.&lt;/p&gt;
&lt;h3 id="inspecting-a-template-without-running-it"&gt;Inspecting a template without running it&lt;/h3&gt;
&lt;p&gt;Because imports, action keys, and context reads are all static positions
(see &lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt;), the CLI can answer questions about a
template without a context file at all:&lt;/p&gt;
&lt;div class="code code--highlighted"&gt;&lt;div class="sourceCode"&gt;&lt;pre class="sourceCode"&gt;&lt;code class="sourceCode"&gt;&lt;span id="1"&gt;&lt;a href="#1" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;node&lt;/span&gt; tramaj-cli/dist/tramaj-cli.js analyze imports hello.tramaj&lt;/span&gt;
&lt;span id="2"&gt;&lt;a href="#2" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;node&lt;/span&gt; tramaj-cli/dist/tramaj-cli.js analyze actions hello.tramaj&lt;/span&gt;
&lt;span id="3"&gt;&lt;a href="#3" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;node&lt;/span&gt; tramaj-cli/dist/tramaj-cli.js analyze holes hello.tramaj&lt;/span&gt;
&lt;span id="4"&gt;&lt;a href="#4" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;node&lt;/span&gt; tramaj-cli/dist/tramaj-cli.js analyze unsupplied hello.tramaj&lt;/span&gt;
&lt;span id="5"&gt;&lt;a href="#5" aria-hidden="true" tabindex="-1"&gt;&lt;/a&gt;&lt;span class="ex"&gt;node&lt;/span&gt; tramaj-cli/dist/tramaj-cli.js analyze all hello.tramaj&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;(These need a bundled CLI first: &lt;code&gt;spago bundle -p tramaj-cli --platform node --outfile dist/tramaj-cli.js&lt;/code&gt;, run from &lt;code&gt;tramaj-cli/&lt;/code&gt;.)&lt;/p&gt;
&lt;h3 id="embedding-in-a-real-host"&gt;Embedding in a real host&lt;/h3&gt;
&lt;p&gt;Running templates from the command line is one thing; wiring them into an
actual application is another — see
&lt;a href="/tramaj/integrate-tramaj.html"&gt;Integrating tramaj&lt;/a&gt; for the four paths (PureScript,
Haskell, JavaScript, or any other language) and what each one actually
looks like.&lt;/p&gt;
&lt;h3 id="where-to-go-next"&gt;Where to go next&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="/tramaj/use-cases.html"&gt;Use cases&lt;/a&gt; for where this fits alongside other
templating approaches.
&lt;/li&gt;
&lt;li&gt;&lt;a href="/tramaj/reference.html"&gt;Reference&lt;/a&gt; for the full grammar, builtins, and the v3/v4
extensions.
&lt;/li&gt;
&lt;li&gt;&lt;a href="/tramaj/integrate-tramaj.html"&gt;Integrating tramaj&lt;/a&gt; for embedding it in a host
application.
&lt;/li&gt;
&lt;li&gt;&lt;a href="/tramaj/howto-agents.html"&gt;For coding agents&lt;/a&gt; if you’re setting an LLM loose on
tramaj templates rather than writing them by hand.
&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/howto-humans.html" rel="alternate"/><summary type="text">Write a first template, run it, and see where to go from there.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/reference.html</id><title type="text">Reference</title><updated>2026-09-10T01:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="reference"&gt;Reference&lt;/h2&gt;
&lt;p&gt;These pages are generated straight from the specs in the
&lt;a href="https://github.com/lucasdicioccio/tramaj/tree/main/specs"&gt;tramaj repository&lt;/a&gt;
— the repository is always the canonical source, and may be ahead of what’s
published here.&lt;/p&gt;
&lt;p&gt;How the documents relate: the language reference is the base, the Node JSON
format encodes what it evaluates to, and v3 and v4 each extend it.&lt;/p&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/spec-layers.dot.png" alt="Spec layers: Node JSON encodes the core’s output; v3 symbols (value realm) and v4 types (type realm) both extend the core" /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="/tramaj/reference-language.html"&gt;&lt;strong&gt;Language reference&lt;/strong&gt;&lt;/a&gt; — core AST, values,
evaluation rules, surface syntax and its desugarings, imports, actions,
builtins, and what’s implementation-defined. Start here.
&lt;/li&gt;
&lt;li&gt;&lt;a href="/tramaj/reference-node-json.html"&gt;&lt;strong&gt;Node JSON representation&lt;/strong&gt;&lt;/a&gt; — the normative
wire format for the evaluated &lt;code&gt;Node&lt;/code&gt; AST.
&lt;/li&gt;
&lt;li&gt;&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;strong&gt;v3 — Symbolic values and constraints&lt;/strong&gt;&lt;/a&gt; —
&lt;code&gt;?(key)&lt;/code&gt; allocation, &lt;code&gt;!constraint(name, ...)&lt;/code&gt;, and the &lt;code&gt;Symbolic&lt;/code&gt;
evaluation mode.
&lt;/li&gt;
&lt;li&gt;&lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;strong&gt;v4 — Types&lt;/strong&gt;&lt;/a&gt; — the nominal type algebra,
type parameters through imports, and typed annotations.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two more documents exist only in the repository, not mirrored here since
they’re development history rather than language reference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/laws.md"&gt;&lt;code&gt;laws.md&lt;/code&gt;&lt;/a&gt;
— the design invariants, rewritten in prose on the
&lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt; page.
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/lucasdicioccio/tramaj/blob/main/specs/decisions.md"&gt;&lt;code&gt;decisions.md&lt;/code&gt;&lt;/a&gt;
— the resolved-conflicts log: which reading won wherever the design drafts
disagreed, and why.
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Earlier, superseded design drafts live under
&lt;a href="https://github.com/lucasdicioccio/tramaj/tree/main/specs/archive"&gt;&lt;code&gt;specs/archive/&lt;/code&gt;&lt;/a&gt;,
kept for the reasoning in them rather than as current documentation.&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/reference.html" rel="alternate"/><summary type="text">The tramaj specification: language reference, wire format, and the shipped v3/v4 extensions.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/properties.html</id><title type="text">Properties of tramaj</title><updated>2026-09-10T01:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="properties-of-tramaj"&gt;Properties of tramaj&lt;/h2&gt;
&lt;p&gt;tramaj is small on purpose. A handful of invariants are what make that
smallness pay off — they’re what let a host embed the language without
trusting it, and what let an LLM emit templates without a feedback loop to
check them against.&lt;/p&gt;
&lt;h3 id="small-core"&gt;Small core&lt;/h3&gt;
&lt;p&gt;The whole language is one expression grammar: bindings, HAML-like elements,
&lt;code&gt;jq&lt;/code&gt;-flavoured paths and calls, and a deliberately tiny builtin set. There is
no separate template phase and no macro system — an element or a fragment is
an ordinary value, so composing documents is just composing expressions.
Nothing about the identity of the language depends on HTML, a browser, or any
particular UI framework: the core (&lt;code&gt;tramaj/&lt;/code&gt;) has no DOM dependency at all,
and folding a &lt;code&gt;Node&lt;/code&gt; into Halogen HTML, JSON, or anything else is entirely
the host’s decision.&lt;/p&gt;
&lt;h3 id="portable-at-the-boundary"&gt;Portable at the boundary&lt;/h3&gt;
&lt;p&gt;Evaluation produces a generic &lt;code&gt;Node&lt;/code&gt; AST with a normative JSON encoding
(&lt;a href="/tramaj/reference-node-json.html"&gt;&lt;code&gt;node-json.md&lt;/code&gt;&lt;/a&gt;): scalars stay scalars, and an
element may carry many opaque, structured actions rather than being tied to
one host’s event model. That boundary is what lets five independent
implementations — PureScript, Haskell, Rust, TypeScript and Python — target
the same grammar and share a conformance corpus, and it’s what lets a host system
receive a rendered document as data rather than as a blob of host-specific
markup.&lt;/p&gt;
&lt;h3 id="cheap-to-analyze-statically"&gt;Cheap to analyze statically&lt;/h3&gt;
&lt;p&gt;Import names, action keys, action-adaptation prefixes, and &lt;code&gt;ctx(path)&lt;/code&gt; reads
are all literal strings in the syntax — never the result of evaluating an
expression. That’s a deliberate restriction, not an oversight: it means a
tool can answer “what does this program import,” “what actions can it
emit,” “what context does it read,” and “which import parameters are still
unsupplied” by walking the AST, with no evaluation step and no need to run
untrusted input to find out what it does.&lt;/p&gt;
&lt;h3 id="deterministic"&gt;Deterministic&lt;/h3&gt;
&lt;p&gt;Templates are referentially transparent — the same bindings evaluated
against the same context values always produce the same result. Evaluation
order is unconstrained outside of binding precedence, so a host is free to
parallelize or reorder work, but never free to change the answer. This is
what makes a rendered document reproducible, cacheable, and safe to diff.&lt;/p&gt;
&lt;h3 id="where-the-properties-sit"&gt;Where the properties sit&lt;/h3&gt;
&lt;p&gt;Determinism lives in the evaluator, portability in the &lt;code&gt;Node&lt;/code&gt; JSON boundary
after it, and everything host-specific (rendering, what an action key does)
after that boundary, in code the host owns.&lt;/p&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/node-boundary.dot.png" alt="The evaluator is deterministic, Node JSON is the portable boundary, and folds and action handlers belong to the host" /&gt;&lt;/p&gt;
&lt;h3 id="what-this-buys-you-in-practice"&gt;What this buys you in practice&lt;/h3&gt;
&lt;p&gt;Put together, these properties mean a host can accept a tramaj template from
an untrusted or automated source — a CMS, a design tool, an LLM — and know,
without running it, what it can possibly do to the page or app it’s
embedded in. See &lt;a href="/tramaj/use-cases.html"&gt;Use cases&lt;/a&gt; for what that looks like in
practice, or the &lt;a href="/tramaj/reference.html"&gt;Reference&lt;/a&gt; for the full specification
these properties are drawn from.&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/properties.html" rel="alternate"/><summary type="text">The invariants tramaj is held to: portability, cheap static analysis, and determinism.</summary></entry><entry><id>https://lucasdicioccio.github.io/tramaj/use-cases.html</id><title type="text">What tramaj is for</title><updated>2026-09-10T01:00:00Z</updated><author><name>Lucas DiCioccio</name></author><content type="html">&lt;div class="main-article"&gt;&lt;section class="main-section"&gt;&lt;h2 id="what-tramaj-is-for"&gt;What tramaj is for&lt;/h2&gt;
&lt;p&gt;tramaj is a small functional language for composing values and documents. A
program evaluates against a JSON-like context and produces either an
ordinary value or a generic document tree — the host decides what to do with
it. That shape shows up in a few recurring situations.&lt;/p&gt;
&lt;h3 id="turning-json-into-markup-safely"&gt;Turning JSON into markup, safely&lt;/h3&gt;
&lt;p&gt;The original motivation: templates that are comfortable to write by hand
&lt;em&gt;and&lt;/em&gt; easy for an LLM to emit. A &lt;code&gt;jq&lt;/code&gt;-flavoured expression half handles the
data, a HAML-like block syntax handles structure, and the builtin set stays
small enough that neither a person nor a model has to hold much in their
head to use it correctly. This site itself is one instance of that — &lt;a href="/tramaj/reference.html"&gt;static
site pre-processing, the way Kitchen-Sink itself does it&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="templating-a-real-ui-not-just-markup"&gt;Templating a real UI, not just markup&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tramaj-halogen&lt;/code&gt; folds an evaluated &lt;code&gt;Node&lt;/code&gt; into &lt;code&gt;Halogen.HTML&lt;/code&gt;, wiring each
&lt;code&gt;action(...)&lt;/code&gt; in the template to the host’s own &lt;code&gt;Action&lt;/code&gt; type. Because
actions are opaque structured data rather than host-specific event handlers,
a template can describe &lt;em&gt;what&lt;/em&gt; should happen (“on-click, select, id: 42”)
without knowing &lt;em&gt;how&lt;/em&gt; the host will react to it — the same template could
drive a different UI framework by pairing it with a different fold.&lt;/p&gt;
&lt;h3 id="documents-an-llm-can-emit-and-a-host-can-trust"&gt;Documents an LLM can emit and a host can trust&lt;/h3&gt;
&lt;p&gt;&lt;img src="/tramaj/gen/images/untrusted-template.dot.png" alt="A template from a model or third party is analyzed statically, checked against host policy, and only then evaluated and folded into the UI" /&gt;&lt;/p&gt;
&lt;p&gt;Because imports, action keys, and context reads are all static positions —
literal strings, never computed — a host can run the static analyses in
&lt;a href="/tramaj/reference-language.html"&gt;&lt;code&gt;Tramaj.Analysis&lt;/code&gt;&lt;/a&gt; over a model-generated template
&lt;em&gt;before&lt;/em&gt; evaluating it: what does it import, what actions can it possibly
emit, what context does it read, which import parameters are still
unsupplied. That turns “an LLM wrote this template” from a trust problem
into an inspectable one. See &lt;a href="/tramaj/properties.html"&gt;Properties&lt;/a&gt; for why this
holds.&lt;/p&gt;
&lt;h3 id="constraint-driven-generation"&gt;Constraint-driven generation&lt;/h3&gt;
&lt;p&gt;v3 adds symbolic values (&lt;code&gt;?(key)&lt;/code&gt;) and constraints (&lt;code&gt;!constraint(name, ...)&lt;/code&gt;). Running evaluation in &lt;code&gt;Symbolic&lt;/code&gt; mode produces an envelope of root
value, symbols, and constraints instead of a finished document — useful when
the actual values should come from a solver, a form the user hasn’t filled
in yet, or a search process, rather than being supplied up front. See
&lt;a href="/tramaj/reference-v3-symbols.html"&gt;&lt;code&gt;v3-symbols.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="typed-template-contracts"&gt;Typed template contracts&lt;/h3&gt;
&lt;p&gt;v4 adds a nominal type algebra on top of that: type declarations, type
parameters threaded through imports, and &lt;code&gt;@x : T = e&lt;/code&gt; annotations that are
resolved and erased before evaluation. tramaj fixes what a type &lt;em&gt;is&lt;/em&gt;
(identity is structural string equality over canonical ids); a separate host
checker decides whether the values flowing through a template actually
satisfy it. That’s useful wherever a template is a contract between two
teams or two systems — e.g. a component library whose consumers should be
checked against its declared shape. See &lt;a href="/tramaj/reference-v4-types.html"&gt;&lt;code&gt;v4-types.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="try-it"&gt;Try it&lt;/h3&gt;
&lt;p&gt;The &lt;a href="/tramaj/playground.html"&gt;playground&lt;/a&gt; runs all of the above in the browser —
tabs-as-importable-libraries, live AST/render panels, an action log, and a
symbolic-mode toggle — with nothing sent to a server.&lt;/p&gt;
&lt;/section&gt;&lt;/div&gt;</content><link href="https://lucasdicioccio.github.io/tramaj/use-cases.html" rel="alternate"/><summary type="text">A small functional language for composing values and documents — where it fits.</summary></entry></feed>