On this page

Eurydice language specification

This specification defines how Eurydice should behave. Any divergence between Eurydice and this description is a bug; please report it.

This document is intended primarily for other AnyDice implementers and power users. If you're looking for an introduction to the AnyDice DSL, this document is likely too technical. See the AnyDice docs for a gentler introduction.

This project is not affiliated with AnyDice or Jasper Flick.

Overview

Eurydice is a dialect of the AnyDice language. It is almost completely compatible with AnyDice, with some extensions, and a few restrictions.

IMPORTANT

Intentional divergences from AnyDice are marked with information boxes like this one. Sections marked ⊕ describe Eurydice-only features.

Eurydice is reproducible and hermetic: programs can't interact with the environment, and two runs will have exactly the same results.

This document frequently uses the following notations to describe pools:

  • <n>d<k> represents a pool of n dice with k faces labeled 1 through k.
  • <n>d{<k1>:<count1>,...,<kn>:<countn>} represents a pool of n dice with count1 faces showing k1, count2 faces showing k2, and so on. If :<count> is omitted for any k, the count defaults to 1.

This is exactly compatible with Eurydice expressions using the d operator.

Lexical elements

A Eurydice program is composed of a sequence of statements. Eurydice is whitespace-insensitive, except to separate tokens.

Block comments start and end with backslashes: \ this is a comment \.

Line comments start with three backslashes: \\\ this is a line comment.

IMPORTANT

AnyDice does not support line comments.

Comments are treated as whitespace, and can be used to separate tokens, e.g. [hello\a comment\world] is equivalent to [hello world].

Variable identifiers are composed of uppercase letters and underscores, for instance X, MY_VARIABLE, or _MY_VARIABLE. MyVariable and x are not variable identifiers.

Words are composed of lowercase letters and underscores (after the first position only), except the single lowercase letter d, which is punctuation rather than a word. word, if, output, and my_word are all words. WORD, wOrd, and _word are not.

IMPORTANT

AnyDice words (function names) can't contain underscores.

Punctuation: the following characters or sequences of characters are tokens:

+   =   &   @   :   (   ..
-   !=  |   d   [   )   ,
*   <   !   #   ]   {
/   <=              }
^   >
    >=

Integer literals: Integer literals are base-10 representations of unsigned 31-bit integers. It is an error if an integer literal represents a number greater than 2^31-1 (2147483647). These literals represent a subset of the underlying int type, which holds 32-bit signed integers; negative values can be obtained with the unary minus operator. Examples include 0 and 123.

