Properties of tramaj

On Thu, 10 Sep 2026, by @lucasdicioccio, 440 words, 0 code snippets, 3 links, 1images.

Properties of tramaj

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.

Small core

The whole language is one expression grammar: bindings, HAML-like elements, jq-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 (tramaj/) has no DOM dependency at all, and folding a Node into Halogen HTML, JSON, or anything else is entirely the host’s decision.

Portable at the boundary

Evaluation produces a generic Node AST with a normative JSON encoding (node-json.md): 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.

Cheap to analyze statically

Import names, action keys, action-adaptation prefixes, and ctx(path) 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.

Deterministic

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.

Where the properties sit

Determinism lives in the evaluator, portability in the Node JSON boundary after it, and everything host-specific (rendering, what an action key does) after that boundary, in code the host owns.

The evaluator is deterministic, Node JSON is the portable boundary, and folds and action handlers belong to the host

What this buys you in practice

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 Use cases for what that looks like in practice, or the Reference for the full specification these properties are drawn from.