The expression syntax tree
Description: Implement an evaluator for exported conditions and instructions.
Narratyr parses each condition and instruction while you edit it. It stores the result as an abstract syntax tree instead of relying on the source string at runtime. An engine evaluates the expression by visiting the tree and returning the root value (no parser needed).
Use this reference when building a runtime for another engine or a tool that reads expression behavior. Expressions documents the language for people who create graphs.
The language uses 16 node kinds with global variable names and straight-line expressions. A complete evaluator can remain small and the source code of the engine runtimes can be used as a reference.
Nested and flat formats
Narratyr stores the same tree in two formats.
| Format | Location | Structure |
|---|---|---|
| Nested | Project .ngraph files in conditionAst or instructionAst |
Objects contain child objects and use a type field |
| Flat | Exported engine data in conditionAstFlat or instructionsFlat |
An array contains nodes that refer to child indices and use a kind field |
Tools that read projects usually use the nested format. Engine exports use the flat format so a runtime can load the tree without creating more objects or parsing the source expression.
Flat format
A flattened tree uses one object:
{
"rootIndex": 4,
"nodes": [
{ "kind": "Identifier", "name": "Gold", "varId": "v_abc" },
{ "kind": "NumberLiteral", "number": 10 },
{ "kind": "BinaryExpression", "operator": ">=", "left": 0, "right": 1 },
{ "kind": "Identifier", "name": "HasMap" },
{ "kind": "LogicalExpression", "operator": "&&", "left": 2, "right": 3 }
]
}
This tree represents Gold >= 10 && HasMap. A parent refers to each child by its integer index
in nodes. Evaluation starts at nodes[rootIndex] and follows those indices.
The format provides these guarantees:
- Post-order layout: a child appears before its parent, and the root appears last. A runtime can evaluate the array in one forward pass.
- Explicit root:
rootIndexidentifies the root even if the layout changes in a later version. Don't assume thatnodes.length - 1is the root.
Each node includes fields used by its kind and omits unused fields instead of assigning null.
Node kinds
The left, right, and children fields contain integer indices into nodes. They don't
contain nested objects.
Values
kind |
Fields | Result |
|---|---|---|
NumberLiteral |
number |
Number |
StringLiteral |
string |
Text |
BoolLiteral |
bool |
True or false |
NullLiteral |
none | Empty value |
UndefinedLiteral |
none | Empty value |
ArrayLiteral |
children with element indices |
List of evaluated elements |
Identifier |
name, optional varId |
Current variable value |
EnumReference |
enumName, entryKey |
Integer value for the option set entry |
The parser keeps NullLiteral and UndefinedLiteral separate because they come from different
source words. Engine runtime implementations convert both kinds to the same empty value.
An Identifier uses name for variable lookup. When the editor resolves the name during
parsing, varId contains the stable internal ID. Tools can use this ID to track a variable
across a rename, while runtime implementations can ignore it.
An EnumReference stays symbolic during export. This lets the evaluator find and return its
integer value without parsing expressions again after an entry changes. A list of option set
entries becomes a list of integers.
Operations
kind |
Fields | Behavior |
|---|---|---|
UnaryExpression |
operator (!, -), left for operand |
Negate one value |
BinaryExpression |
operator, left, right |
Apply a binary operator |
LogicalExpression |
operator, left, right |
Apply short-circuiting and or logic |
CallExpression |
left for callee, children for arguments |
Call a function |
IndexExpression |
left for list, right for index |
Read a list element |
BinaryExpression supports *, /, %, +, -, <, >, <=, >=, ==, !=, in,
and not in. LogicalExpression supports && and ||. Logical operators use a separate kind
because they short-circuit.
The parser normalizes the word forms and, or, and not to &&, ||, and !.
The meaning of left depends on the node kind. It identifies the left operand of a binary
expression, the operand of a UnaryExpression, or the callee of a function call.
Statements
kind |
Fields | Behavior |
|---|---|---|
StatementBlock |
children with statement indices |
Run statements in order |
Assignment |
operator, left for target, right for value |
Apply :=, +=, -=, *=, or /= |
UpdateExpression |
operator (++, --), left for target |
Add or subtract one |
Only instruction trees use these node kinds, and StatementBlock forms the root. An assignment
or update target always refers to an Identifier. The parser rejects other targets, so the
evaluator can read nodes[node.left].name directly.
A bare expression can also appear as a statement. This usually represents a CallExpression
for its side effect. Evaluate it and discard its result.
Expression evaluation
Start at the root and evaluate each node according to its kind.
Variables
An Identifier reads a variable by name. Report an evaluation error when the variable store
doesn't contain that name. Returning an empty value could hide a spelling mistake and select the
wrong graph branch.
True and false conversion
The caller converts the root value of a condition to true or false. Empty values, zero, empty
text, and false convert to false. Other values convert to true. This rule lets a bare
traversed("...") call act as a condition.
Short-circuit logic
For &&, evaluate the left operand first. Return it when it converts to false. Otherwise,
return the result of evaluating the right operand. For ||, return the left operand when it
converts to true. Otherwise, return the result of evaluating the right operand.
Both operators return an operand value without converting it to a true or false value. Don't evaluate the right operand when the left side determines the result because the right side might call a game function with side effects.
Comparisons
Compare values with matching types without conversion. Numbers compare with numbers, and text uses alphabetical ordering.
Equality supports mixed types. == returns false and != returns true when the types differ.
Ordering operators report an evaluation error for different types.
Arithmetic
The + operator joins values as text when either operand contains text. Otherwise, it adds
numbers. The -, *, /, and % operators require numbers, as does the prefix - operator.
Division or modulo by zero reports an evaluation error.
Keep an integer result when both operands are integers and the operation produces an integer. This matches the Narratyr engine plugins and prevents counts from becoming decimal values.
Membership
The in and not in operators require a list on the right. Compare the left operand with each
list element by value. Report an error for another right operand type.
Indexing
IndexExpression requires a list and an integer index. An index outside the list, including a
negative index, returns an empty value. This behavior lets conditions read a short list without
a separate length check.
Function calls
The callee of CallExpression always refers to an Identifier. Read its name, evaluate
arguments from left to right, and dispatch the call. The runtime handles built-ins and sends
other calls to the game function host.
Statement block execution
Run the children of the root StatementBlock in order and stop at the first error.
Stage variable writes before committing them. Each assignment stores its result in a temporary map. Reads within the block check that map before the main variable store. Commit the map after the complete block succeeds.
Later statements see earlier writes, so Gold += 5; Gold += 5 adds ten. If an error occurs,
the main variable store remains unchanged.
Assignment type conversion
Before writing a value, convert it to the variable's declared type. This prevents an assignment
such as MyFlag := "hello" from storing text in a true or false variable.
Apply only conversions defined by the language:
- True or false: use the condition conversion rules. The text
"false"converts to true because it isn't empty. - Number: convert true to
1and false to0. - Integer: convert a true or false value to
1or0, and round a decimal value. - Text: convert the value with the same rules as text concatenation.
Because the language doesn't parse text as numbers or convert between lists and individual values, report an evaluation error for those conversions.
Writing an undeclared variable also reports an error. Expressions have no var declaration, so
assignment can't create variables.
Assignment operators
:=stores the value.+=and-=add or subtract numbers. For lists, they append or remove the last matching element. The variable store removes duplicates when writing a set.*=and/=work with numbers only.++and--add or subtract1from the target.
Runtime function dispatch
A CallExpression dispatches to either a built-in or a game function. The exporter resolves
macros before runtime.
Built-ins
The runtime implements length, count, and indexOf for lists. It also implements
randomInteger, randomNumber, debugLog, traversed, choiceTakenCount,
lastChoiceTakenCount, questStatus, and objectiveStatus. See
Expressions for return values.
Traversal and status functions read current play state. Pass the traversal counts and most
recent choice ID into the evaluation context. While checking a choice's hide or disable
condition, also pass that choice ID for choiceTakenCount(). Calling choiceTakenCount()
without a current choice ID reports an error.
Game functions
Calls matching document function definitions go to the game function host. Send the name and evaluated arguments, then return the host result. Report an error for an unknown name.
Macros
The exporter resolves names beginning with #, such as
#questIdFromName("The Heist"), and replaces each call with its resulting string literal. An
exported flat tree doesn't contain macro calls, so the runtime doesn't implement them.
A tool that reads a project .ngraph file can encounter a macro as a normal CallExpression.
Its callee name starts with #, and its argument is a string literal.
Nested format
The nested format contains child objects directly and uses type instead of kind. These
fields differ from the flat format:
| Node | Nested (type) |
Flat (kind) |
|---|---|---|
| True or false | BooleanLiteral, payload value |
BoolLiteral, payload bool |
| Numbers or text | value |
number or string |
| Array literal | elements |
children |
UnaryExpression |
operand |
left |
| Call | callee, arguments |
left, children |
| Index | object, index |
left, right |
| Assignment | target, value |
left, right |
| Update | argument |
left |
| Statement block | statements |
children |
BinaryExpression, LogicalExpression, Identifier, and EnumReference use the same fields
in both formats.
This nested object represents Gold >= 10 && HasMap:
{
"type": "LogicalExpression",
"operator": "&&",
"left": {
"type": "BinaryExpression",
"operator": ">=",
"left": { "type": "Identifier", "name": "Gold", "varId": "v_abc" },
"right": { "type": "NumberLiteral", "value": 10 }
},
"right": { "type": "Identifier", "name": "HasMap" }
}
Exported instructions. An exported Instruction Node contains flat instructionsFlat and
nested instructions. The nested copy supports debugging and can still contain macro calls.
Evaluate instructionsFlat, which contains the resolved literals.
Export fields
Exported graphs store trees for expressions in these locations:
| Element | Field |
|---|---|
| Connection between nodes | conditionAstFlat |
| Gate node | conditionAstFlat |
| Event Listener node | conditionAstFlat |
| Choice slot | hideConditionAstFlat, disableConditionAstFlat |
| Instruction node | instructionsFlat |
Each tree appears beside its source string in condition or instructionText. The source helps
with debugging and editor display. Evaluate the tree because it contains the parsed,
authoritative form.
Elements without a condition omit the field. A connection or choice slot without a condition
is unconditional. A Gate without a condition uses requiredVariables, which requires all
listed values to be true. An Event Listener without a condition never fires.
Next
Expressions defines the source language. Exporting explains how to produce engine data that contains these trees.