Reference
Grammar
The lexical structure and the productions — the shape of a .trl at the token level, the
sigils and operators it spends, and an EBNF you can read top to bottom. For what the words mean,
see the vocabulary glossary; for the AST behind them, the
architecture map.
This reference is derived from the implementation — the lexer and the standard corpus. The canonical grammar lives with the core; where the two ever disagree, the core wins. Every snippet on this page is validated through the same engine the editor runs.
Lexical structure
A source file is a stream of these tokens. Whitespace separates them and is otherwise insignificant.
| Token | Form | What it is |
|---|---|---|
| identifier | vale · Grifter · the_switch | a name — letters, digits, _; can't start with a digit |
| role var | $s · $target | a bound role / slot, used in rules and parametric facts |
| number | 7 · -3 · 0.5 | a numeric literal — stat values and thresholds |
| string | "the appraisal" | an inline label on an edge, or an import path |
| prose | """…""" | a raw prose node — inert, parses & round-trips, never evaluated |
| comment | // … | to end of line |
Comments are inert to parsing with one convention on top: the leading // block
is read as metadata and surfaced next to a recognition. A // @name value line
is a tag (repeat a name → a list), the non-tag prose is a reserved description, and
@laconic is the one-line summary. Only the leading block counts, and it never affects matching.
See the style guide.
Sigils
A tag opens with exactly one leading sigil — the character that decides how the tag reads. Three families:
| Family | Sigils | Meaning |
|---|---|---|
| deployment | + - ~ ? ! | how a trope is used: straight · subverted · deconstructed · averted · invoked |
| facet | = # % @ & | which dimension a name denotes: Body · Mind · Essence · Rel · Verb |
| stat | ^ | a numeric stat: [^HP(cur=7, max=7)], or a threshold [^HP <= 0] in a when: |
A tag may also carry one trailing role mark inside the bracket — a
recognition-ranking signal, distinct from the leading tier markers. [=Sign!] marks a facet
necessary: if a matching story leaves it unmatched, the candidate is gated below the
report floor (so a trope whose distinctive anchor is absent drops out rather than coasting on
scaffolding). [+Smoker?] marks a presence-only bonus: it lifts confidence
when present and is ignored when absent (supporting evidence that should never penalize its own absence).
Both are opt-in, local to the tag, and inert to eval.
Edge operators
An edge ties two names. The operator names the tie; a leading ! negates it.
| Operator | Tie | Negated |
|---|---|---|
-- | associates / knows / relates | !-- |
-> | causes / leads to / targets | !> |
> | possesses / holds / carries | — |
>< | mutual conflict | — |
@ | located-at / relation | !@ |
~~ | co-present — same identity at two story-times (a self-encounter) | — |
~~ is the time-axis sibling of !=: != asserts different
identities, ~~ the same identity at different story-times. Don't confuse the chron
clock's ~> (precedence, "then in story-time") with causal -> ("therefore").
Keywords
The reserved words, grouped by what they introduce.
- entities
char·set·obj·evt·arc·work— the node kinds (actor, place/group, object, event, thread, and the work-as-artifact: the medium-bearing container — the film, novel, game).conceptdeclares a lowercase foundational node.- vocabulary
verb·prop·rel·attr·state·stat— the kinds you declare in a corpus, each minting a tag name on a facet.- scopes
arc·act·scene·beat— the nested temporal containers; reading order is work-time (the order shown).- story-time
chron·before·after— declare the second clock (story-time: when an event happens in-world) and query its order. Its operators are@@(story-time anchor),~>(precedence), and~~(co-presence). See the two clocks.- rules
rule·when·then— a recognition pattern and its consequent.- patterns
not·count·foreach·where·unique·past·future— quantify and constrain awhen:clause.- ontology
imply·assoc·exclusive— relations between tags: one expands into many; names cluster; a partition's members exclude each other (at most one holds per entity;exclusive Name!for exactly-one). A braced axis on a+tag —[+Child{Axis}]— adds a taxonomy (is-a) edge recognition expands; it carries no keyword of its own (engine ≥ 0.32.0).- corpus analysis
derive·metric— a computed header field from a formula, and a corpus-wide reduction (sum·count·avg·min·max) over declared fields. Read-only, inert to recognition and eval.- modules
import— pull in another.trlby path.- layering
extends·redacts— accretive corpus layering: add a rule to (extends "stem") or withdraw one from (redacts RuleName) an existing trope by its stem, inheriting its header metadata — an accretion, not a whole-file fork. Engine ≥ 0.30.0.- prose
dialog— attach diegetic prose or spoken lines to an event / edge (kind=dialog·narration·monologue·exposition·aside).- queries & agency
surface·surface_global·fork·resolve·retcon·prompt— addressing and rewriting the log; see the glossary.- literals
true·false— boolean parameter values (e.g. aunique:rule modifier, or a tag param).
By example
Nodes carry tags; edges and parametric facts wire them together:
char vale [+Grifter] [#Suspicious]
obj ledger
vale > ledger
vale -- renn : "the appraisal"
evt the_switch [&Deceives(agent=vale, target=renn)] A corpus declares the vocabulary other files spend — and the implications between tags:
concept ambition [+Abstract]
verb Strikes(target) [+Interpersonal]
rel Models(other: char)
imply Grifter -> [Deceptive, Cunning]
A braced axis on a + tag declares a taxonomy edge — an is-a specialization
recognition expands, so a chart that only says [+Swordsman] satisfies an anchor gated on
[+Warrior]. It is a DAG (multiple parents allowed) and stays out of eval and
drams:
concept Archetype
prop Warrior
prop Swordsman [+Warrior{Archetype}]
char kael [+Swordsman] Typed & optional signature params
A verb / rel / prop signature constrains how call-sites bind its
arguments. Each param sits in one of three registers:
- Untyped — a bare
findingaccepts anything, nothing checked. - Typed —
course: concept [+Course]is checked (the value must be a[+Course]concept — thevalidate --corpuslint) — but required: every event that uses the verb must fill it. - Optional-typed (
?:) —course?: concept [+Course]resolves the tension: typed, so it's checked when present; optional, so a course-less call still matches.
concept acute [+Course]
verb Onsets(subject: char, course?: concept [+Course], finding)
char pt
scene chart {
beat 1 { evt e [&Onsets(subject=pt, finding=Fever)] } // course omitted — matches
beat 2 { evt f [&Onsets(subject=pt, course=acute)] } // course present — checked
}
Under the hood p?: T is sugar for the union p: T | null — the | null
member is the "…or absent" marker that exempts the param from the otherwise-required role. You write the
friendly ?:; a formatter re-serializes it to the | null form (same meaning, same
AST). It's new syntax, so a corpus that uses ?: needs engine ≥ 0.15.0;
corpora that don't are unaffected. A tag name may also carry (aka …) alias forms.
Scopes are the timeline. A beat is the finest unit; nesting is reading order:
scene the_appraisal {
beat 1 {
char vale [+Grifter]
char renn [+Mark]
vale -- renn : "introductions"
}
}
A scope can also occupy a slot by carrying a presence edge in its head —
scene the_stinger @ post_credits { … } — placing it on a work's
structural skeleton. See the style guide on work skeletons.
A second clock — story-time — can run under the presentation. A chron spine
orders events as they happen in-world; @@ stamps a scope (or a single reference) with its
story-time. Here the scenes are shown trial-first but happen heist-first — a flashback:
chron heist ~> trial // story-time: the heist, then the trial
char vale [+Protagonist]
char da
scene the_trial @@ trial { // shown first…
beat 1 { da -- vale : "lays out the case" }
}
scene the_heist @@ heist { // …but happened first — a flashback
beat 1 { vale -- da : "picks the mark" }
} before(a, b) / after(a, b) query that clock (three-valued — yes, no, or
incomparable), the story-time duals of work-time's past / future. See
the two clocks for the whole model.
For a work that is chronological by genre — a chart, a log, a case file — the spine is boilerplate:
@@ auto is a top-level directive that derives the clock from presentation order,
so before / after read the beat sequence with no chron to write.
It's opt-in and exclusive: mixing it with any explicit chron / @@ / ~~
is a validation error, and auto is reserved as a marker name.
A rule matches a shape in the graph and asserts what it implies:
rule Establish_Ethos {
when:
char $s [+Speaker] [+Credible]
then:
$s [+Authoritative]
}
A when: facet can carry a trailing role mark to steer ranking — ! makes it
necessary (its absence gates the match), ? a presence-only bonus
(lifts when present, neutral when absent). Reach for a necessary mark on the single distinctive anchor; the
style guide has the discipline.
verb Presents(subject: char)
rule Klinefelter {
when:
char $p [+Patient] [=KlinefelterSign!] [+Smoker?] // ! necessary · ? presence-bonus
evt $e [&Presents(subject=$p)]
then:
$p [+Recognized]
}
Two corpus-analysis declarations sit outside recognition. derive computes a header field from
a formula (a curated literal still wins over it); metric reduces declared fields corpus-wide.
Both are read-only and inert to matching:
derive prevalence: number = 40 + 2 * 5
metric total = sum(prevalence where prevalence > 0) Grammar (EBNF)
A pragmatic EBNF — { } is repetition, [ ] optional, |
alternation, (* … *) a comment. It covers the syntax; the
glossary and guides carry the semantics.
(* ─── Lexical ─── *)
identifier = (letter | "_") { letter | digit | "_" } ;
role-var = "$" identifier ; (* a bound role / slot: $x *)
number = ["-"] digit {digit} ["." digit {digit}] ;
boolean = "true" | "false" ; (* a parameter value / rule modifier *)
string = '"' { character } '"' ; (* an inline edge label *)
prose = '"""' { character } '"""' ; (* a raw prose node — inert, round-trips *)
comment = "//" { character } newline ;
(* ─── Document ─── *)
document = { statement } ;
statement = import | layering | declaration | fact | scope | rule | plane | prose ;
(* a fact may be wrapped to mark its truth-tier; unwrapped = asserted *)
fact = [ tier ] core-fact [ tier ] ;
tier = "!" | "*" | "?" ; (* !absolute! *contingent* ?latent? *)
core-fact = tagging | edge ;
(* ─── Names ─── *)
name = identifier | role-var | crossing ;
(* ─── Imports ─── *)
import = "import" string ; (* import "trl/concepts/universal.trl" *)
(* ─── Accretive layering (engine >= 0.30.0) ─── *)
layering = extends | redacts ;
extends = "extends" string ; (* this file's rules join the trope with this bare stem, *)
(* inheriting its header metadata — an accretion, not a fork *)
redacts = "redacts" identifier ; (* withdraw one named rule from the extended trope *)
(* ─── Declarations ─── *)
declaration = entity-decl | concept-decl | vocab-decl | ontology | exclusive-decl | derive-decl | metric-decl ;
entity-decl = entity-kw name { tag } ;
entity-kw = "char" | "set" | "obj" | "evt" | "arc" | "work" ;
concept-decl = "concept" identifier { tag } ; (* a lowercase foundational node *)
vocab-decl = vocab-kw identifier [ sig-params ] { tag } [ aka ] ;
vocab-kw = "verb" | "prop" | "rel" | "attr" | "state" | "stat" ;
sig-params = "(" sig-param { "," sig-param } ")" ;
sig-param = identifier [ ["?"] ":" param-type { tag } ] ; (* bare = untyped; "?:" = optional-typed *)
param-type = type { "|" type } ; (* a union; a "| null" member makes the slot optional. *)
(* "p?: T" is sugar for "p: T | null" (engine >= 0.15.0). *)
type = "char" | "set" | "obj" | "evt" | "arc" | "work"
| "entity" | "concept" | "value" | "node" | "null" ;
aka = "(" "aka" identifier { "," identifier } ")" ; (* alias forms for the tag name *)
ontology = ("imply" | "assoc") tagname "->" tag-list ;
exclusive-decl = "exclusive" tagname [ "!" ] "->" tag-list ; (* partition: at most one member; "!" = exactly-one — *)
(* eliminating members then derives the last (engine >= 0.27.0; derivation 0.33). *)
(* A rule may ANCHOR the label — [+Name!] in when: — to demand the partition be resolved. *)
tag-list = "[" tagname { "," tagname } "]" ;
(* ─── Derived header fields & metrics (corpus analysis; inert to recognition & eval) ─── *)
derive-decl = "derive" identifier ":" identifier "=" d-expr ; (* a computed header value *)
metric-decl = "metric" identifier "=" agg "(" [ d-expr ] [ "where" d-cmp { "and" d-cmp } ] ")" ;
agg = "sum" | "count" | "avg" | "min" | "max" ;
d-expr = d-term { ("+" | "-") d-term } ;
d-term = d-factor { "*" d-factor } ;
d-factor = number | identifier [ "(" d-expr { "," d-expr } ")" ] | "(" d-expr ")" ;
d-cmp = d-expr ("==" | "!=" | ">=" | "<=" | ">" | "<") d-expr ;
(* ─── Tags ─── *)
tagging = name tag { tag } ;
tag = "[" sigil tagname [ taxon ] [ "(" bindings ")" ] [ compare ] [ role-mark ] "]" ;
taxon = "{" identifier "}" ; (* a subsumption axis: [+Child{Axis}] on a "+" tag declares is-a — *)
(* recognition expands specific -> ancestor (a DAG; engine >= 0.32.0). *)
(* Structural vocabulary, never matched; inert to eval and drams. *)
role-mark = "!" | "?" ; (* a facet role inside the bracket (§5.5), at most one: "!" necessary — its *)
(* absence gates recognition · "?" presence-only bonus — lifts when present. *)
(* Recognition-ranking signals, inert to eval; distinct from the leading tier markers. *)
sigil = "+" | "-" | "~" | "?" | "!" (* deployment *)
| "=" | "#" | "%" | "@" | "&" (* facet *)
| "^" ; (* stat *)
tagname = identifier ;
bindings = binding { "," binding } ;
binding = identifier "=" value ; (* agent=vale, to=$k, max=7 *)
value = name | number | string | boolean ;
compare = ("<" | "<=" | ">" | ">=" | "==" | "!=") number ; (* a ^stat threshold *)
(* ─── Edges ─── *)
edge = name edge-op name [ ":" string ] ;
edge-op = "--" | "->" | ">" | "><" | "@" (* associate · cause · hold · mutual · locate *)
| "~~" (* co-presence: same identity, two story-times *)
| "!--" | "!>" | "!@" ; (* a leading "!" negates: the tie does NOT hold *)
(* ─── Scopes — the temporal log ─── *)
scope = scope-kw scope-name { tag } presence? [ "@@" marker ] block ;
scope-kw = "arc" | "act" | "scene" | "beat" ;
scope-name = identifier | number ; (* scene the_heist · beat 1 *)
block = "{" { statement } "}" ;
presence = "@" name ; (* the slot the scope occupies — sugars to a leading name @ slot edge inside the block *)
(* the optional "@@" marker stamps the scope's active story-time (S23) *)
(* ─── Planes — diegetic levels ─── *)
plane = "<<" plane-path [ ":" plane-attrs ] ">>" ;
crossing = "<<" plane-path "|" name ">>" ; (* reach into another plane *)
plane-path = identifier { "." identifier } ;
plane-attrs = plane-attr { "," plane-attr } ;
plane-attr = identifier [ "=" value ] ; (* non_canon · teller=gm *)
(* ─── Story-time — the second clock (fabula vs syuzhet) ─── *)
chron-decl = "chron" chron-term { chron-op chron-term } ;
chron-op = "~>" | "~~" ; (* precedence (the clock) · co-presence *)
chron-term = marker [ tag ] [ ":" string ] ; (* marker; optional [+Loop]; optional note *)
marker = identifier ; (* a timeline point; declared by first use here *)
story-dir = "@@" ( marker | "auto" ) ; (* ambient directive — active story-time (file-order); *)
(* "@@ auto": derive the clock from presentation order, *)
(* top-level only; "auto" is a reserved marker name (§16) *)
anchor = (name | role-var) "@@" marker ; (* postfix; displace one reference; paren-free *)
(* a scope head may also carry "@@" marker *)
story-order = ("before" | "after") "(" name "," name ")" ; (* a when:-condition; three-valued *)
(* ─── Rules — recognition patterns ─── *)
rule = "rule" identifier "{" { rule-mod } "when:" { pattern } "then:" { core-fact } "}" ;
rule-mod = "unique:" boolean ; (* a rule-level modifier, before when: *)
pattern = [ quantifier ] core-fact [ "where" condition ]
| identity (* a binding-identity constraint between two role-vars *)
| story-order ; (* before(a, b) / after(a, b) — the story clock *)
quantifier = "not" | "count" | "past" | "future" ;
condition = value compare value ; (* a where-refinement / comparison test *)
identity = role-var ("==" | "!=" | "?=") role-var ;
(* == same entity · != distinct · ?= may-be-same, a no-op that only states the intent *)
(* so a coincidental collision isn't reported as ambiguous (engine >= 0.29.0). *)
(* then: also admits foreach · surface · fork · prompt · dialog — see the glossary *)
(* ─── Prose / dialog — an annotation on an evt decl, an edge, or a then: action ─── *)
dialog-anno = "dialog" "(" dialog-field { "," dialog-field } ")" ;
dialog-field = "kind" "=" dialog-kind | "src" "=" string | "text" "=" string | "inline" "=" prose ;
dialog-kind = "dialog" | "narration" | "monologue" | "exposition" | "aside" ; See also
- Architecture & anatomy
- The AST each form lowers to, and the nomenclature.
- Vocabulary glossary
- Every documented concept, prop, state, verb, relation, and attribute.
- Style guide
- How to organize a file: setup vs. the event log, naming, epistemic & temporal discipline.