Docs: Expression language

Expression language

The language of every _x field, of state.derived, of functions (let, x), of outcome derived_x, and of let in tactics. It is a safe subset of Python expressions: if you can read Python, you can read it.

An expression has no side effects: it reads the state and returns one value. Changing the state is done by ops (see reference.md).

1. Values

KindExampleNote
noneNonenull in YAML
truth valueTrue, False
number3, -2, 2.5, 1e3, 0x1f, 0o17, 0b101, 1_000One number type for integers and decimals
text'abc', "it's", r'a\n', '''long'''
list[1, 2]
tuple(1, 'x'), 1,, ()Never equal to a list
dict{'power': 3}Keys are text
set{1, 2}, set()
generator(x for x in L), zip(...), enumerate(...)Lazy, usable once
functionlen, modified, board and own functions

True and False count as 1 and 0 (True + True == 2). 1 == 1.0 == True, and these three are the same key in a set.

2. Syntax

Allowed:

  • literals as above; adjacent text literals are joined ('a' 'b' is 'ab');
  • lists, tuples, dicts (also {**d, 'x': 1}) and sets;
  • arithmetic + - * / // % **, unary - and +;
  • comparisons == != < <= > >=, also chained (0 < x <= 3);
  • in, not in, is, is not;
  • and, or, not, and x if c else y;
  • indexing a[i] and slicing a[i:j:k];
  • calls with keyword arguments (max(L, key=len));
  • the six methods in §5;
  • comprehensions [...], {...}, {k: v ...}, with several for and if clauses and tuple unpacking (for k, v in d.items());
  • generator expressions, also as the only argument without extra parentheses (sum(x for x in L));
  • line breaks anywhere (they count as spaces); names with non-ASCII letters.

