Documentation
Language Basics

Language basics

Quint mostly resembles a functional programming language, but it has a few key differences that are highlighted on this page.

  • Prime operator (')
  • Non-determinism (nondet)
  • Modes (pure def, pure val, def, val, nondet, action, temporal, run)
  • No recursion

The high-level reasons for this and any other differences are two:

  • Quint is used to define state machines and properties, not regular executable code
  • Quint uses the same logic as TLA+ and can be translated into SMT-constraints by Apalache, which is a very powerful thing and we have to constrain the language in some ways to enable it.

Prime operator

Quint uses the prime operator (') to define transitions of the state machine. It can be read as "in the next state", that is,

x'

is "x in the next state" and

x' = x + 1

is "x in the next state is equal to x (in the present state) plus one".

Although in theory (and in TLA+) you could apply the prime operator to any expression, in Quint it has to always come in the format <var>' = <expr> which is called assignment. This prevents us from writing things that are really hard to grasp and potentially not possible to compute with the available tools.

Quint's tooling is equipped to report several different errors if you use ' in forbidden ways. It won't let you assign a value to a variable more than once in the same transition, or forget to update a variable in some branches of transitions.

Assignments have no order

This assignments are not done as in an imperative language. They are, in fact, only defining a set of state transitions. Consider the example:

all {
  x' = 1,
  y' = x,
}

This could be read as "x in the next state is equal to 1 and y in the next state is equal to x in the current state". Someone used to imperative programming assignments will likely expect y to be 1 in the next state, but this is not the case here.

init and step

Actions named init and step are special as they define the state machine. Action init defines the initial states and step defines the transitions (you can use different names and specify the on the CLI, but init and step are the defaults). We use the prime operator to define them. Remember this example from Quint basics:

var x: int
 
action init = x' = 1
 
action step = x' = x + 1

You can see init and step as the entrypoint for the model, like the main function is the entrypoint for many programming language applications. All other defintions don't matter for the model, unless they are referred to by init or step.

Non-determinism

If only a single path was possible in our model, it would be easy to table test it manually and Quint tools wouldn't be necessary. In the reality, most systems involve many possibilities through non-deterministic factors: user's input, random factors, processes that can execute in different orders, failures that may or may not happen, etc. In Quint models, we express all of these possibilities using any and oneOf.

For example, there's non-determinism when a user get's to decide if they want to deposit or withdraw, so we use any:

action step = any {
  deposit,
  withdraw,
}

Also, this user can pick the amount they want to deposit or withdraw, which therefore is also non-deterministic:

action step = {
  nondet amount = 1.to(100).oneOf()
  any {
    deposit(amount),
    withdraw(amount),
  }
}

Modes

Most of Quint code is just functional programming, but a bit of it involves non-determinism and state machines. In order to avoid mixing those up and increasing complexity, Quint provides different modes, which help separate specifications into layers: some of them purely functional, some of them which interact with state variables, etc.

Definitions prefixed with pure are just like functional programming functions: given the same input (parameters), they will always return the same output. If the pure modifier is not present, they can read state variables which can change over time and, therefore, they may return different outputs given the same inputs.

pure val PROCESSES = ["p1", "p2", "p3"]
 
pure def plus_four(p) = p + 4

Definitions that don't take parameters can be of mode val (or pure val), while definitions that take parameters need to be def (or pure def).

var x: int
 
val process_at_x = PROCESSES[x]
 
def plus_x(p) = p + x

Definitions that use the prime operator (') can only be of mode action. Actions are defintions that "modify" the state.

action increment = x' = x + 1
 
action reset = x' = 0

Non-determinisc definitions are special ones: you can use them only inside actions, before some assignment(s), in order to pick a value non-determintically.

action increase = {
  nondet amount = 1.to(5).oneOf()
  x' = x + amount
}

What is non-deterministically? In theory, we are defining a bunch of transitions: 0 -> 1, 0 -> 2, ..., 0 -> 5, 1 -> 2, ... In practice: The random simulator will pick a random one and continue, while the model checkers will branch out all possibilities and examine each one of them.

No recursion

Quint doesn't support recursive operators, mutually recursive operators, nor recursive type definitions. Formal verification and recursion have complicated interactions, and on the context of Quint and the available tools, all recursion that it could, in theory, support, is the type of recursion that can be written using folds (a.k.a. reduces).

For example, although you can't write a recursive factorial like this

pure def factorial(n) = n * factorial(n - 1)

You can rewrite it using a fold:

pure def factorial(n) = 1.to(n).fold(1, (acc, i) => acc * i)

The lack of recursion enables powerful analysis tools, and although it can be a burden sometimes, thinking about recursive algorithms in terms of folds can be a good exercise to find problems and optimization oportunities.