Documentation menu

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: rootIndex identifies the root even if the layout changes in a later version. Don't assume that nodes.length - 1 is 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 1 and false to 0.
  • Integer: convert a true or false value to 1 or 0, 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 subtract 1 from 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.