Not allowed (a syntax error naming the construct; the validator reports it, and the engine when it first evaluates the expression):

  • assignment: =, :=, += and the other augmented forms;
  • lambda, yield, await, async, and statement keywords (def, import, …);
  • names that start with _ (also as keyword argument or loop variable);
  • attributes other than the six methods (x.append is an error);
  • bit operators | & ^ ~ << >> and @;
  • * and ** unpacking in calls and lists (** inside a dict literal is allowed);
  • f-strings, bytes (b''), complex numbers (1j), ...;
  • comments (#), ;, a lone !;
  • more than one expression;
  • a positional argument after a keyword argument; a generator argument next to other arguments without its own parentheses (sum(x for x in L, 0)).

Text escapes: \n \t \r \0 \\ \' \", \xhh, \uhhhh, \Uhhhhhhhh, backslash-newline; any other backslash stays as is. In r'...' nothing is escaped.

Since | is not allowed, a set union is written set(list(a) + list(b)).

3. Meaning

  • and/or return an operand, not True/False: 0 or 'x' is 'x'. They evaluate no more than needed: 1 and 0 and 1/0 is 0.
  • A comparison chain stops at the first that fails: 1 < 0 < 1/0 is False.
  • None, False, 0, and empty text, lists, tuples, dicts and sets are false; everything else is true.
  • Arithmetic:
    • / always divides exactly (7/2 is 3.5);
    • // rounds down (-7 // 2 is -4);
    • % takes the sign of the divisor (-7 % 3 is 2);
    • dividing by zero is a ZeroDivisionError; so is 0 ** -1.
  • + joins texts, lists, or tuples of the same kind. * repeats them with an integer.
  • Sets: - gives the difference; <=, <, >=, > test subsets. Set elements must be hashable: None, booleans, numbers, text, tuples of those. A list or dict in a set is a TypeError.
  • Ordering (< etc., sorted, min, max): numbers with numbers, text with text, or lists (or tuples) of the same kind, element by element. Anything else is a TypeError.
  • Equality is deep: lists, tuples, dicts and sets compare by content. A list never equals a tuple.
  • is compares identity; use it only with None (x is None).
  • in: element of a list, tuple or set; key of a dict; substring of a text.
  • Indexing:
    • negative indexes count from the end;
    • a slice out of range gives fewer elements;
    • text is indexed per character (code point);
    • a missing key is a KeyError, an index out of range an IndexError.
  • Iterating over a dict gives its keys.
  • Comprehensions have their own scope: loop variables do not leak out, and the first in value is computed outside that scope.
  • Generators are lazy: any(1/x for x in [1, 0]) is True without dividing by zero. A generator can be consumed only once and has no len.

4. Built-in functions

These are all the built-in functions; any other name must come from the model.

FunctionMeaning
len(x)Length of text, list, tuple, dict or set
any(it), all(it)Lazy; stop at the first value that decides
min(...), max(...)From one iterable or several arguments; key=f, default=v (for an empty iterable). On ties the first wins
sum(it, start=0)Sum (also lists with start=[]); not for text. Numbers are summed with compensation like Python ≥ 3.12: sum([1, 0.1, 0.2]) == 1.3
range(n), range(a, b[, step])List of integers (at most 10,000,000)
abs(x)Absolute value
round(x), round(x, n)Half to even (round(2.5) is 2), exact on the decimal value (round(2.675, 2) is 2.67)
sorted(it, key=f, reverse=False)Stable; reverse keeps equal values in their order
list(x), tuple(x), set(x)Convert any iterable (no argument: empty)
dict(x), dict(k=v)From a dict, from pairs with text keys, and/or keyword arguments
enumerate(it, start=0), zip(a, b, ...)Lazy; yield tuples
int(x)Truncates toward zero; text must be an integer (int('3.5') is a ValueError)
float(x)Number or text ('inf', 'nan' allowed)
str(x)Text form (Python notation; see §10)
bool(x)Truth value
sigmoid(x) or σ(x)1 / (1 + e^−x)
exp(x), sqrt(x), log(x[, base])Maths; log(0) and sqrt(-1) are a ValueError (math domain error)

Functions the model or engine adds:

FunctionWhenMeaning
modified(stat, value, **context)Always (not in init_x)value after all active modifiers for stat; the keyword arguments are names in the modifiers' if_x and change_x. See reference.md
cells, directions, xy, cell, step, leap, ray, neighbours, distance, path, path_cost, reachable, line_of_sight, shapeWhen the model has boardsBoard geometry, board name first. See board-geometry.md
Own functionsFrom functionsCalled by position or by keyword. See board-geometry.md

5. Methods

Only these, and without keyword arguments:

OnMethod
dict.get(k), .get(k, default), .items(), .keys(), .values()
list, tuple, text.count(x), .index(x) (ValueError if absent; on text x must be text)

.items(), .keys() and .values() return lists. Any other method is an AttributeError at run time if the name is one of the six, and a syntax error otherwise.

6. Names in scope

Every expression sees the global part:

NameValue
constantsUnder their own name
variables, zonesUnder their own name (a player zone is a list of lists)
derived valuesUnder their own name, computed when used
players[0, 1, …, n−1]
n_playersNumber of players
eliminatedList of eliminated players
results{subflow: result} with result 'win', 'loss' or 'draw', for scoped end conditions
contentThe content layer
modifiedSee §4
board functions, own functions, builtinsSee §4

Then, depending on the place, local names:

PlaceExtra names
Flow: over_x, until_x, when_x, step actor_x and visible_to_xNone. Use the loop variable (as), which is a global variable
End conditions: if_x, who_x, returns_xNone
Action pre_xactor (the step's actor, or who has priority in a window)
Ops in cost and effectactor, plus every name bound by earlier ops (bind of choose, roll, let, look, search, create); names bound in cost stay for effect
for_each bodyAlso the as name (default it)
search filter_xAlso the item, as it (or as)
In a priority windowAlso window (its name) and stack (list of {action, actor, cancelled}, bottom first); for a window op also the op's local names
protocol.first_xFor a window on a node: none. For a window op: the op's local names
Ability on a content item (pre_x, cost, effect)As an action, plus self (the item)
exec effectThe local names of the exec, plus self (the item)
Trigger if_x, controller_x, doThe local names at the emit, plus event; self for a content trigger. On an end_event only event
Content active_xself, plus the local names of the trigger or modifier being checked
Modifier if_x, change_xvalue (the value so far), the keyword arguments of modified(), self for a content modifier, and for modify the local names at that time
Outcome derived_x, p_x, effectThe local names of the sample, plus earlier derived_x names
Agent policy_xoptions (list of labels), decider (the player)
Variable init_xOnly: builtins, constants, content, players, n_players, eliminated, results, board and own functions, and variables declared above. No zones, derived values or modified
Zone init_xAs variable init_x, with all variables
Own function let, xIts parameters and earlier lets, plus the global part; not the caller's local names
Tactic expressionsSee tactics.md (me, pool, chosen, o, i, opts, labels, where)

Lookup order when names collide: local names, then variables, then zones, then the global names. Among the global names: modified, own functions, board functions, the reserved names (content, players, n_players, eliminated, results), constants, builtins, in that order of precedence. So a local cell hides the board function cell(), and a variable max hides the builtin. Pick names that do not collide.

7. Twin fields _x

A field named <name>_x holds an expression; <name> without _x is text for people (or, for some fields, a literal value). See reference.md § Conventions for which fields accept literals and what happens to text without _x.

All expression fields the engine reads:

WhereFields
Flow nodeover_x, until_x, actor_x, visible_to_x; case when_x
Actionpre_x
Opsvalue_x, amount_x, of_x, what_x, at_x, n_x, with_x, die_x, options_x, min_x, max_x, actor_x, visible_to_x, filter_x, items_x, until_x, cond_x, target_x, change_x, if_x (modify)
Triggerif_x, controller_x
Modifierif_x, change_x
Content itemactive_x
Priority protocolfirst_x
Stateinit_x (zone, var); derived (x, or the value itself)
Outcome modelderived_x (dict of expressions), p_x
Agentpolicy_x
End conditionif_x, who_x, returns_x
Functionlet (dict of expressions), x

An _x value may be a YAML string, number or boolean. The validator compiles every _x field with a text or number value, every derived_x and functions expression, and derived entries written as {x: ...}, so their syntax errors show up before play. A derived value written as a plain string is only compiled when it is first used.

Quote expressions in YAML when they contain :, #, {, [, or start with a quote: pre_x: "hand[actor] != []".

8. Errors

Syntax errors (reported by the validator):

MessageCause
syntax error in expression "<src>": <what> (position <n>)Not valid syntax (empty expression, unterminated string, comments are not allowed, f-strings are not allowed, …)
<Construct> is not allowed in "<src>"A forbidden construct: Lambda, Yield, Await, assignment, NamedExpr, Starred, BitOr, BitAnd, BitXor, Invert, LShift, RShift, MatMult, Ellipsis, semicolon, …
attribute .<name> is not allowed in "<src>"An attribute other than the six methods
name <name> is not allowed in "<src>"A name starting with _

Run-time errors:

error evaluating '<src>': <Kind>: <message>

<Kind> is named as in Python: TypeError, ValueError, KeyError, IndexError, ZeroDivisionError, NameError (unknown name at that place), AttributeError, RecursionError. A few errors come without that wrapper: dict keys must be text, not int, string formatting with % is not allowed; use str() and +.

9. Examples

score[actor] >= GOAL
[c for c in hand[actor] if c['cost'] <= gold[actor]]
max(players, key=total_of)            # total_of from functions
sum(c['value'] for c in table)
{**unit, 'damage': 0}
'Ranged' in self.get('keywords', '')
sorted(hand[actor], key=card_value, reverse=True)[:2]   # card_value from functions

10. Differences from Python

The language follows Python's semantics, with these deliberate differences:

  1. One number type. 2.0 and 2 are the same number: str(4/2) is '2' (Python '2.0'), and L[4/2] works (Python: TypeError). Use int() when an integer is meant.
  2. Dict keys are text. {1: 'a'} and dict([(1, 2)]) are errors. k in d with a non-text k is False.
  3. Key order. Dicts keep insertion order, except text keys that are integers ('2', '10'): they come first, ascending. Sort before relying on such an order.
  4. range, .keys(), .values() and .items() return lists. range(3) == [0, 1, 2] is True.
  5. Set order is insertion order (Python: arbitrary). Use sorted() when order matters.
  6. No % formatting on text. Use str() and +.
  7. No complex numbers or bytes. (-8) ** 0.5 is nan.
  8. Integers above 2^53 lose precision.
  9. Dates in YAML stay text.

This page as Markdown