Formula Expression AST
Formula values are stored as a JSON-encoded Abstract Syntax Tree (AST). Each node in the tree represents an expression and is identified by the expressionType field, which acts as a discriminator for the node type. An interpreter recursively evaluates the tree starting from the root node.
Reference implementations exist for C#, TypeScript, and C++.
![digraph ExpressionAST {
rankdir=TB;
graph [fontname="Helvetica", fontsize=11, bgcolor="#f8f9fa", pad=0.4, nodesep=0.3, ranksep=0.5, splines=polyline];
node [fontname="Helvetica", fontsize=10, shape=box, style="filled,rounded", height=0.3, width=1.6, fixedsize=false];
edge [fontname="Helvetica", fontsize=8, arrowsize=0.6];
// Root union
FormulaExpressionObj [fillcolor="#4a90d9", fontcolor="white", color="#2c6fad", penwidth=2];
// Expression nodes
node [fillcolor="#c8e6c9", color="#388e3c"];
UnaryExpressionObj
BinaryExpressionObj
ConditionExpressionObj
ConstantExpressionObj
ConvertExpressionObj
DefaultExpressionObj
IndexExpressionObj
InvokeExpressionObj
LambdaExpressionObj
MemberExpressionObj
NewArrayBoundExpressionObj
NewArrayInitExpressionObj
ListInitExpressionObj
MemberInitExpressionObj
NewExpressionObj
TypeIsExpressionObj
TypeOfExpressionObj
// Collection / ref types
node [fillcolor="#ffe082", color="#f57f17"];
TypeReferenceObj
ArgumentCollectionObj
TypeArgumentCollectionObj
ElementInitCollectionObj
MemberBindingCollectionObj
// Binding types
node [fillcolor="#ce93d8", color="#6a1b9a"];
ElementInitBindingObj
MemberMemberBindingObj
MemberListBindingObj
MemberAssignmentBindingObj
// ── Union membership (dashed blue) ──
edge [style=dashed, arrowhead=onormal, color="#4a90d9"];
FormulaExpressionObj -> {
UnaryExpressionObj BinaryExpressionObj ConditionExpressionObj
ConstantExpressionObj ConvertExpressionObj DefaultExpressionObj
IndexExpressionObj InvokeExpressionObj LambdaExpressionObj
MemberExpressionObj NewArrayBoundExpressionObj NewArrayInitExpressionObj
ListInitExpressionObj MemberInitExpressionObj NewExpressionObj
TypeIsExpressionObj TypeOfExpressionObj
}
// ── Composition (solid dark) ──
edge [style=solid, arrowhead=vee, color="#333333"];
// → ArgumentCollectionObj
IndexExpressionObj -> ArgumentCollectionObj
InvokeExpressionObj -> ArgumentCollectionObj
LambdaExpressionObj -> ArgumentCollectionObj
NewArrayBoundExpressionObj -> ArgumentCollectionObj
NewArrayInitExpressionObj -> ArgumentCollectionObj
NewExpressionObj -> ArgumentCollectionObj
ElementInitBindingObj -> ArgumentCollectionObj
// → TypeArgumentCollectionObj
MemberExpressionObj -> TypeArgumentCollectionObj
TypeReferenceObj -> TypeArgumentCollectionObj
// → FormulaExpressionObj (back-refs, gray)
edge [color="#999999", style=dashed, arrowhead=vee];
ArgumentCollectionObj -> FormulaExpressionObj
MemberAssignmentBindingObj -> FormulaExpressionObj
// → TypeReferenceObj (back-refs)
TypeArgumentCollectionObj -> TypeReferenceObj
TypeReferenceObj -> TypeReferenceObj [label="self"]
// Compound nodes
edge [style=solid, color="#333333", arrowhead=vee];
ListInitExpressionObj -> NewExpressionObj
ListInitExpressionObj -> ElementInitCollectionObj
MemberInitExpressionObj -> NewExpressionObj
MemberInitExpressionObj -> MemberBindingCollectionObj
// Collections → binding types
edge [style=dashed, color="#999999", arrowhead=vee];
ElementInitCollectionObj -> ElementInitBindingObj
MemberBindingCollectionObj -> MemberMemberBindingObj
MemberBindingCollectionObj -> MemberListBindingObj
MemberBindingCollectionObj -> MemberAssignmentBindingObj
// Binding recursive refs
edge [style=solid, color="#333333", arrowhead=vee];
MemberMemberBindingObj -> MemberBindingCollectionObj
MemberListBindingObj -> ElementInitCollectionObj
}](../_images/graphviz-bb33483807172db16356982113df6334dc49c7ba.png)
export type FormulaExpressionObj = UnaryExpressionObj | BinaryExpressionObj | ConditionExpressionObj | ConstantExpressionObj |
ConvertExpressionObj | DefaultExpressionObj | IndexExpressionObj | InvokeExpressionObj | LambdaExpressionObj | MemberExpressionObj |
NewArrayBoundExpressionObj | NewArrayInitExpressionObj | ListInitExpressionObj | MemberInitExpressionObj | NewExpressionObj |
TypeIsExpressionObj | TypeOfExpressionObj;
export interface UnaryExpressionObj {
'expressionType': 'UncheckedScope' | 'CheckedScope' | 'Group' | 'UnaryPlus' | 'Negate' | 'NegateChecked' | 'Not' | 'Complement';
'expression': FormulaExpressionObj;
}
export interface BinaryExpressionObj {
'expressionType': 'Divide' | 'MultiplyChecked' | 'Multiply' | 'Power' | 'Modulo' | 'AddChecked' | 'Add' | 'SubtractChecked' | 'Subtract' | 'LeftShift' | 'RightShift' | 'GreaterThan' | 'GreaterThanOrEqual' | 'LessThan' | 'LessThanOrEqual' | 'Equal' | 'NotEqual' | 'And' | 'Or' | 'ExclusiveOr' | 'AndAlso' | 'OrElse' | 'Coalesce';
'left': FormulaExpressionObj;
'right': FormulaExpressionObj;
};
export interface ConditionExpressionObj {
'expressionType': 'Condition';
'test': FormulaExpressionObj;
'ifTrue': FormulaExpressionObj;
'ifFalse': FormulaExpressionObj;
}
export interface ConstantExpressionObj {
'expressionType': 'Constant';
'type': TypeReferenceObj | string;
'value': any;
};
export interface ConvertExpressionObj {
'expressionType': 'TypeAs' | 'Convert' | 'ConvertChecked';
'type': TypeReferenceObj | string;
'expression': FormulaExpressionObj;
}
export interface DefaultExpressionObj {
'expressionType': 'Default';
'type': TypeReferenceObj | string;
}
export interface IndexExpressionObj {
'expressionType': 'Index';
'expression': FormulaExpressionObj;
'arguments': ArgumentCollectionObj;
'useNullPropagation': boolean;
}
export interface InvokeExpressionObj {
'expressionType': 'Invoke';
'expression': FormulaExpressionObj;
'arguments': ArgumentCollectionObj;
}
export interface LambdaExpressionObj {
'expressionType': 'Lambda';
'arguments': ArgumentCollectionObj;
'expression': FormulaExpressionObj;
}
export interface MemberExpressionObj {
'expressionType': 'MemberResolve';
'name': string;
'arguments'?: TypeArgumentCollectionObj;
'expression'?: FormulaExpressionObj | null;
'useNullPropagation': boolean;
};
export interface NewArrayBoundExpressionObj {
'expressionType': 'NewArrayBounds';
'type': TypeReferenceObj | string;
'arguments': ArgumentCollectionObj;
};
export interface NewArrayInitExpressionObj {
'expressionType': 'NewArrayInit';
'type': TypeReferenceObj | string;
'initializers': ArgumentCollectionObj;
};
export interface ListInitExpressionObj {
'expressionType': 'ListInit';
'new': NewExpressionObj;
'initializers': ElementInitCollectionObj;
};
export interface MemberInitExpressionObj {
'expressionType': 'MemberInit';
'new': NewExpressionObj;
'bindings': MemberBindingCollectionObj;
};
export interface NewExpressionObj {
'expressionType': 'New';
'type': TypeReferenceObj | string;
'arguments': ArgumentCollectionObj;
}
export interface TypeIsExpressionObj {
'expressionType': 'TypeIs';
'type': TypeReferenceObj | string;
'expression': FormulaExpressionObj;
}
export interface TypeOfExpressionObj {
'expressionType': 'TypeOf';
'type': TypeReferenceObj | string;
}
export interface TypeReferenceObj {
'name': string;
'expressionType'?: 'MemberResolve';
'expression'?: TypeReferenceObj | string;
'arguments'?: TypeArgumentCollectionObj;
}
export interface ElementInitBindingObj {
'expressionType': 'ElementInitBinding';
'initializers': ArgumentCollectionObj;
}
export interface MemberMemberBindingObj {
'expressionType': 'MemberBinding';
'name': string;
'bindings': MemberBindingCollectionObj;
}
export interface MemberListBindingObj {
'expressionType': 'ListBinding';
'name': string;
'initializers': ElementInitCollectionObj;
}
export interface MemberAssignmentBindingObj {
'expressionType': 'AssignmentBinding';
'name': string;
'expression': FormulaExpressionObj;
}
export interface TypeArgumentCollectionObj {
[name: string]: TypeReferenceObj | string;
}
export interface ArgumentCollectionObj {
[name: string]: FormulaExpressionObj | null;
}
export interface ElementInitCollectionObj {
[name: string]: ElementInitBindingObj;
}
export interface MemberBindingCollectionObj {
[name: string]: MemberMemberBindingObj | MemberListBindingObj | MemberAssignmentBindingObj;
}
Structure
Every AST node is a JSON object with at least one field:
expressionType— a string that identifies the node type and determines which other fields are present.
Nodes that refer to types use either a plain string (e.g. "System.Int32") or a TypeReference object for generic or nested types.
Argument collections (ArgumentCollectionObj) are JSON objects where each key is either a zero-based positional index ("0", "1", …) or a named parameter, and each value is a child expression node (or null for omitted optional arguments).
TypeReference
A type reference describes a type, including generic types. It is used wherever a type is needed (e.g. Constant, Convert, New).
Fields
name(string) — The simple or fully-qualified type name.expressionType(string, optional) — When set to"MemberResolve", the type is resolved relative toexpression.expression(TypeReference or string, optional) — The containing namespace or type from whichnameis resolved.arguments(object, optional) — A map of generic type arguments, where keys are type parameter names and values are TypeReference objects or strings.
Example: List<int>
{
"name": "List",
"expression": { "expressionType": "MemberResolve", "name": "System.Collections.Generic" },
"expressionType": "MemberResolve",
"arguments": {
"T": "System.Int32"
}
}
Expression Types
Constant
Represents a literal value of a known type.
Fields
expressionType:"Constant"type— The type of the value (TypeReference or string).value— The literal value. The JSON type matches the declared type (number, string, boolean, null, etc.).
Example: the integer literal 42
{
"expressionType": "Constant",
"type": "System.Int32",
"value": 42
}
Example: the string literal "hello"
{
"expressionType": "Constant",
"type": "System.String",
"value": "hello"
}
MemberResolve
Resolves a named member (variable, property, field, method, or type) optionally relative to another expression. This is also used to reference formula parameters by name.
Fields
expressionType:"MemberResolve"name(string) — The member name to resolve.expression(FormulaExpression or null, optional) — The target object. Whennullor absent, the name is resolved in the current scope (e.g. a formula parameter or a known type).arguments(object, optional) — Generic type arguments for the member, keyed by type parameter name.useNullPropagation(boolean) — Whentrue, the access uses null-safe semantics (equivalent to?.in C#). Ifexpressionevaluates to null, the result is null instead of throwing.
Example: accessing target.HP
{
"expressionType": "MemberResolve",
"name": "HP",
"expression": {
"expressionType": "MemberResolve",
"name": "target",
"expression": null,
"useNullPropagation": false
},
"useNullPropagation": false
}
Example: null-safe access target?.HP
{
"expressionType": "MemberResolve",
"name": "HP",
"expression": {
"expressionType": "MemberResolve",
"name": "target",
"expression": null,
"useNullPropagation": false
},
"useNullPropagation": true
}
Unary
Applies a unary operator to a single operand.
Fields
expressionType— One of:"UnaryPlus"— identity (+x)"Negate"— arithmetic negation (-x)"NegateChecked"— arithmetic negation with overflow checking"Not"— logical NOT (!x)"Complement"— bitwise complement (~x)"Group"— grouping parentheses, no operation change ((x))"UncheckedScope"— disables overflow checking for the inner expression (unchecked(x))"CheckedScope"— enables overflow checking for the inner expression (checked(x))
expression(FormulaExpression) — The operand.
Example: -x
{
"expressionType": "Negate",
"expression": {
"expressionType": "MemberResolve",
"name": "x",
"expression": null,
"useNullPropagation": false
}
}
Binary
Applies a binary operator to two operands.
Fields
expressionType— One of:Arithmetic:
"Add","AddChecked","Subtract","SubtractChecked","Multiply","MultiplyChecked","Divide","Modulo","Power"Bitwise / Shift:
"And","Or","ExclusiveOr","LeftShift","RightShift"Comparison:
"Equal","NotEqual","LessThan","LessThanOrEqual","GreaterThan","GreaterThanOrEqual"Logical short-circuit:
"AndAlso"(&&),"OrElse"(||)Null coalescing:
"Coalesce"(??)
Variants with
Checkedsuffix perform overflow-checked arithmetic.left(FormulaExpression) — The left operand.right(FormulaExpression) — The right operand.
Example: weaponPower * targetResistance
{
"expressionType": "Multiply",
"left": {
"expressionType": "MemberResolve",
"name": "weaponPower",
"expression": null,
"useNullPropagation": false
},
"right": {
"expressionType": "MemberResolve",
"name": "targetResistance",
"expression": null,
"useNullPropagation": false
}
}
Condition
A conditional (ternary) expression: test ? ifTrue : ifFalse.
Fields
expressionType:"Condition"test(FormulaExpression) — The boolean condition.ifTrue(FormulaExpression) — Evaluated whentestis true.ifFalse(FormulaExpression) — Evaluated whentestis false.
Example: x > 0 ? x : -x
{
"expressionType": "Condition",
"test": {
"expressionType": "GreaterThan",
"left": { "expressionType": "MemberResolve", "name": "x", "expression": null, "useNullPropagation": false },
"right": { "expressionType": "Constant", "type": "System.Int32", "value": 0 }
},
"ifTrue": {
"expressionType": "MemberResolve",
"name": "x",
"expression": null,
"useNullPropagation": false
},
"ifFalse": {
"expressionType": "Negate",
"expression": { "expressionType": "MemberResolve", "name": "x", "expression": null, "useNullPropagation": false }
}
}
Convert / TypeAs
Converts or casts an expression to a different type.
Fields
expressionType— One of:"Convert"— explicit cast; throws on failure."ConvertChecked"— explicit cast with overflow checking."TypeAs"— safe cast; returns null if the object is not of the target type (equivalent toasin C#).
type— The target type (TypeReference or string).expression(FormulaExpression) — The expression to convert.
Example: (float)damage
{
"expressionType": "Convert",
"type": "System.Single",
"expression": {
"expressionType": "MemberResolve",
"name": "damage",
"expression": null,
"useNullPropagation": false
}
}
Default
Produces the default value for a type (e.g. 0 for numeric types, null for reference types).
Fields
expressionType:"Default"type— The type whose default value to produce (TypeReference or string).
Example: default(Int32)
{
"expressionType": "Default",
"type": "Int32"
}
TypeIs
Tests whether an expression is of a given type (equivalent to is in C#). Evaluates to a boolean.
Fields
expressionType:"TypeIs"type— The type to test against (TypeReference or string).expression(FormulaExpression) — The expression whose type is tested.
Example: obj is Enemy
{
"expressionType": "TypeIs",
"type": "Enemy",
"expression": {
"expressionType": "MemberResolve",
"name": "obj",
"expression": null,
"useNullPropagation": false
}
}
TypeOf
Returns the runtime type descriptor for the given type (equivalent to typeof(T) in C#).
Fields
expressionType:"TypeOf"type— The type to retrieve (TypeReference or string).
Example: typeof(String)
{
"expressionType": "TypeOf",
"type": "String"
}
Index
Accesses an element of an indexable object (array, list, dictionary, or any type with an indexer).
Fields
expressionType:"Index"expression(FormulaExpression) — The collection or indexable object.arguments(ArgumentCollectionObj) — The index arguments, keyed by positional index ("0","1", …).useNullPropagation(boolean) — Whentrue, the access is null-safe (?[...]). Returns null ifexpressionis null.
Example: items[0]
{
"expressionType": "Index",
"expression": {
"expressionType": "MemberResolve",
"name": "items",
"expression": null,
"useNullPropagation": false
},
"arguments": {
"0": { "expressionType": "Constant", "type": "Int32", "value": 0 }
},
"useNullPropagation": false
}
Invoke
Calls a method or invokes a delegate/function. The method itself is resolved as a MemberResolve node in the expression field.
Fields
expressionType:"Invoke"expression(FormulaExpression) — The callable to invoke (typically aMemberResolvenode).arguments(ArgumentCollectionObj) — Arguments passed to the call, keyed by positional index ("0","1", …) or parameter name.
Example: target.DoDamage(100)
{
"expressionType": "Invoke",
"expression": {
"expressionType": "MemberResolve",
"name": "DoDamage",
"expression": {
"expressionType": "MemberResolve",
"name": "target",
"expression": null,
"useNullPropagation": false
},
"useNullPropagation": false
},
"arguments": {
"0": { "expressionType": "Constant", "type": "Int32", "value": 100 }
}
}
Lambda
Defines an anonymous function (lambda). The arguments field declares the parameters; expression is the body.
Fields
expressionType:"Lambda"arguments(ArgumentCollectionObj) — Parameter declarations. Keys are parameter names; values arenull(parameter type is inferred from context) or aMemberResolveexpression denoting the declared type.expression(FormulaExpression) — The lambda body.
Example: x => x * 2
{
"expressionType": "Lambda",
"arguments": {
"x": null
},
"expression": {
"expressionType": "Multiply",
"left": { "expressionType": "MemberResolve", "name": "x", "expression": null, "useNullPropagation": false },
"right": { "expressionType": "Constant", "type": "System.Int32", "value": 2 }
}
}
New
Instantiates a new object of a given type by calling a constructor.
Fields
expressionType:"New"type— The type to instantiate (TypeReference or string).arguments(ArgumentCollectionObj) — Constructor arguments, keyed by positional index ("0","1", …) or parameter name.
Example: new Vector2(1.0, 0.5)
{
"expressionType": "New",
"type": "Vector2",
"arguments": {
"0": { "expressionType": "Constant", "type": "System.Single", "value": 1.0 },
"1": { "expressionType": "Constant", "type": "System.Single", "value": 0.5 }
}
}
NewArrayBounds
Creates a new array with specified dimension sizes but without initializers.
Fields
expressionType:"NewArrayBounds"type— The element type of the array (TypeReference or string).arguments(ArgumentCollectionObj) — Dimension size expressions, keyed by dimension index ("0"for one-dimensional arrays).
Example: new int[10]
{
"expressionType": "NewArrayBounds",
"type": "System.Int32",
"arguments": {
"0": { "expressionType": "Constant", "type": "System.Int32", "value": 10 }
}
}
NewArrayInit
Creates a new single-dimensional array and initializes it with a list of values.
Fields
expressionType:"NewArrayInit"type— The element type of the array (TypeReference or string).initializers(ArgumentCollectionObj) — Element expressions, keyed by positional index ("0","1", …).
Example: new int[] { 1, 2, 3 }
{
"expressionType": "NewArrayInit",
"type": "System.Int32",
"initializers": {
"0": { "expressionType": "Constant", "type": "System.Int32", "value": 1 },
"1": { "expressionType": "Constant", "type": "System.Int32", "value": 2 },
"2": { "expressionType": "Constant", "type": "System.Int32", "value": 3 }
}
}
MemberInit
Creates a new object and initializes its members. Combines a New node with a set of member bindings.
Fields
expressionType:"MemberInit"new(NewExpression) — The object construction node.bindings(MemberBindingCollectionObj) — A map of member bindings, keyed by member name. Each value is one of:AssignmentBinding— assigns an expression to a member.MemberBinding— recursively initializes members of a nested object.ListBinding— adds elements to a collection member.
Example: new Vector2 { X = 1.0, Y = 0.5 }
{
"expressionType": "MemberInit",
"new": {
"expressionType": "New",
"type": "Vector2",
"arguments": {}
},
"bindings": {
"X": {
"expressionType": "AssignmentBinding",
"name": "X",
"expression": { "expressionType": "Constant", "type": "System.Single", "value": 1.0 }
},
"Y": {
"expressionType": "AssignmentBinding",
"name": "Y",
"expression": { "expressionType": "Constant", "type": "System.Single", "value": 0.5 }
}
}
}
ListInit
Creates a new collection object and populates it using an initializer list. Each element is added by calling the collection’s Add method (or equivalent).
Fields
expressionType:"ListInit"new(NewExpression) — The collection construction node.initializers(ElementInitCollectionObj) — A map of element initializers, keyed by positional index. Each value is anElementInitBindingobject with:expressionType:"ElementInitBinding"initializers(ArgumentCollectionObj) — Arguments passed to the Add method for this element.
Example: new List<int> { 1, 2, 3 }
{
"expressionType": "ListInit",
"new": {
"expressionType": "New",
"type": {
"name": "List",
"expression": "System.Collections.Generic",
"expressionType": "MemberResolve",
"arguments": { "T": "System.Int32" }
},
"arguments": {}
},
"initializers": {
"0": {
"expressionType": "ElementInitBinding",
"initializers": {
"0": { "expressionType": "Constant", "type": "System.Int32", "value": 1 }
}
},
"1": {
"expressionType": "ElementInitBinding",
"initializers": {
"0": { "expressionType": "Constant", "type": "System.Int32", "value": 2 }
}
},
"2": {
"expressionType": "ElementInitBinding",
"initializers": {
"0": { "expressionType": "Constant", "type": "System.Int32", "value": 3 }
}
}
}
}
Member Bindings
Member bindings are used inside MemberInit nodes to specify how object members are initialized.
AssignmentBinding
Assigns a value to a named member.
expressionType:"AssignmentBinding"name(string) — Member name.expression(FormulaExpression) — The value to assign.
MemberBinding
Recursively initializes the members of a nested object member without reassigning the member itself.
expressionType:"MemberBinding"name(string) — Member name of the nested object.bindings(MemberBindingCollectionObj) — Nested bindings applied to the member’s sub-members.
ListBinding
Adds elements to a collection member using element initializers.
expressionType:"ListBinding"name(string) — Member name of the collection.initializers(ElementInitCollectionObj) — Elements to add, keyed by positional index.
Implementing an Interpreter
An interpreter for this AST typically consists of a recursive evaluate(node, context) function that dispatches on expressionType. The context holds the current scope: parameter values, known types, and available members.
Suggested steps:
Dispatch — read
expressionTypefrom the node and select the appropriate handler.Recurse — evaluate child nodes before computing the current node’s result (bottom-up evaluation).
Scope — maintain a variable scope for formula parameters;
MemberResolvewith a nullexpressionlooks up names in this scope first, then in known types.Type resolution — map type names (strings or TypeReference objects) to concrete types in your runtime. Built-in types follow .NET naming conventions (e.g.
System.Int32,System.String).Null propagation — for
MemberResolveandIndexnodes withuseNullPropagation: true, short-circuit and return null when the target expression is null.Overflow checking —
Checkedvariants of arithmetic operators should raise an error on integer overflow;UncheckedScope/CheckedScopechange the checking mode for a sub-tree.