Expand description
§xDy: Once-and-for-all dice expression compiler
xDy is an extremely fast dice expression compiler that can be used to
generate pseudorandom numbers. It is designed for use within a variety of
applications, such as role-playing games (RPGs) and simulations, but also
suits other applications that must introduce controlled randomness or
generate specific probability distributions. It is written in Rust for
maximum performance, safety, and portability.
§Overview
xDy compiles dice expressions into reusable functions that can be applied
to user-supplied pseudorandom number generators (pRNGs) to simulate dice
rolls. Functions may define formal parameters or bind values supplied
through an environment. A six-pass optimizing compiler produces efficient
code for each dice expression, so the resulting functions can be
used to generate dice rolls with minimal overhead. The generated code
targets a dedicated intermediate representation (IR) that is designed
specifically for dice expressions. An efficient evaluator interprets the IR
to produce the final result.
Beyond generating just a final tally, xDy also provides detailed
information about the individual dice rolls that contributed to the total.
So 3D6 + 1D8 might produce a total of 18, but it also tells you that
the six-sided dice produced [3, 5, 4] and the eight-sided die
produced 6.
xDy also provides analytical capabilities. xDy can compute the bounds
and probability distribution of a dice expression. For example, xDy can
determine that 3D6 + 1D8 has a minimum value of 4, has a maximum value
of 26, and puts the probability of rolling a 10 at 0.125 (27/216).
Probability distributions can be computed serially or in parallel (with the
parallel-histogram feature). For static dice expressions, xDy can
calculate the total number of outcomes without iterating through the state
space.
§Language features
xDy aims to achieve sufficient expressiveness to cover the gamut of use
cases established by popular RPG systems. The following features are
supported:
- Constants:
-1,0,1,2,3, … - Standard dice, e.g.,
xDyfor allxandy:1D6,2D8,3D10,4D17, … - Custom dice, e.g., Fudge dice:
1D[-1, 0, 1],2D[-1, 0, 1, 3, 5],3D[1, 1, 2, 3, 5, 8], … - Arithmetic
- Negation:
-1,-1D4, … - Addition:
1 + 1,1 + 1D4,1D6 + 1D8, … - Subtraction:
3 - 2,1 - 1D4,1D6 - 1D8, … - Multiplication:
2 * 3,2 * 1D4,2D6 * 1D8, … - Division:
6 / 2,6 / 1D4,1D6 / 1D8, … - Modulus:
6 % 2,6 % 1D4,1D6 % 1D8, … - Exponentiation:
2 ^ 3,2 ^ 1D4,2D6 ^ 1D8, …
- Negation:
- Grouping:
(1 + 2) * 3,1 + (2 * 3D4),(3 * 2)D5,4D(5 - 2), … - Drop lowest:
1D6 drop lowest 1,2D8 drop lowest 2, … - Drop highest:
1D6 drop highest 1,2D8 drop highest 2, … - Formal parameters:
x: 1D6 + {x},y: 1D6 + {y},x, y: {x}D{y}, … - Environmental variables:
1D6 + {x},1D6 + {y},{x}D{y}, … - Dynamic expressions:
3D(2D6),x: ({x}D3)D8, …
Environmental variables permit background state to be associated with an evaluator, while formal parameters permit dynamic state to be passed into each evaluation. Both features can be used to parameterize dice expressions and make them more flexible. Environmental variables could easily cover, for example, the attributes and skills of a character in an RPG, while formal parameters could cover the situational modifiers applied to a particular roll.
§Examples
§One-shot evaluation
Rolling one standard six-sided die:
use xdy::evaluate;
use rand::rng;
let result = evaluate("1d6", vec![], vec![], &mut rng()).unwrap();
assert!(1 <= result.result && result.result <= 6);
assert!(result.records.len() == 1);
assert!(result.records[0].results.len() == 1);
assert!(1 <= result.records[0].results[0] && result.records[0].results[0] <= 6);Rolling a variable number of dice, using an argument named x to supply the
number of dice to roll:
use xdy::evaluate;
use rand::rng;
let result = evaluate("x: {x}D6", vec![3], vec![], &mut rng()).unwrap();
assert!(3 <= result.result && result.result <= 18);
assert!(result.records.len() == 1);
assert!(result.records[0].results.len() == 3);
assert!(1 <= result.records[0].results[0] && result.records[0].results[0] <= 6);
assert!(1 <= result.records[0].results[1] && result.records[0].results[1] <= 6);
assert!(1 <= result.records[0].results[2] && result.records[0].results[2] <= 6);Rolling a variable number of dice, using an external variable named x to
supply the number of dice to roll:
use xdy::evaluate;
use rand::rng;
let result =
evaluate("{x}D6", vec![], vec![("x", 3)], &mut rng()).unwrap();
assert!(3 <= result.result && result.result <= 18);
assert!(result.records.len() == 1);
assert!(result.records[0].results.len() == 3);
assert!(1 <= result.records[0].results[0] && result.records[0].results[0] <= 6);
assert!(1 <= result.records[0].results[1] && result.records[0].results[1] <= 6);
assert!(1 <= result.records[0].results[2] && result.records[0].results[2] <= 6);Evaluating an arithmetic expression involving different types of dice:
use xdy::evaluate;
use rand::rng;
let result = evaluate("1d6 + 2d8 - 1d10", vec![], vec![], &mut rng()).unwrap();
assert!(-7 <= result.result && result.result <= 21);
assert!(result.records.len() == 3);
assert!(result.records[0].results.len() == 1);
assert!(result.records[1].results.len() == 2);
assert!(result.records[2].results.len() == 1);
assert!(1 <= result.records[0].results[0] && result.records[0].results[0] <= 6);
assert!(1 <= result.records[1].results[0] && result.records[1].results[0] <= 8);
assert!(1 <= result.records[1].results[1] && result.records[1].results[1] <= 8);
assert!(
1 <= result.records[2].results[0]
&& result.records[2].results[0] <= 10
);§Repeated evaluation
Compiling and optimizing a dice expression and evaluating it multiple times:
use xdy::{compile, Evaluator};
use rand::rng;
let function = compile("3D6")?;
let mut evaluator = Evaluator::new(function);
let results = (0..10)
.flat_map(|_| evaluator.evaluate(vec![], &mut rng()))
.collect::<Vec<_>>();
assert!(results.len() == 10);
assert!(results.iter().all(|result| 3 <= result.result && result.result <= 18));Compiling and optimizing a dice expression with formal parameters and evaluating it multiple times with different arguments:
use xdy::{compile, Evaluator};
use rand::rng;
let function = compile("x: 1D6 + {x}")?;
let mut evaluator = Evaluator::new(function);
let results = (0..10)
.flat_map(|x| evaluator.evaluate(vec![x], &mut rng()))
.collect::<Vec<_>>();
assert!(results.len() == 10);
(0..10).for_each(|i| {
let x = i as i32;
assert!(1 + x <= results[i].result && results[i].result <= 6 + x);
});Compiling and optimizing a dice expression with environmental variables and evaluating it multiple times:
use xdy::{compile, Evaluator};
use rand::rng;
let function = compile("1D6 + {x}")?;
let mut evaluator = Evaluator::new(function);
evaluator.bind("x", 3)?;
let results = (0..10)
.flat_map(|x| evaluator.evaluate(vec![], &mut rng()))
.collect::<Vec<_>>();
assert!(results.len() == 10);
assert!(
results.iter().all(|result| 4 <= result.result && result.result <= 9)
);§Performance
xDy is very fast. Consider the following dice expression, where x = 5,
y = 2, and z = 2:
x, y, z: {x}D[-1, 0, 1, 3, 5] drop lowest {y} drop highest {z}This dice expression involves argument binding, custom dice, and dropping
values: roll 5 custom dice, each with faces [-1, 0, 1, 3, 5], drop the
2 lowest results, and drop the 2 highest results, leaving only 1 die.
On a 2023 MacBook Pro, xDy compiled and optimized this expression with
mean time 22.664 µs and evaluated it with mean time 216.89 ns.
Furthermore, the serial probability distribution calculation took mean time
394.85 µs and the parallel calculation took mean time 291.46 µs.
xDy provides a six-pass optimizer that rewrites IR into more efficient
forms. The optimizer folds constant expressions, performs strength-reducing
operations, eliminates common subexpressions, eliminates dead code, and
coalesces registers. The optimizer can also reorder commutative operations
and operands to improve opportunities for constant folding and strength
reduction. The optimizer runs all passes repeatedly, in predefined order,
until a fixed point is reached. The optimizer is designed to be fast and
effective, but it can be disabled if desired.
§Safety
xDy is designed to be well-behaved for all inputs. Dice expression values
are i32 and all arithmetic operations saturate on overflow or underflow.
Neither the compiler nor evaluator should panic or cause undefined behavior,
even for invalid dice expressions and inputs, though client misuse of vector
results can lead to panics. unsafe code is limited to the tree-sitter
dependency, and pertains exclusively to foreign function interfaces (FFIs)
that are not exposed to users of the main crate.
§Cargo features
xDy provides several optional features that can be enabled or disabled in
your Cargo.toml:
parallel-histogram: Enables parallel computation of probability distributions. This feature requires therayoncrate, and is enabled by default.serde: Implements theSerializeandDeserializetraits for various types. This feature requires theserdecrate, and is enabled by default.
Modules§
- parallel
- Parallel implementation of the histogram builder
- serial
- Serial implementation of the histogram builder
Structs§
- Add
- Instruction: Add two values together, saturating on overflow.
- Bounds
- The bounds of a function, as computed by a bounds evaluator.
- Compiler
- A compiler for the dice syntax tree, capable of walking the syntax tree and generating intermediate representation (IR) code. The IR code represents the body of a single function.
- Dependency
Analyzer - Analyzes the dependencies between instructions.
- Div
- Instruction: Divide one value by another, saturating on overflow. Treat division by zero as zero.
- Drop
Highest - Instruction: Drop the highest dice from a rolling record. This simply involves marking the highest dice as dropped, so that they are not included in the sum computed by SumRollingRecord. A negative count will put back highest dice that were previously dropped. The number of dice to drop is clamped to the number of dice in the record.
- Drop
Lowest - Instruction: Drop the lowest dice from a rolling record. This simply involves marking the lowest dice as dropped, so that they are not included in the sum computed by SumRollingRecord. A negative count will put back lowest dice that were previously dropped. The number of dice to drop is clamped to the number of dice in the record.
- Evaluation
- The result of a dice expression evaluation. The result includes the final value of the expression and the rolling records of the dice subexpressions.
- Evaluation
Bounds - The bounds of a function evaluation.
- Evaluation
State - The complete machine state of a histogram builder during execution. Each state serves as a continuation of the machine, and may be used to resume execution. The machine is suspended after every range or die roll, which only occur inside RollRange, RollStandardDice, and RollCustomDice instructions.
- Evaluator
- A dice expression evaluator. The evaluator uses a client-supplied pseudo-random number to generate the results of dice rolls.
- Exp
- Instruction: Compute the exponentiation of one value by another, saturating on overflow.
- Function
- A function in the intermediate representation. This is the output of a compiler.
- Histogram
- A histogram of the outcomes of a dice expression, as a map from outcomes to
counts. Counts are
u64to allow for large numbers of outcomes, and protected from overflow only by their enormous size. - Immediate
- An immediate constant value.
- Mod
- Instruction: Compute the remainder of dividing one value by another, saturating on overflow. Treat division by zero as zero.
- Mul
- Instruction: Multiply two values together, saturating on overflow.
- Neg
- Instruction: Compute the negation of a value, saturating on overflow.
- Passes
- A set of optimization passes. An optimizer applies a set of passes to a function until a fixed point is reached.
- Program
Counter - A program counter, which references an instruction in a compiled function. As there are no control flow instructions in the dice language, the program counter is not really an addressing mode, per se, but is still useful for various tools.
- Register
Index - The index of a register in the frame’s register set.
- Return
- Instruction: Return a value from the function.
- Roll
Custom Dice - Instruction: Roll a set of custom dice.
- Roll
Range - Instruction: Roll an inclusive range.
- Roll
Standard Dice - Instruction: Roll a set of standard dice.
- Rolling
Record - A record of a roll of dice. The record includes the results of each die in
a set of dice.
RollStandardDiceandRollCustomDiceboth populate aRollingRecordin the frame’s rolling record set. - Rolling
Record Index - The index of a rolling record in the frame’s rolling record set.
- Standard
Optimizer - The standard optimizer applies the specified standard optimization passes to a function. The passes are applied in sequence, and the optimizer continues to apply passes until no further changes are made to the function.
- Sub
- Instruction: Subtract one value from another, saturating on overflow.
- SumRolling
Record - Instruction: Compute the result of a rolling record,
ignoring any dice marked as dropped by DropLowest or DropHighest.
The summation is clamped to the range of an
i32.
Enums§
- Addressing
Mode - The addressing mode for instructions. An addressing mode specifies how to access the operand of an instruction. The operand can be an immediate constant or a register. Registers are identified by their index into the frame’s register set.
- Compilation
Error - An error that may occur during the evaluation of a dice expression.
- Evaluation
Error - An error that may occur during the evaluation of a dice expression. Note that evaluation itself never causes an error, but setup may fail.
- Instruction
- An instruction in the intermediate representation of the dice language.
- Pass
- A standard optimization. Each pass applies a single optimization to a function. Each pass is represented as a single bit in a bitfield of passes. The passes are performed by the standard optimizer in definition order.
- Rolling
Record Kind - The kind of rolling record.
Traits§
- CanAllocate
- Provides the ability to allocate a new instance of the receiver. This is useful for generating new register and rolling record indices.
- CanVisit
Instructions - Provides the ability to apply a visitor.
- CanVisit
Syntax Tree - Provides the ability to apply a visitor.
- Histogram
Builder - A builder for histograms of the outcomes of dice expressions.
- Instruction
Visitor - A visitor for instructions in the intermediate representation of the dice language.
- Optimizer
- An optimizer performs a specific optimization on a function. It might be one of the standard optimization passes or some custom client-provided optimization.
- Syntax
Tree Visitor - A visitor for the syntax tree produced by the dice language parser. Each method visits a specific type of node in the syntax tree, and is responsible for visiting any interesting children of that node.
Functions§
- add
- Computes the sum of the two operands, saturating on overflow. This serves as the definition of the addition operation in the dice expression language.
- compile
- Compile an alleged dice expression into a function. Optimize the function using the standard optimizer.
- compile_
unoptimized - Compile an alleged dice expression into a function. Do not optimize the function.
- div
- Computes the quotient of the first operand divided by the second operand, saturating on overflow. Division by zero is treated as zero. This serves as the definition of the division operation in the dice expression language.
- evaluate
- Evaluate the dice expression using the given arguments, environment, and
pseudo-random number generator (
pRNG). Missing external variables default to zero during evaluation, but all parameters must be bound. Optimize the function using the standard optimizer. - evaluate_
unoptimized - Evaluate the dice expression using the given arguments, environment, and
pseudo-random number generator (
pRNG). Missing external variables default to zero during evaluation, but all parameters must be bound. Do not optimize the function before evaluation. - exp
- Raises the first operand to the power of the second operand, saturating on
overflow. Produces correct answers for base 0 or 1 even for negative
exponents, otherwise treats
x^ywheny<0as0. This serves as the definition of exponentiation in the dice expression language. - mod
- Computes the remainder of the first operand divided by the second operand, saturating on overflow. This serves as the definition of the remainder operation in the dice expression language.
- mul
- Computes the product of the two operands, saturating on overflow. This serves as the definition of the multiplication operation in the dice expression language.
- neg
- Computes the negation of the operand, saturating on overflow. This serves as the definition of the negation operation throughout the library.
- roll_
custom_ dice - Generate a custom dice roll, using the same algorithm as the
evaluator. If
count≤0, then no dice are rolled. Iffacesis empty, then the result of each die is0. - roll_
range - Generate a value within the specified range, using the same algorithm as
the evaluator. If the range is empty (i.e.,
end<start), then the result is0. - roll_
standard_ dice - Generate a standard dice roll, using the same algorithm as the
evaluator. If
count≤0, then no dice are rolled. Iffaces≤0, then the result of each die is0. - sub
- Computes the difference of the two operands, saturating on overflow. This serves as the definition of the subtraction operation in the dice expression language.