Skip to content

7. Expressions

EBNF
expression = assignment_expression
| arithmetic_expression
| logical_expression
| inscription
| inspection
| unary_expression .
unary_expression = [ negation ] "(" expression ")"
| [ negation ] simple_unary_expression .
simple_unary_expression = object
| list
| custom_function_call
| reference
| chance_expression
| literal
| trope_fit
| trope_fit_sugared
| action_search
| sifting .
negation = "!" .

An expression is a syntactic form that evaluates to a value.

Expressions appear throughout Viv—in conditions, effects, casting-pool directives, and many other contexts. This chapter describes each expression form.

The following table lists operators from lowest to highest precedence. Operators within the same row share the same precedence level. To override the default grouping for a complex expression, parenthesize one or more of the component expressions.

PrecedenceOperatorsAssociativity
1 (lowest)=, +=, -=, *=, /=, append, removeNone
2inscribe, inspectNone
3`
4&&Left
5==, !=, <, <=, >, >=, in, knows, caused, triggered, precededNone
6+, -Left
7*, /Left
8 (highest)!Unary (prefix)

Assignments, inscriptions, and inspections are tried before the precedence chain via ordered alternatives in the grammar. As such, they do not compose freely with other operators. For example, an expression like @a.mood + 5 inscribe @b yields a parse error.

As the table notes, relational operators are non-associative: A < B < C is a parse error. Use A < B && B < C instead.

Here are some examples showing how valid complex expressions are grouped according to operator precedence:

  • A + B * CA + (B * C)
  • A * B + C > D((A * B) + C) > D
  • A > B && C > D || E((A > B) && (C > D)) || E
  • A + B > C && !D || E == F(((A + B) > C) && (!D)) || (E == F)

Any expression may be wrapped in parentheses to override the default precedence grouping:

(@person.mood + @friend.mood) / 2

The negation operator ! inverts the truthiness of an expression:

!@person.dead
!(@person.best_friend == @target)

The literal forms are valid expressions whose syntaxes are specified in their respective sections: booleans, null, numbers, strings, template strings, enums, lists, and objects.

EBNF
reference = [ scratch_variable_sigil | local_variable_sigil ]
( entity_sigil | symbol_sigil ) identifier [ group_role_decorator ]
[ eval_fail_safe_marker ] [ reference_path ] .

A reference is the primary way to read data from the simulated storyworld.

It’s anchored in the name of a role or variable, and may include an optional reference path.

A bare reference to an entity—meaning a bare entity name with no reference path—always evaluates to its entity ID, which is ultimately a string. (Note that as soon as any reference path is attached to such a reference, the entity ID is automatically dereferenced—i.e., you can write @person.friends in lieu of @person->friends.) This is called dehydration.

Meanwhile, a bare reference to a symbol evaluates to the bound symbol value itself.

EBNF
reference_path = ( property_access | pointer_access | lookup_access )+ .
A *reference path* extends a reference with a sequence of one or more chained access operations, potentially reaching into data associated with other entities. There are three kinds of path segments:
EBNF
property_access = "." property_name [ eval_fail_safe_marker ] .
property_name = ( letter | digit | "_" )+ .
eval_fail_safe_marker = "?" .

A property access uses dot notation to evaluate the specified property of the evaluation of a given reference.

Note that, unless the fail-safe marker is used, the specified property MUST exist.

Here’s a few examples:

  • @person.name
    • Evaluates to the value stored in the name property of the entity cast as @person.
  • @person.personality.shy
    • Evaluates to the value stored in the shy field of the personality property on the entity cast as @person.
  • _@wand.power
    • Evaluates to the value stored in the power property of the entity saved to the local variable _@wand.
EBNF
lookup_access = "[" expression "]" [ eval_fail_safe_marker ] .
eval_fail_safe_marker = "?" .

A lookup access uses bracket notation to access by key (or index) a value in the evaluation of a given reference, where the key is an arbitrary expression.

Here are some important constraints (which can be suppressed via the fail-safe marker):

  • If the property being accessed is an array, the key expression MUST evaluate to a non-negative integer and MUST fall in bounds (given the array length).
  • If the property being accessed is an object, the key expression MUST evaluate to a string and MUST be defined in the object.

And here are some examples:

  • @person.friends[0]
    • Evaluates to the 0th element stored in the friends array property of the entity cast as @person.
  • @person.affinities[@other]
    • Evaluates to the value stored a) in the affinities object property of the entity cast as @person, b) under the key corresponding to the evaluation of @other. As noted above, a bare reference like @other evaluates to an entity ID, which is ultimately a string (and thus a valid key).
EBNF
pointer_access = "->" property_name [ eval_fail_safe_marker ] .
property_name = ( letter | digit | "_" )+ .
eval_fail_safe_marker = "?" .

A pointer access uses arrow notation of the form <reference>-><property_name> to access a property on another entity.

The reference preceding the arrow evaluates to an entity ID, which is dereferenced, and then the specified property on that entity is accessed.

Here are some important constraints (which can be suppressed via the fail-safe marker, except where noted):

  • A pointer access MUST NOT be the first segment in a reference path. While a syntax like @person->boss might seem required—because @person evaluates to an entity ID—Viv enables authors to directly access the properties of the anchor entity, as in @person.boss. To reduce confusion, Viv enforces that this pattern be used exclusively. Under the hood, this is supported by the interpreter hydrating the anchor entity ID prior to walking a reference path. This constraint cannot be suppressed by the fail-safe marker.
  • The reference preceding the arrow MUST evaluate to an entity ID.
  • The specified property MUST exist on the dereferenced entity.

Finally, some examples:

  • @person.boss->boss
    • Evaluates to the person’s boss’s boss—that is, the value stored in the boss property of the entity whose ID is stored in the boss property of the entity cast in the @person role.
  • @item.location->address
    • Evaluates to the address of the location of the item: the value stored in the address property of the entity whose ID is stored in the location property of the entity cast in the @item role.

Path segments may be chained in any combination, with the components of the reference path being evaluated left to right, with each segment operating on the result of the previous one.

Here’s some examples of complex reference paths:

@person.friends[0]->name
@person.kids[0]->best_friend->father->name.middle
@person.spouse->spouse->spouse->spouse

The fail-safe marker ? instructs the interpreter to treat a missing or null value as a soft failure rather than an error.

It may appear after a reference or after any segment of a reference path, including the final segment. If the value at the marked position is null or undefined, evaluation of the reference short-circuits and yields null instead of raising an error:

// If @person has a null 'partner' property, yields null instead of an error
@person.partner?->name
// Fail-safe on the reference itself
@person?.name
// Multiple fail-safe markers in a chain
@person?.partner?->friends?[0]?

The fail-safe marker also applies in casting-pool directives, where a null result causes the pool to be treated as empty rather than producing an error.

EBNF
assignment = reference assignment_operator expression .
assignment_operator = "=" | "+=" | "-=" | "*=" | "/=" | "append" | "remove" .

An assignment expression modifies a value in the host application via the adapter. The left-hand side MUST be a reference. The available operators are:

OperatorMeaning
=Set the value.
+=Add to the value.
-=Subtract from the value.
*=Multiply the value.
/=Divide the value.
appendAppend to a list value.
removeRemove from a list value.
@person.mood = 50
@person.mood += 10
@person.friends append @new_friend
@person.enemies remove @former_enemy

Assignment expressions always evaluate to true.

EBNF
arithmetic_expression = unary_expression arithmetic_operator expression .
arithmetic_operator = "+" | "-" | "*" | "/" .

An arithmetic expression applies a binary arithmetic operator to two operands:

@person.strength + 10
@person.gold * 2
(@buyer.budget + @seller.asking) / 2
EBNF
logical_expression = disjunction .
disjunction = conjunction { "||" conjunction } .
conjunction = relational_expression { "&&" relational_expression } .
relational_expression = unary_expression relational_operator unary_expression
| unary_expression .

Logical expressions combine relational tests using the conjunction operator && (logical AND) and the disjunction operator || (logical OR). Standard short-circuit evaluation applies.

@person.mood > 50 && @person.boldness > 30
@person.hungry || @person.tired
EBNF
relational_operator = "==" | "!=" | "<" | "<=" | ">" | ">="
| "in" | "knows" | "caused" | "triggered" | "preceded" .

A relational expression compares two operands using one of the following operators:

OperatorMeaning
==Equal to
!=Not equal to
<Less than
<=Less than or equal to
>Greater than
>=Greater than or equal to

The additional relational operators each merit a bit more detail.

The in operator tests whether the left operand is contained in the right operand (typically a list or collection):

@person in @group.members

The knows operator tests whether a character (left operand) has a memory of a given action (right operand). It returns true if the character’s memories include the action, and false otherwise:

@person knows @betrayal_action

The caused operator tests whether an action (left operand) is a causal ancestor of another action (right operand)—that is, whether the left action directly or transitively led to the right action:

@insult caused @retaliation

The triggered operator tests whether an action (left operand) is a direct cause of another action (right operand)—that is, whether the right action’s immediate causes include the left action:

@insult triggered @retaliation

The preceded operator tests whether an action (left operand) occurred before another action (right operand). If both actions have the same timestamp, the left is deemed to have preceded the right if and only if the right is not a causal ancestor of the left:

@first_meeting preceded @betrayal

The caused, triggered, and preceded operators MUST receive single-action operands. A group action role reference (e.g., @setup*) is not permitted as an operand. To test a relation for each member of a group, use a loop:

loop @setup* as _@s:
_@s preceded @climax
end
EBNF
chance_expression = number "%" .

A chance expression evaluates to true with the specified probability (expressed as a percentage) and false otherwise:

50% // true half the time
10% // true 10% of the time

Chance expressions are useful in conditions and effects to introduce randomness.

EBNF
custom_function_call = "~" identifier "(" [ expression { "," expression } ] ")" [ "?" ] .

A custom function call invokes a function exposed by the host application in its adapter. The call is prefixed with the ~ operator, followed by the function name, parenthesized arguments, and an optional fail-safe marker:

~getRandomInsult()
~calculateDamage(@attacker.strength, @defender.armor)
~findNearbyCharacters(@location)?

Arguments are evaluated and dehydrated (entity data is converted to entity IDs) before being passed to the adapter function. If the ? marker is present and the function returns null or undefined, evaluation short-circuits gracefully rather than raising an error.

EBNF
trope_fit = "fit" "trope" identifier ":" bindings .
trope_fit_sugared = bindings_sugared "fits" "trope" identifier .

A trope fit expression tests whether a trope matches for a given set of bindings. It evaluates to a boolean:

// Standard form
fit trope rivalry:
with:
@hero: @person1
@villain: @person2
// Sugared form
<@person1, @person2> fits trope rivalry
EBNF
action_search = action_search_header ":" action_search_body
| action_search_bare_header ":" action_search_bare_body .
action_search_header = "search" "query" identifier .
action_search_bare_header = "search" .
action_search_body = (* unordered, each field optional *)
[ search_domain ]
[ bindings ] .
action_search_bare_body = search_domain .

An action search expression executes a query over a search domain and returns the list of matching action instances. There are two forms:

A named query search references a defined query by name, with optional bindings and a search domain:

search query recent-betrayals:
over: chronicle
with:
@perpetrator: @villain

A bare search specifies only a search domain, without referencing a query:

search:
over: @person
EBNF
search_domain = "over" ":" ( "inherit" | "chronicle" | expression ) .

A search domain specifies the collection of action instances to search over. There are three forms:

DomainMeaning
inheritThe search domain inherited from an enclosing context (e.g., a sifting pattern that is itself searching over a domain).
chronicleThe global chronicle—the complete record of all actions performed in the simulation.
expressionAn expression that evaluates to a collection of action instances, or a character reference whose memories will be searched.
search query recent-acts:
over: chronicle
search query personal-history:
over: @person
search query subset:
over: inherit
EBNF
sifting = sifting_header sifting_body .
sifting_header = "sift" "pattern" identifier ":" .
sifting_body = (* unordered, each field optional *)
[ search_domain ]
[ bindings ] .

A sifting expression invokes a sifting pattern over a search domain, with optional bindings. It returns the list of matches—each match being a set of role and action bindings that satisfy the pattern:

sift pattern betrayal-arc:
over: chronicle
with partial:
@betrayer: @villain
EBNF
inscription = unary_expression "inscribe" expression .

An inscription expression records knowledge of an action onto an item. The left operand MUST evaluate to an item, and the right operand MUST evaluate to an action. After evaluation, the item carries a record of the action, which can later be revealed to a character via inspection:

@letter inscribe @secret_meeting

Inscription always evaluates to true.

EBNF
inspection = unary_expression "inspect" expression .

An inspection expression causes a character to learn about all actions inscribed on an item. The left operand MUST evaluate to a character, and the right operand MUST evaluate to an item. After evaluation, the character gains memories of all actions previously inscribed on the item:

@reader inspect @letter

Inspection always evaluates to true.