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.

TokenFormWhat it is
identifiervale · Grifter · the_switcha name — letters, digits, _; can't start with a digit
role var$s · $targeta bound role / slot, used in rules and parametric facts
number7 · -3 · 0.5a 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:

FamilySigilsMeaning
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.

OperatorTieNegated
--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). concept declares 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 a when: 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 .trl by 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. a unique: 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:

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.