String literals: String literals can occur only in a few places. They are enclosed in double quotes ("). A string literal can contain escaped double quotes: "message: \"hello\"". No other escape sequences are supported.

Program execution

A Eurydice program is composed of a sequence of statements, which are executed in order. Once the program has finished executing, all outputs are displayed to the user.

Errors are fatal and terminate program execution.

Data types

The type grammar for Eurydice is

Scalar  = int | symbol | additive_identity
Element = Scalar | tuple of 2 to 4 Scalars
Type    = Element | list | pool

There are four kinds of element values, the first three of which are scalars:

  • int: 32-bit signed integers.
  • symbols: declared non-numeric values which can be used to represent discrete outcomes, booleans, etc.
  • additive identity: a single value equivalent to a polymorphic 0; see discussion in (empty collections)[#-empty-collections]. This value is equivalent to the int 0 in most contexts.
  • tuples: ordered sequences of two two four scalars (ints or symbols), called fields.

Two collection types can contain element values:

  • list: ordered sequences of elements. These have usual array semantics, and elements can be repeated. The items can be of any element type, and different items can have different types.
  • pool: a non-ordered map-like collections of elements (called outcomes), each with an associated weight (whose representation is unspecified). The mapping may be empty, see empty collections for further discussion. Like in a list, different outcomes may be of different element types. A given outcome may only appear once. A pool also holds a single nonnegative integer called the dimension, which is used to represent multiple independent distributions with the same outcomes.

The maximum number of elements in a list, or of outcomes in a pool, is 2^31-1.

IMPORTANT

A reader concerned only with AnyDice compatibility may read Element as int throughout — every collection is then homogeneous. Such a reader may also treat every ✗ in the table below as "does not arise", and skip those sections entirely.

AnyDice does not restrict integers to 32-bit values. Experimentation suggests that AnyDice integers are represented as double-precision floats (output 9007199254740993 returns 9007199254740992).

There are no first-class functions.

⊕ Mixed collections

Collections (both lists and pools) may have items of different types. However, all outcomes must have the same arity: either they must all be scalars, or they must all be tuples of the same arity.

enum { TIMES_TWO }
DIE: d{1, TIMES_TWO}
\\\ The following is equivalent to d{[tuple 1 1], [tuple DIE 1]}
PAIR: [tuple DIE 1]

A collection whose outcomes are not all ints (or not all tuples of ints) is not additive, so it cannot be summed unless its dimension is one. This means that most restrictions that apply to symbol-valued collections are also errors.

What element types support

intsymboltuple of intstuple with a symbol field
=, !=
<, <=, >, >=, [sort]
+, -, unary -componentwise
*, /by an int only
^, &, |, !
#digit count1arityarity
endpoint of a .. range
dice count or side count for d, index for @
iteration order: multisets, "position order"
summing a pool of dimension other than 1

= and != are total: they are defined between any two elements, and elements of different kinds are never equal. Nothing else is: every operation requires int operands unless this document says otherwise, and supplying a symbol or a tuple anywhere else is an error. Besides the ✓ entries above, the operations that accept other element types are collection construction, pool construction, multiset iteration, @ selecting a single element, [SEQ contains N], [count NEEDLES in HAYSTACK], the integer built-ins, the tuple built-ins, and print/output.

A type is additive when it has a ✓ in the + row: an int, or a tuple of ints only. A list or pool is additive when every one of its outcomes is. Only additive collections can be summed to an element.

IMPORTANT

In AnyDice the only element type is int, so every column but the first is empty, everything is additive, and the rule above imposes no restrictions.

⊕ Symbols

Symbols can be declared at the top level only, with the enum keyword:

enum: MISS
enum: HIT
enum: CRITICALHIT

\\\ equivalent syntax
enum { MISS, HIT, CRITICALHIT }

Symbol names use variable-identifier syntax. Variables and symbol names share a namespace. It is an error to declare a symbol whose name is the same as a declared variable. Conversely, it is an error to declare a variable whose name is that same as a declared symbol.

Symbols have an iteration order, which is their declaration order in the program. It is used only to sort multisets during multiset iteration, and by the "position order" setting. It does not make ordering comparisons or [sort] available for symbols. Every int sorts before every symbol.

⊕ Tuples

Tuples are constructed by the built-in functions [tuple A B], [tuple A B C], and [tuple A B C D]. Each field must be a scalar. Nested tuples are not supported.

PAIR: [tuple 3 5]
output [field 1 of PAIR]  \ Outputs 3
output [field 2 of PAIR]  \ Outputs 5
output #PAIR              \ Outputs 2

Tuple fields are selected with [field INDEX of TUPLE]. Indices start at one and it is an error if the index is not within the tuple's bounds. The @ operator does not work on tuples.

As discussed above, a tuple value is additive if all of its fields are int values. Two additive tuples can be summed or subtracted componentwise, negated componentwise, and multiplied or divided by a single integer.

The iteration order for tuples is lexicographic by field.

Like all other functions, tuple constructors support pool-based function evaluation, which makes it easy to generate joint distributions:

JOINT: [tuple d2, d2]
\\\ JOINT has outcomes (1, 1), (1, 2), (2, 1), and (2, 2).
output [field 1 of JOINT]

Pools

Every die in a pool is identical. For instance, 3d6 is a pool of three d6s, but there is no way to represent a pool of one d6 and one d8.

Pools may have no possible outcomes (for instance, such a pool is created with expression d{}). Such a pool has dimension 0.

The following operations are frequently referred to in this document:

Summing

Summing an additive pool transforms it into a pool of dimension 1 whose outcomes are the possible sums when sampling from the dice in the pool, with associated probabilities. Integer outcomes are added normally and additive tuple outcomes are added componentwise.

For instance, summing 2d2 results in a pool equivalent to d{2, 3, 3, 4}.

Summing a pool of dimension 1 does nothing, including for a non-additive pool, as does summing an empty pool. Summing a non-additive pool of any other dimension is an error. Summing an empty pool is covered in Empty collections.

Multiset iteration

Iterating over the multisets in a pool refers to processing each possible sorted outcome of the collection of dice in the pool, together with its associated probability. For instance, the multisets of 3d{1, 2:2, 3:3} are:

multiset              probability
{1, 1, 1}:   (1/6)^3                     * 1
{1, 1, 2}:   (1/6)^2 *  1/3              * 3
{1, 1, 3}:   (1/6)^2           *  1/2    * 3
{1, 2, 2}:    1/6    * (1/3)^2           * 3
{1, 2, 3}:    1/6    *  1/3    *  1/2    * 6
{1, 3, 3}:    1/6              * (1/2)^2 * 3
{2, 2, 2}:             (1/3)^3           * 1
{2, 2, 3}:             (1/3)^2 *  1/2    * 3
{2, 3, 3}:              1/3    * (1/2)^2 * 3
{3, 3, 3}:                       (1/2)^3 * 1

The probability associated with each multiset is the product of its values' probabilities, multiplied by the number of orderings that produce it. That count is the multinomial coefficient n!m1!m2!mk!\frac{n!}{m_1!\,m_2!\cdots m_k!}, where nn is the number of dice in the pool and mim_i is the number of times the ii-th distinct value occurs in the multiset. These ten probabilities sum to 1.

If there is a single die in the pool, this is the same as iterating over the die's outcomes.

Multiset iteration is available for every lists of any type, additive or not. Given enum { MISS, HIT }, the multisets of 2d{MISS, HIT} are {MISS, MISS} with probability 1/4, {MISS, HIT} with probability 1/2, and {HIT, HIT} with probability 1/4.

Outcome mapping

Outcome mapping a pool transforms each of its outcomes with a specified operation. If two outcomes are transformed into the same value, their probabilities are summed. The operation applies to individual outcomes, not to multisets, so a pool of dimension greater than one is summed first by whichever operation calls for the mapping.

For instance, outcome mapping 2d{-1..1} with the operation x→2*abs(x) results in a pool equivalent to d{0:3, 2:4, 4:2}.

Flat mapping

Flat mapping a pool uses a function that turns a multiset into a pool. The function is called for each multiset of the original pool. The resulting pools are combined according to the probability of each original multiset.

Each produced pool is summed if its dimension is not 1. The resulting pool always has a dimension of 1.

In pseudocode:

def flat_map(pool, f):
    final_outcomes = {}
    for (outcome, prob) in pool.multisets():
        for (new_outcome, new_prob) in f(outcome).sum():
            final_outcomes[new_outcome] += prob * new_prob
    return final_outcomes

For instance, flat mapping d3 with an operation x→d(x) produces a pool equivalent to d{1:11, 2:5, 3:2}. d3's outcomes are 1, 2, and 3, with equal probabilities of 1/3.

  • 1 maps to d1 = d{1}
  • 2 maps to d2 = d{1, 2}
  • 3 maps to d3 = d{1, 2, 3}

In the final result, 1 gets probability 1/3 * 1 + 1/3 * 1/2 + 1/3 * 1/3 = 11/18; 2 gets probability 1/3 * 1/2 + 1/3 * 1/3 = 5/18; and 3 gets probability 1/3 * 1/3 = 2/18, equivalent to d{1:11, 2:5, 3:2}.

Multiset cross product

Starting with pools p1,p2,,pnp_1, p_2, \ldots, p_n, the multiset cross product is an iterator over pairs of (list of multisets, probability), where:

  • The lists range over all possible combinations containing one multiset from each pool's multiset iterator.
  • The probability associated with each list is the product of the multisets' probabilities.

In pseudocode:

for (multiset_1, prob_1) in p_1.multisets():
    ...
        for (multiset_n, prob_n) in p_n.multisets():
            yield ([multiset_1, ..., multiset_n], prob_1 * ... * prob_n)

⊕ Empty collections

In AnyDice, where the only element type is int, and an empty collection sums to 0. In Eurydice, empty collections must also sum together with non-int additive types, which induces more complex typing rules.

Summing a collection with no outcomes produces the empty sum, written 𝑒. It is a single value, not a type the user can write.

𝑒 adopts the type of whatever it is combined with: added to an int it is 0, and added to an additive tuple it is that tuple's all-zero value. Where an int is required and 𝑒 is still unconstrained, it becomes the integer 0; this is what makes {} + 1 evaluate to 1, as in AnyDice. The print statement displays an unconstrained 𝑒 as 𝑒; output displays it as 0.

Summing an empty pool of any dimension produces a pool with the additive identity as its only outcome. For instance,

d{} + 2d{} + d{[tuple 1 2]}

is a well-typed expression.

Values, variables, and bindings

All Eurydice values are immutable: it is not possible to modify a list or a pool in place.

Eurydice uses dynamic scoping. At every point during execution, an environment maps variable names to values, and function names to functions. Outside a function, the environment is known as the global environment. When execution is about to enter a function's body, a new environment frame is pushed to the stack of environments. This environment frame is popped and deleted when execution leaves the function.

function: dynamic one {
    X: 1
    result: [x]
}

function: dynamic two {
    X: 2
    result: [x]
}

function: x {
    result: X
}

output [dynamic one]  \\\ Outputs 1
output [dynamic two]  \\\ Outputs 2

Values can be bound to variable names in three ways: through an assignment statement, through function parameters, or through a loop statement. All methods only affect the innermost environment frame.

Functions can be bound to function names in just one way: with a function statement. This method also only affects the innermost frame.

Variable and function bindings are mutable: new values can be assigned to variables, and new functions can be assigned to function names. Only bindings in the innermost environment frame can be mutated. Reusing a name from an outer frame in an assignment will shadow the outer binding while the inner frame is live.

function: x {
    result: X
}

function: reassign x {
    X: 2
    result: X
}

X: 0
output [x]           \\\ Outputs 0
X: 1
output [x]           \\\ Outputs 1
output [reassign x]  \\\ Outputs 2
output [x]           \\\ Outputs 1 - the global environment
                     \\\ was not modified by [reassign x].

Global settings

There are three named global settings:

  • explode depth: a non-negative integer with a default of 2.
  • maximum function depth: a non-negative integer with a default of 10. If the current recursion depth already equals this setting, an attempted function call does not execute and instead returns an empty list. Summing that list yields the additive identity, allowing truncated recursion to keep accumulating values of any additive type.
  • position order: either "lowest first" or "highest first" (the default). This setting affects three things:

Settings can only be set outside of a function, using the set statement.

Expressions

References

The name of a variable is an expression. It evaluates to the value of that variable in the innermost environment frame that has a binding for that variable name.

It is an error to refer to an unbound variable name.

X: 3
output X  \\\ Outputs 3
X: {1, 2}
output X  \\\ Outputs {1, 2}
X: d4
output X  \\\ Outputs d4
output Y  \\\ Error

Note that assigning a pool to a variable does not attach the variable to a particular outcome. For instance,

X: d4
output X + X   \\\ Output outcomes are {2, 3, 4, 5, 6, 7, 8}
               \\\ *not* {2, 4, 6, 8}.

To achieve this effect, use a pool-evaluated function:

function: add D:n to self { result: D + D }
output [add d4 to self]  \\\ Output outcomes: {2, 4, 6, 8}

Literals

The value of an integer literal is the integer it represents.

output 25  \\\ Outputs 25

List expressions

A list expression is a comma-separated list of list elements, enclosed in curly braces:

ListExpr = '{' {ListElem ','} [ListElem [',']] '}'.

Once all list elements are evaluated, they are each flattened into a list, and concatenated.

List elements

Each list element is one of an expression or a range, optionally followed by a repeat count:

ListElem = (Expr | Expr '..' Expr) [':' Expr].

Expressions composing a list element are evaluated in an unspecified order. If the list element is a range, both expressions must evaluate to int values, and the range is replaced by a list containing all integers between the start and end of the range, inclusive. If the start of the range is greater than the end, an empty list is produced.

If a repeat expression is present, it must evaluate to an int. If this value is negative, it is replaced with 0. The primary expression is then flattened into a list, and is concatenated to itself the indicated number of times. If the repeat count is 0, the empty list is produced.

List flattening

List flattening transforms values in the following way:

  • Element values are flattened by enclosing them in a single-element list.
  • list values are unchanged by flattening.
  • pool values are flattened first by summing, then by discarding the probabilities and creating a list containing each outcome in ascending order. Outcomes with nonzero probability appear once regardless of their probabilities.

Flattening a non-additive pool whose dimension is not one is an error, because it would require adding its outcomes together.

Examples

output {}                \\\ Outputs {}
output {1, 2, 3}         \\\ Outputs {1, 2, 3}
output {d4}              \\\ Outputs {1, 2, 3, 4}
output {d2:2}            \\\ Outputs {1, 2, 1, 2}
output {{1, 2}, {3, 4}}  \\\ Outputs {1, 2, 3, 4}

Parenthesized expressions

'(' Expr ')'.

A single expression enclosed in parentheses yields the result of that expression. Explicit parentheses may be used for clarity, or to override the default association of subexpressions.

Unary operators

There are four unary operators, all of which bind more tightly than any binary operator: !, -, #, and d.

! and - compute logical and arithmetic negation respectively. ! requires an int. - accepts all additive values, and negates tuple fields componentwise.

  • If the argument is an int, - negates the value, while ! evaluates to 0 if the argument is nonzero and 1 otherwise.
  • If the argument is a list, its values are summed to an element, to which the operator is applied.
  • If the argument is a pool, the pool is summed, then the outcomes of the pool are all mapped with the operator.

# evaluates to the length of its argument:

  • If the argument is an int, it returns the number of digits in the base-10 representation. #0 evaluates to 1. A leading - is not counted as a digit.
  • If the argument is a symbol, it returns 1.
  • If the argument is a tuple, it returns the tuple's arity.
  • If the argument is a list, it returns the number of elements.
  • If the argument is a pool, it returns the pool's dimension (count of dice in the pool).

The d unary operator acts as the d binary operator, with a left argument of 1.

Binary operators

This is the binary operator precedence list for Eurydice, in decreasing order of precedence:

  1. d
  2. @
  3. ^
  4. *, /
  5. +, -
  6. =, !=, <, <=, >, >=
  7. &, |

All operators are left-associative.

d operator

The d operator is the main way to create a pool.

  1. The RHS operand is converted to a pool with the following operation:
    1. If it is an int i, it is converted to a pool with values 1 to abs(i) inclusive (if i is 0, the pool contains the single outcome 0). If i is negative, the pool outcomes then are mapped to their opposite. For instance, d(-3) evaluates to d{-1, -2, -3}.
    2. If it is a list, it is converted to a pool whose outcomes are the distinct values in the list, and whose probability for each outcome is proportional to the number of occurrences of each value in the list. An empty list produces a pool of dimension 0.
    3. Pools provided as an RHS operand are not transformed.
  2. The LHS operand is first summed if it is a list (which must be an additive list), then:
    1. If it is an int i, the dimension of the RHS pool is multiplied by abs(i). If i is negative, then each outcome in the resulting pool is multiplied by -1; since that requires negating every outcome, a negative LHS requires an additive RHS outcome type.
    2. If it is a pool with int outcomes, then the LHS is flat mapped with the operation described in step 1.1. (Recall that flat mapping takes a pool and an int -> pool function; the operation described in step 1.1 is such a function.) Flat mapping sums each intermediate pool, so a non-additive RHS is an error unless every LHS outcome is 1.
    3. It is an error if a value of another type is used as LHS to d.

@ operator

The @ operator selects the (LHS)-th element from its RHS. A tuple is not treated as a list by this operator; use [field INDEX of TUPLE] to project a tuple field.

The LHS supplies the indices, and must be an int or a list of ints; anything else is an error. An int LHS is converted to a singleton list.

  • If the RHS is an int or list, for each value i in the resulting LHS list:
    • If the RHS is an int j, the base-10 digit of abs(j) at index i is selected. If j is negative, this digit is multiplied by -1. If the "position order" global setting is set to "highest first", index 1 corresponds to the most significant digit; otherwise, to the least significant. Valid indices start at 1.
    • If the RHS is a list, its element at position i is selected. The first element of the list has index 1. This is not affected by the "position order" setting.
    • In both the int and list RHS cases, if i is invalid (zero or negative, or greater than list length or digit count), the expression evaluates to 0.
    • Finally, all selected elements are summed. Selecting a single element works for any RHS outcome type; it is an error to select a number of elements other than one from a RHS that is not additive, since they can't be summed.
  • If the RHS argument is a pool, each outcome multiset is flat mapped with the following function:
    • The multiset is sorted according to the "position order" global setting.
    • Elements are selected from the multiset and summed as if using the @ operator from a list.

Mathematical operators

The operators ^, *, /, +, -, &, and | are mathematical operators, performing exponentiation, multiplication, integer division, addition, subtraction, logical conjunction, and logical disjunction, respectively.

Applied to ints, & evaluates to 1 if both of its arguments are nonzero, 0 otherwise; | evaluates to 1 if at least one argument is nonzero. Other mathematical operators have standard behavior on the integers.

Division by 0 causes an error to be raised. 0^0 evaluates to 1.

Operators have the following signatures on element values:

^ * / + - & | : int, int                       -> int
+ -           : additive tuple, additive tuple -> additive tuple
*             : int, additive tuple            -> additive tuple
* /           : additive tuple, int            -> additive tuple

The empty sum 𝑒 may stand in for either operand of +, -, * and /, adopting the other operand's type; 𝑒 combined with 𝑒 is again 𝑒.

If the arguments are not both elements, the coercion rules are the following:

  1. It is an error if either argument is a non-additive list or pool.
  2. If either argument to a mathematical operator is an additive list, it is summed to an element.
  3. After this, if both arguments are elements, the operator is applied according to the rules above.
  4. Otherwise, both arguments are converted to pools and summed. The LHS is then flat mapped with a function that applies the operator to the LHS and each value in the RHS.

Comparison operators

The operators =, !=, <, <=, >, and >= are comparison operators, performing equality, inequality, less-than, less-than-or-equal, greater-than, and greater-than-or-equal checks, respectively.

In all cases, these operators evaluate to 1 if their condition is true, or 0 otherwise.

Equality and inequality are defined between any two values, whatever their types. Two elements of different types are never equal. Symbols compare by identity and tuples of the same arity structurally.

This totality is what makes a mixed collection usable: a value drawn from one carries no record of the collection it came from, so X = TIMES_TWO has to be answerable when X happens to be a number.

  1. If both arguments are elements, equality or inequality compares them directly. Ordering comparisons require two ints.
  2. If one argument is a list, and the other is an element, the comparison is performed between the element and each member of the list. The expression's value is an int: the count of comparisons that evaluated to true. Ordering in this form requires int outcomes.
  3. If both arguments are lists, equality and inequality compare the complete lists structurally. Lists of ints may additionally be compared in lexicographic order.
  4. If either argument is a pool, both arguments are converted to pools and summed. The LHS is then flat mapped with a function that applies the operator to the LHS and each value in the RHS. Additive tuple pools are summed componentwise. For non-additive pools only equality and inequality are available, and it is an error if either operand is a non-additive pool whose dimension is not one.

Function calls

Functions can be called by enclosing the words that make up their names, interleaved with expressions, between square brackets:

ArgumentTail = SafeExpr | ',' Expr.
FunctionCallItem = Word | Expr {ArgumentTail} Word.
FinalFunctionCallItem = Expr {ArgumentTail}.
FunctionCall = '[' {FunctionCallItem} [FinalFunctionCallItem] ']'.

A technical trick is needed in the grammar because arguments are allowed to follow each other with no word in between: SafeExpr is any expression that does not start with the tokens - or d. Because expressions can immediately follow each other, it would otherwise not be possible to know if a call like [f 1 -2] should be parsed as [f (1) (-2)] or as [f (1-2)]. An Expr in the function call must therefore be followed by a Word, which clearly separates the two arguments, or by a SafeExpr, which cannot be a suffix of an Expr and is thus necessarily a new argument.

A comma may also be used to separate two adjacent arguments. Because the comma has already done the separating, any Expr may follow one, including expressions starting with - or d:

output [tuple d6, d8]   \\\ Two arguments
output [tuple d6 d8]    \\\ Error: one argument, the expression d(6d8)

Commas are pure separators: they are not part of the function's identifier. [add 1, 2] and [add 1 2] resolve to the same function. A comma is only valid between two arguments, so a leading, trailing, or doubled comma is an error, as is a comma before a word.

IMPORTANT

AnyDice does not accept commas between arguments. Code using them is not portable.

To evaluate a function call, the function identifier is resolved first. A function is identified by the sequence of words and argument locations in its name, and it is an error if the function's identifier is not in scope in the current environment. Here are some examples:

function: add A:n B:n { result: A + B }
output [add 1 2]    \\\ outputs 3
output [add 1, 2]   \\\ outputs 3 - same function
output [add 1 - 2]  \\\ error: one argument, the expression 1-2

function: add A:n and B:n { result: A + B }
output [add 1 and 2]   \\\ outputs 3
output [add 1 and -2]  \\\ outputs -1

After the identifier has been resolved, function calls proceed in several steps.

Argument coercion

The actual types of the arguments are compared to the expected argument types, and each is transformed in the following way:

  • If the expected argument type is unspecified, or if the expected shape matches the actual value, the value is not transformed.
  • If the actual argument is a list:
    • If an element is requested, an additive list is summed to an element. It is an error if the list is not additive.
    • If a pool is requested, a pool is created, with equally likely outcomes from the list.
  • If the actual argument is an element:
    • If a list is requested, a singleton list is created.
    • If a pool is requested, a single-outcome pool is created.
  • If the actual argument is a pool and an element is requested, the pool is summed, creating a new pool (see the note below: pool-typed values can be passed to element-typed arguments). This is an error for a non-additive pool whose dimension is not one.

After this process, some values of type pool may still correspond to arguments where element or list shapes are requested. If this is not the case, the function is called once, and the value of the expression is the result of evaluating the function. If it is the case, evaluation proceeds as described in the next section.

Pool-based evaluation

This section applies if any pool values are passed to element- or list-typed arguments. As mentioned above, any pool corresponding to an element argument is summed to dimension 1. Then, the multiset cross product of the pools is generated. The order in which elements inside each multiset are ordered follows the "position order" global setting.

The function is then evaluated once for each value in the multiset cross product (this may be 0 times if the cross product is empty). In each invocation, the argument values are:

  • For any argument whose actual shape matches its declared shape, the value is passed untransformed.
  • For arguments where a pool was provided but a list or element was requested, that pool's multiset value in the current item of the multiset cross product iterator. An element argument receives the multiset's sole value, which may be of any element type.

Each invocation's result is converted to a pool through the standard conversions and summed, so returning a non-additive pool whose dimension is not one is an error. The final result of the function call is a pool created by iterating through every possible outcome of the intermediate pools, and summing the probability of that outcome in each of the intermediate pools, multiplied by the probability of the value that generated this intermediate pool.

For instance, suppose the following call needs to be evaluated:

function: one or N:n th die from FACES:s {
  result: d{1, N@FACES}
}
output [one or d3 th die from 3d2]

The cross-product multisets and their corresponding intermediate pools are shown below. "position order" has its default value of "highest first", so each FACES multiset is ordered from its highest value down, and N@FACES indexes into it in that order.

Multiset for NMultiset for FACESWeightOutcomeWeight for 1Weight for 2
[1][1, 1, 1]1d{1}1
[2][1, 1, 1]1d{1}1
[3][1, 1, 1]1d{1}1
[1][2, 1, 1]3d{1, 2}3/23/2
[2][2, 1, 1]3d{1}3
[3][2, 1, 1]3d{1}3
[1][2, 2, 1]3d{1, 2}3/23/2
[2][2, 2, 1]3d{1, 2}3/23/2
[3][2, 2, 1]3d{1}3
[1][2, 2, 2]1d{1, 2}1/21/2
[2][2, 2, 2]1d{1, 2}1/21/2
[3][2, 2, 2]1d{1, 2}1/21/2

The final result has outcome 1 with weight 18, and outcome 2 with weight 6. (Setting "position order" to "lowest first" reverses which value of N selects which face, but produces the same totals here, because N is uniform.)

Function evaluation

A new environment frame is created, binding the value of each expression to its corresponding argument name. Each statement in the function's body is then evaluated sequentially. If a return statement is encountered, execution returns to the caller, and the function call's value is the return statement's expression.

If no return statement is encountered, the function returns a pool of dimension 1 with no outcomes. Unlike an empty list, this contributes no outcomes to the caller rather than summing to zero:

function: sometimes nothing A:n {
  if A >= 5 { result: A }
}
output [sometimes nothing d10]  \\\ Outcomes 5 through 10 only

function: sometimes empty A:n {
  if A < 5 { result: {} }
  result: {A}
}
output [sometimes empty d10]    \\\ Outcome 0 with probability 40%, then 5 through 10

Statements

Statement = IfStatement | LoopStatement | PrintStatement | OutputStatement
          | FunctionDefinitionStatement | ResultStatement | AssignmentStatement
          | EnumDefinitionStatement | SetStatement.
Block = '{' {Statement} '}'.

Conditionals

Statements can be executed conditionally:

IfStatement = 'if' Expr Block ['else' (IfStatement | Block)].

The condition expression is evaluated. It is an error if it does not evaluate to an int. If it evaluates to 0, the else block, if any, is executed statement by statement. Otherwise, the if block is executed.

There is optional special syntax if the else block is itself an if statement.

Loops

A loop executes a body repeatedly while changing the value of a binding:

LoopStatement = 'loop' VariableName 'over' Expr Block.

The loop expression is evaluated. It is an error if it does not evaluate to a list (in particular, pools cannot be iterated over). A binding for VariableName is created in the current environment. For each value in the list, the loop variable is bound to that value, then all statements in the body are executed in succession.

If there are no values in the list, the loop does not execute. If the loop executes at least once, the loop variable remains bound to the last value after the end of the loop.

Both statements allow the user to output a value.

PrintStatement = 'print' Expr ['named' Str].
OutputStatement = 'output' Expr OutputOptions.
OutputOptions = ['named' Str ['labeled' Str {',' Str}]
               | 'labeled' Str {',' Str} ['named' Str]].

IMPORTANT

AnyDice does not support the print statement or the labeled clause.

It is an error if an output statement occurs inside a function. No such restriction exists for the print statement.

In both cases, the expression is evaluated. Any strings present as part of a named or a labeled clause are then interpolated: any pair of square brackets containing a variable name will be replaced by the named variable's value. Square brackets containing any other data will not be replaced. A syntactically valid variable name that is not defined is an error.

IMPORTANT

Eurydice is a little more explicit than AnyDice in replaced variables for lists and pools, using strings like {1, 3, 4} instead of {?}.

For an output statement, the value of the expression is converted to a pool and summed, then added to an output list. It is therefore an error to output a non-additive pool whose dimension is not one.

A displayed distribution carries one description per field, shared by all of its outcomes, so it is also an error to output a value whose outcomes disagree about a field: one holding a number in some outcomes and a symbol in others has no single description. A distribution that is entirely symbols is unaffected — that is an ordinary categorical output. Map a mixed value to one kind of value first, for instance with [sum integers in SEQ], or inspect it with print.

The optional named clause attaches a name to the distribution for display purposes. If no name is provided, the distribution is assigned the default name output n, where n is its one-based index in program execution order.

The optional labeled clause assigns display names to the fields of a tuple-valued output. It is an error to use it with a non-tuple output or to provide a number of labels different from the tuple's arity. The labels do not change the tuple's type or values. named and labeled may occur in either order.

output [tuple d6, d8] labeled "First die", "Second die"
output [tuple d6, d8] named "Joint roll" labeled "d6 result", "d8 result"

For a print statement, the value of the expression is shown to the user as soon as possible, attached to the name if present. There is no default name otherwise. print does not sum its argument, so it can display a pool of any dimension and outcome type, such as 2d{MISS, HIT} or 4d{0:2, 1:2, 2, TIMES_TWO}.

Set

The set statement allows changing global settings. It is an error for a set statement to occur inside a function.

SetStatement = 'set' '"position order"' 'to' ('"highest first"' | '"lowest first"')
             | 'set' '"explode depth"' 'to' Int
             | 'set' '"maximum function depth"' 'to' Int.

IMPORTANT

Eurydice does not allow arbitrary expressions in set statements. Arguments must be integer literals.

Assignment

Assignment statements either create a new binding in the innermost environment frame, or replace the value of an existing binding, if a variable with the same name already exists in that frame.

AssignmentStatement = VariableName ':' Expr.

Assigning to the name of a declared symbol is an error. Symbol names also cannot be reused for function parameters or loop variables.

⊕ Symbol definition

EnumDefinitionStatement = 'enum' ':' VariableName
                        | 'enum' '{' VariableName {',' VariableName} '}'.

Symbol definitions are only valid at the top level. The form with braces is syntactic sugar for a list of symbol declarations.

Function definition

Function definitions create a new function binding in the innermost environment frame, or replace the value of an existing binding, if a function with the same identifier already exists in that frame.

Function definitions are executed sequentially, like other statements. A function call can therefore only resolve a function definition that has already been executed. A later definition with the same identifier replaces the earlier binding from that point onward.

FunctionDefinitionStatement = 'function' ':' (Word | ParameterGroup)+ Block.
ParameterGroup = Parameter {',' Parameter}.
Parameter = VariableName [':' Shape].
Shape = 'n' | 's' | 'd'.

IMPORTANT

AnyDice function definitions can only occur at the top level, and do not accept commas between parameters.

The function's identifier is the sequence of words and argument positions in the name. It is valid for a function identifier to contain no words, or to contain no argument positions.

As in function calls, adjacent parameters may be separated by commas, and the commas are not part of the identifier. function: add A:n, B:n and function: add A:n B:n define the same function, and either can be called with or without commas.

Each parameter name can optionally be annotated with a shape: n for an element, s for a list, or d for a pool (dice). This constrains the shape only: the value's outcomes may be integers, symbols, or tuples, in any combination a collection can hold. Specifying a shape causes the usual argument coercion and pool-based evaluation.

An s-shaped parameter accepts a pool of any dimension, and the function is evaluated once per multiset. An n-shaped parameter sums the pool, so passing a non-additive pool whose dimension is not one is an error.

Return from function

The result statement allows returning a value from a function. It is an error for result to appear outside a function.

ResultStatement = 'result' ':' Expr.

Control flow immediately returns to the current function's caller.

Built-in functions

The following functions are available in the top-level environment frame at the beginning of a program:

[absolute N:n]

Returns the absolute value of N.

[choose FIRST:d if CONDITION:n else SECOND:d]

Returns FIRST when CONDITION is nonzero, and SECOND when it is zero.

As with other n-shaped function parameters, a pool passed as CONDITION causes the function to be evaluated for every possible value of the summed pool. The resulting distributions are combined according to the probabilities of those values.

For example, this selects d6 three quarters of the time and 2d6 one quarter of the time:

output [choose d6 if d{0, 1:3} else 2d6]

More generally, distributions with nonnegative integer weights FIRST_WEIGHT and SECOND_WEIGHT can be mixed with:

output [choose FIRST if d{0:SECOND_WEIGHT, 1:FIRST_WEIGHT} else SECOND]

IMPORTANT

The [choose FIRST:d if CONDITION:n else SECOND:d] function is not available in AnyDice. For compatible code, it can be defined as:

function: choose FIRST:d if CONDITION:n else SECOND:d {
  if CONDITION { result: FIRST }
  result: SECOND
}

[SEQ:s contains N:n]

Returns 1 if SEQ contains N, or 0 otherwise.

![IMPORTANT] If SEQ is a pool, Eurydice does not perform multiset enumeration and uses a Icepool instead. AnyDice performs multiset enumeration, and can be much slower.

[count NEEDLES:s in HAYSTACK:s]

Counts the number of occurrences of each element of NEEDLES in HAYSTACK. Returns the sum of these counts.

![IMPORTANT] If HAYSTACK is a pool, Eurydice does not perform multiset enumeration and uses Icepool instead. AnyDice performs multiset enumeration, and can be much slower.

Examples:

output [count {1, 2} in {3, 4}]  \ Outputs 0 \
output [count {1, 1, 2} in {1, 2, 2, 3}]  \ Outputs 4 \
output [count {2, 4, 6} in 2d6]  \ Outputs d{0:9, 1:18, 2:9} \
output [count {4..6, 6} in d6]  \ Outputs d{0:3, 1:2, 2} \
output [count {HIT} in 8d{MISS, HIT, CRITICALHIT}]  \ Counts hits across eight dice \
output [count {TIMES_TWO} in 4d{0:2, 1:2, 2, TIMES_TWO}]  \ Counts one face of a mixed die \

[field INDEX:n of TUPLE:n]

Returns the field at the one-based INDEX in TUPLE. INDEX must be an int, TUPLE must be a tuple, and an out-of-bounds index is an error.

If TUPLE is a pool, normal pool-based evaluation projects the field from every tuple outcome while preserving its probabilities.

[N:n is integer]

Returns 1 if N is an int, or 0 if it is a symbol or a tuple. This is the only way to tell the numeric outcomes of a mixed collection apart from the rest; symbols are told apart from each other with =.

As with any n-shaped parameter, a pool argument is summed and the function is evaluated for each of its outcomes, so [POOL is integer] reports the distribution of a single die's kind and is an error for a mixed pool of dimension other than one.

enum { TIMES_TWO }
output [d{0:2, 1:2, 2, TIMES_TWO} is integer]  \ Outputs d{0:1, 1:5} \

[sum integers in SEQ:s]

Returns the sum of the int outcomes of SEQ, ignoring outcomes that are not ints. Unlike summing SEQ itself, this is defined for a mixed collection of any dimension.

If SEQ is a pool, the result is the distribution of that sum across all of its dice.

enum { TIMES_TWO }
output [sum integers in {1, TIMES_TWO, 2}]  \ Outputs 3 \
output [sum integers in d{0:2, 1:2, 2, TIMES_TWO}]  \ Outputs d{0:3, 1:2, 2} \

![IMPORTANT] Eurydice does not perform multiset enumeration for a pool argument: the sum is additive over the dice, so each die's outcomes are mapped and the results summed.

[count integers in SEQ:s]

Returns how many outcomes of SEQ are ints. As with [sum integers in SEQ], this is defined for a mixed collection of any dimension, and a pool argument gives the distribution of that count across its dice.

enum { TIMES_TWO }
output [count integers in {1, TIMES_TWO, 2}]  \ Outputs 2 \
output [count integers in 2d{1, TIMES_TWO}]  \ Outputs d{0:1, 1:2, 2:1} \

[explode POOL:d]

This transforms a die to match an explosion rule: if the die rolls its highest face value, that value is kept and the die is rerolled.

The maximum number of rerolls is controlled by the "explode depth" global setting.

If the argument is a pool with dimension >= 1, it is summed first. The return value's dimension is always 1, or 0 if the input pool has dimension 0.

output [explode d{}]  \ Outputs d{} \

\ Outputs d{1:9, 2:9, 4:3, 5:3, 7, 8, 9} \
output [explode d{1, 2, 3}]

Here's a worked example for [explode 2d2] with explode depth set to 1:

  1. The pool is summed to d{2, 3:2, 4}
  2. The outcome 4 is mapped to d{2+4, (3+4):2, 4+4}, which is the same as the summed die with each value incremented by its largest value. This is equivalent to keeping the 4 and adding the original die again. Other outcomes are left as-is.
  3. The final probabilities for each value are:
    • 2: 1/4
    • 3: 1/2
    • 4 and 5: 0
    • 6: 1/4 (probability of rolling an initial 4) * 1/4 (probability of rolling a 2) = 1/16
    • 7: 1/4 (initial) * 1/2 (reroll) = 1/8
    • 8: 1/16 (same as 6)
  4. This is equivalent to d{2: 4, 3: 8, 6, 7: 2, 8}

[explode POOL:d on COND:s]

This is similar to [explode POOL:d], but instead of exploding on the highest face value, it explodes on any value contained in the COND list.

The maximum number of rerolls is controlled by the "explode depth" global setting.

output [explode d6 on {1, 6}]  \ Explodes on both 1s and 6s \
output [explode d{1, 2, 3} on {2, 3}]  \ Explodes on 2s and 3s \

[reroll POOL:d]

This transforms a die to match a reroll rule: if the die rolls its highest face value, the die is rerolled and only the new value is kept (unlike explode, which keeps both the original and new values).

The maximum number of rerolls is controlled by the "explode depth" global setting.

output [reroll d6]  \ Rerolls on 6s, keeping only the reroll result \

[reroll POOL:d on COND:s]

This is similar to [reroll POOL:d], but instead of rerolling on the highest face value, it rerolls on any value contained in the COND list.

The maximum number of rerolls is controlled by the "explode depth" global setting.

output [reroll d6 on {1, 6}]  \ Rerolls on both 1s and 6s \
output [reroll d{1, 2, 3, 4} on {1, 4}]  \ Rerolls on 1s and 4s \

[highest COUNT:n of POOL:d], [lowest COUNT:n of POOL:d], [middle COUNT:n of POOL:d]

These functions return pools of dimension 1 whose distribution is the sum of the highest COUNT, lowest COUNT, or middle COUNT values in the possible outcomes of POOL.

[middle COUNT:n of POOL:d] uses zero-based indices from floor((length - COUNT) / 2), inclusive, through floor((length - COUNT) / 2) + COUNT, exclusive. For instance, [middle 2 of 5d2] will return a pool corresponding to the 2nd and 3rd lowest dice, excluding the 1st, 4th and 5th.

[highest of FIRST:n and SECOND:n], [lowest of FIRST:n and SECOND:n]

These functions return the max and min of their two arguments, respectively.

[maximum of POOL:d]

Sums POOL, then returns its largest possible outcome as an int.

[reverse SEQUENCE:s]

Returns SEQUENCE in reverse order.

[sort SEQUENCE:s]

This function returns a list with the same elements as SEQUENCE, sorted in ascending or descending order depending on the value of the "position order" global setting.

The list must have int outcomes.

[tuple A:n B:n], [tuple A:n B:n C:n], [tuple A:n B:n C:n D:n]

Constructs a tuple from two, three, or four Scalar fields; nested tuples are an error.

Pool-valued arguments are expanded using normal pool-based evaluation, producing a joint tuple distribution.

Acknowledgements

The Eurydice language is largely compatible with AnyDice by Jasper Flick. Thanks!

This spec is inspired by the Starlark spec.