Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Why this language

Vishy is a language for the logic of an application: what exists, what may be done to it, what must always hold, and who may do what. A program in it is a set of units, each owning some state and the rules about it; contracts that change a unit’s state and name what happened; flows that compose contracts into transactions across units; and scenarios and invariants that say what must be true. A compiler checks all of that, a verifier looks for the state that breaks a rule, and the same program can be turned into a stored, served application.

Two things are taken away, on purpose, and everything else in this book follows from them.

A unit cannot be reached into. No contract in one unit reads or writes another unit’s state. The only way in is through the unit’s own contracts, and the only things that span units are flows and world rules, both written where the compiler can see them. So a unit can be written and checked alone, a program splits into parts that different people or models write at the same time, and the compiler knows every point where two units touch.

A contract is known by what it declares, not by its body. A contract’s header names its outcomes: how it can succeed, how it can fail, what value it carries. Its examples say what happens in particular states. Everything a caller may rely on is in that header and those examples, and the verifier holds the body to them in states nobody wrote down. Writing the body is the smaller half of the work.

Each layer of computing that lasted took something away and had a machine underneath that made the removal free: the transistor let you stop caring about voltage, the compiler about registers. Here the removals are the two above and the machine is the verifier. Whether they are the right removals is what the programs in this repository are testing; the measurements are in the appendices, not in this foreword.

How to read this guide

One concept per chapter, in the order a person meets them. Every chapter has one program, under forty lines, and shows what the compiler prints for it. The programs are files in the repository (book/programs/), and a check, tests/check_book.sh, runs each one and compares the compiler’s answers with the ones printed here, so what you read is what the compiler does on the day the book was built. Most chapters end with one thing to try: remove a line and watch what the verifier finds.

The guide is written for a person learning the language, and for a model that will write in it. The reference is the authority on notation and is cited where a chapter leaves something out. Building an application with several writers, human or model, is a chapter of its own and a separate handbook; you do not need it to learn the language.

The shape of a program

A Vishy program is a model of a domain that the compiler can run and check. Before any syntax, hold this shape:

  • Units own facts. Each unit holds some state, rows in tables and a few scalars, and every rule about that state. Nothing outside the unit can read or change it.
  • Contracts are what may happen to a thing, and how it can refuse. Each contract names its outcomes: the ways it succeeds, the ways it fails, the value it carries. Its body says what changes.
  • Flows are the transactions that compose contracts across units: a sequence of calls that either all happen or none do. They are the only place two units meet, and a fact one unit needs from another travels through a flow as a value.
  • Views are the reads: an expression over the state, with the rule of who may see it inside the expression.
  • Rules are invariants: what must hold inside one unit after every call, and what must hold across units after every flow.
  • Nothing reaches into anything else, and the machine checks the whole: examples, scenarios, sampled calls from random states, walks of the flows, every rule after every step.

If you have built software with domain-driven design, these are the names you know with the boundaries enforced: a unit is an aggregate, a contract a command with its invariant, a flow an application service, a view a read model, emitted events and the outbox the domain events, a migration a migration. The difference is that here the aggregate boundary is a compile error, not a convention: a contract that reads another unit does not compile. The guide uses those names where they fit.

If you come from elsewhere, three short pages map the shape onto what you know, each with the first program you would write: SQL, Elm and Redux, and domain-driven design in more detail. Readers of formal methods need no page: scenarios are traces, invariants are invariants, and the sampled checks are small-scope checking that runs. Readers of Rust are the easiest to mislead, which is why this guide never borrows Rust’s shape; the one mapping that holds is ownership of state for ownership of memory.

The rest of the guide takes the shape one piece at a time: first everything inside one unit, then many units, then many writers, then the world outside the program.

Your first program

The compiler

The compiler is one program, vishy, built from the repository with Cargo:

cargo build

The binary is target/debug/vishy. Every command in this book is run from the repository root, and vishy in the text means that path. Three commands are all a learner needs:

commandwhat it answers
vishy check <file>Does the program make sense? Types, names, examples, and a count of what it declares.
vishy run <file>What do the scenarios do? Each step’s outcome, then each unit’s state.
vishy verify <file> [seed] [iterations]Does every rule hold? Examples, scenarios, then thousands of sampled calls from random states.

The smallest program

Other languages begin with a program that prints “hello, world”. A Vishy program prints nothing, ever: it has no output statement, no console, no strings to write. What it has is state and outcomes. A program is run by calling its contracts, and what comes back is the outcome of each call and the state afterwards. So the smallest program is a unit with a little state, two contracts, and a scenario that calls them and says what must come back:

unit Counter {
    state n: int = 0;
    contract bump() => Ok: n += 1;
    contract read() -> int => Value(n);
}
scenario hello {
    Counter.bump() => Ok;
    Counter.bump() => Ok;
    Counter.read() => Value(2);
}

Read it in words. There is one unit, Counter, which owns one number, n, starting at zero. It has two contracts. bump succeeds with the outcome Ok and adds one to n. read succeeds with the outcome Value and carries the current n with it, which is why its header says it returns an int. The scenario hello is a script: call bump twice, then read, and each line says which outcome must come back.

vishy check book/programs/hello.vish prints:

ok: 0 row(s), 0 event(s), 0 fn(s), 1 unit(s), 2 contract(s), 2 flow(s)

Two flows, though the program declares none: a scenario step that calls a contract directly is a one-call flow the compiler makes for it. Flows are the subject of a later chapter; for now, every call from outside a unit goes through one.

vishy run book/programs/hello.vish runs the scenario and prints the trace and the final state. This trace is the program’s output; there is no other:

scenario hello
  Counter.bump() => Ok
  Counter.bump() => Ok
  Counter.read() => Value(2)
  Counter = Counter { n: 2 }
3 step(s), all as expected

vishy verify book/programs/hello.vish 7 1000 runs the scenario too, then calls bump and read from sampled states, four thousand calls in all, and checks that nothing goes wrong. There are no rules in this program yet, so “nothing goes wrong” means no call failed in a way the program did not declare:

verify: 0 examples, 3 scenario steps and 4000 sampled checks passed

The two numbers after the file name are the random seed and the number of iterations. The same seed gives the same sampled states every time, so a failure can be reproduced by anyone with the same file.

The playground

sh playground/run.sh builds a small web page that runs these three commands on a program you paste in, at http://127.0.0.1:8080. The page is itself a Vishy program served the way the last chapters describe.

The answer comes back

So where is “hello, world”? In Vishy it is an answer, not a printed line. A contract can carry text as its value; a flow calls the contract; a route names the flow; and a served program answers a call with that value:

unit Greeter {
    caps storage;
    state greeted: int = 0;
    contract greet() -> string(16) => Greeting("hello, world"): greeted += 1;
}
flow greet() atomic { Greeter.greet(); }
route POST "/hello" => greet();
scenario once {
    greet() => Greeting("hello, world");
}

vishy run shows the scenario receiving the greeting:

scenario once
  greet() => Greeting("hello, world")
  Greeter = Greeter { greeted: 1 }
1 step(s), all as expected

And vishy service book/programs/greeting.vish out sqlite writes a small server that, built and started (the chapter on storage and the door shows the three commands), answers the route:

POST /hello
{"events":[],"ok":true,"outcome":"Greeting","value":"hello, world"}

ok says the flow succeeded, outcome is the name the contract answered with, and value is what it carried. That JSON line is the guide’s hello world, and the check that keeps this guide true starts that server and makes the call every time it runs.

Why there is no print statement: printing is a side effect, and the verifier cannot check what leaves the program that way. So every way out of a Vishy program goes through a declared boundary the compiler can see: the answer to a call, a view, a delivery, an effect. What a program says is always something a caller receives.

Try

Change Value(2) to Value(3) and run vishy run again. The trace stops at the step that disagreed and shows the state at that moment. That is the shape of every failure the tools report: the step or the call, what was expected, what happened, and the state.

What Vishy is not

If you have written Rust, TypeScript or Python, your hands know constructs that Vishy does not have, and the compiler will refuse them. This page is those refusals, each as a program a reader from another language would write and the compiler’s answer, so that the habits are unlearned before the guide begins. Read it twice if you are a model: every construct below was tried by a model in the experiments that shaped the language.

Vishy has two layers, and the refusals on this page are the first layer’s. The core, where applications live, is closed on purpose: every value bounded, no recursion, no unbounded loop, every rule checkable, and that is what lets the verifier know everything about a program. General computation exists, behind a declared boundary called the sealed layer: recursion, while, return, unbounded text and lists, rows that contain themselves. Vishy’s own lexer and parser are written in it today, and the compiler is heading there. So each refusal below reads “the core refuses”, never “the language cannot”; the last section shows the boundary.

What Vishy is not is a scripting language: no console, no print statement, no top-to-bottom run. A program is state and rules, and it is run by calling it.

The core has no while

unit Counter {
    state n: int = 0;
    contract bump_to(k: int(1, 10)) => Done: n := count_up(n, k);
}
fn count_up(n: int, k: int) -> int { var i = n; while i < k { i := i + 1; } i }
book/refusals/loop.vish:5:49: error: `while` belongs to a `sealed fn`; a core function's loops are bounded (`repeat n { … }`)
  fn count_up(n: int, k: int) -> int { var i = n; while i < k { i := i + 1; } i }
                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^

A core function is total: it always finishes. Loops are repeat n { … } with a bound, and most of what a loop would do is a quantifier over a table (count, all, any, where, fold), which the chapter on reading state shows. A while is written in a sealed function, below.

The core has no recursion

unit Counter {
    state n: int = 0;
    contract set(k: int(0, 10)) => Done: n := fact(k);
}
fn fact(k: int) -> int = if k <= 1 then 1 else k * fact(k - 1);
book/refusals/recursion.vish:5:4: error: `fact` calls itself; a core function is total, recursion belongs to a `sealed fn`
  fn fact(k: int) -> int = if k <= 1 then 1 else k * fact(k - 1);
     ^^^^

The core has no return

unit Counter {
    state n: int = 0;
    contract read() -> int { else => Value(n); }
}
fn pick(a: int, b: int) -> int { if a > b { return a; } b }
book/refusals/return.vish:5:45: error: `return` belongs to a `sealed fn`; a core function's block ends with its result expression
  fn pick(a: int, b: int) -> int { if a > b { return a; } b }
                                              ^^^^^^^^^

A block ends with its result. if is an expression: if a > b then a else b.

There is no assignment; there are deltas

unit Counter {
    state n: int = 0;
    contract bump() { else => Done: n = n + 1; }
}
book/refusals/mutation.vish:3:39: error: expected `:=`, `+=`, `-=`, `max=` or `min=` in delta, found `=`
      contract bump() { else => Done: n = n + 1; }
                                        ^

A contract does not run statements. It names what changes, as deltas: n += 1, n := 0, t += [row], t[k].f := e. Everything not named is unchanged, and every right-hand side reads the state before the call. = is not an operator in a delta, and there is no variable to assign to.

There is no for; there are set-valued deltas

row Item { id: id, n: int }
unit Items {
    state items: Item[8];
    contract bump_all() => Done: for x in items { items[x.id].n += 1; };
}
book/refusals/forloop.vish:4:38: error: expected `:=`, `+=`, `-=`, `max=` or `min=` in delta, found `x`
      contract bump_all() => Done: for x in items { items[x.id].n += 1; };
                                       ^

Changing every row, or every row that matches, is one delta: items.n += 1, or items.n := for x where x.n > 3 => 0. A loop that walks a table and changes rows one by one is not written in this language.

There are no structs, classes, traits or interfaces in that sense

struct Point { x: int, y: int }
unit Points {
    state p: Point;
    contract origin() => Done: p := Point { x: 0, y: 0 };
}
book/refusals/struct.vish:1:1: error: expected `unit`, `flow`, `view`, `row`, `events`, `fn`, `invariant`, `derived`, `scenario`, `route`, `deliver`, `version` or `migrate`, found `struct`
  struct Point { x: int, y: int }
  ^^^^^^

The compiler’s answer lists everything a program may declare at the top level. A record type is a row. There are no methods on rows, no inheritance, no generics of your own; the word “interface” in this guide means a program whose contracts are declared but not yet written, which is a later chapter.

A unit cannot read another unit

unit Rooms {
    state open: bool = true;
    contract close() => Closed: open := false;
}
unit Bookings {
    state count: int = 0;
    contract book() {
        case !Rooms.open => fail RoomClosed;
        else => Booked: count += 1;
    }
}
book/refusals/reach.vish:8:15: error: unknown name `Rooms`
          case !Rooms.open => fail RoomClosed;
                ^^^^^

Inside Bookings, the name Rooms does not exist. This is the language’s first removal, and it is not a visibility setting that could be opened: there is no pub, no import that would allow it. A fact that Bookings needs from Rooms is carried in by a flow, as a value, which the flows chapter shows.

A contract cannot call a contract

unit Rooms {
    state open: bool = true;
    contract close() => Closed: open := false;
}
unit Bookings {
    state count: int = 0;
    contract book_and_close() => Booked: count += 1, Rooms.close();
}
book/refusals/call.vish:7:65: error: expected `:=`, `+=` or `-=` in delta, found `(`
      contract book_and_close() => Booked: count += 1, Rooms.close();
                                                                  ^

A contract’s body is cases and deltas, nothing else: no calls, no effects, no clock. Composition happens in flows, which call contracts in sequence as one transaction.

What else is missing, by design

  • No floating-point numbers; money and time are integers with a scale.
  • No null; an absent value is option<T>, read with match.
  • No exceptions; a failure is a named outcome, fail Insufficient, and it changes nothing.
  • No pointers, references, borrowing or lifetimes; there is nothing to point at.
  • No input or output from the program itself; the world enters through declared effects and leaves through outcomes, events and views.
  • No user interface; a served program answers JSON at routes, and the screen is someone else’s program.

The compiler accepts an unbounded int or string, and the guide will ask you never to write one in a program that matters, for reasons the chapter on values gives.

What exists behind the boundary

The same three constructs, accepted, because they are declared sealed:

sealed row Node { v: int, kids: Node[] }
sealed fn total(n: Node) -> int = n.v + fold(k in n.kids, acc = 0: acc + total(k));
sealed fn count_to(n: int(0, 100)) -> int(0, 100) { var i = 0; while i < n { i := i + 1; } i }
examples total { (Node { v: 1, kids: [Node { v: 2, kids: [] }, Node { v: 3, kids: [] }] }) => 6; }
examples count_to { (0) => 0; (7) => 7; }
unit Counter {
    state n: int = 0;
    contract set(k: int(0, 100)) => Set: n := count_to(k);
}
scenario s { Counter.set(7) => Set; }
verify: 3 examples, 1 scenario steps and 2000 sampled checks passed

A sealed function may recurse, loop with while, return early, and take and return text and lists without a bound; a sealed row may contain itself. What the seal promises: the function is pure, it has no state and no effects, and its result is trusted only to its declared type, which the boundary checks. What it does not promise: that it finishes; so a sealed function is tested by its examples and by every call the core makes to it, under a step budget, and is never sampled on its own. The core calls it as it calls any function, and the core stays closed. The chapter on functions says the rest, and the reference (§7) is the authority.

Readers from Rust will want to map the seal onto unsafe. That is the wrong row. Three kinds of code exist in a program, and the trust the compiler gives each is different:

codewhat checks itwhat it may do
the core: units, contracts, flows, viewsthe verifier: examples, scenarios, sampled checks, every ruleapplications
sealed functionstheir examples, a step budget, the declared type at the boundarygeneral computation: recursion, loops, trees, text; pure, no unit state, no events, no effects; the result cannot corrupt the core
effects with a Rust bodynothing; trustedanything: the outside world, crates, input and output

The seal gives up totality and sampling and keeps everything else, so a sealed function is tested, not proven, and never unsafe. Vishy’s unsafe is an effect’s Rust body, where the checker trusts you completely (reference §18).

Everything Vishy does have is in the chapters that follow. When a construct you expect is missing, the answer is almost always one of: a quantifier, a delta, a flow, or an outcome.

Where the tests went

In most codebases the test code outweighs the code. This page says why that is, what Vishy does about it, and, honestly, what it does not.

Why tests outweigh code

An ordinary compiler proves types and nothing about meaning. It knows that withdraw takes a number and returns a result; it does not know that the balance may not go negative. So every fact about behaviour has to be stated somewhere else, as a hand-written test: one point in the state space per fact, and one more per bug ever found. The suite is the specification, written by enumeration, and enumeration never finishes: the test that would have caught the next bug is the one nobody thought to write.

The compiler that builds Vishy has this disease in the mild form of a careful project. Its source is 8331 lines of Rust and its checks are 3331 lines of scripts and programs, two fifths as much again, and every one of those checks is a point somebody chose. That is one honest reason the roadmap ends at a compiler written in Vishy.

What Vishy does about it

The specification is not reduced. It is moved, from the place where it decays into the place the compiler reads it.

  • An example is one artifact with three jobs. { balance: 100 } (200) => Insufficient; is the writer’s specification of the contract, the checker’s acceptance test of the body, and the rule’s documentation, in one line that the compiler runs. In a suite those are three files that drift apart.
  • The number of examples is fixed by the interface, not by fear. Every declared outcome needs one example that produces it, and the checker refuses an interface without. Nobody decides how many tests are enough; the outcomes decide.
  • The sampled checks generate what a suite approximates by hand. A thousand random states and arguments per contract, from random and from reached states, with every rule checked after each call, is the test nobody could enumerate. An invariant is a test over every state the verifier reaches, written once.
  • Scenarios are the behavioural tests that remain, and they are few, because they say what the product does, not what each function returns.

On the tracker, an issue tracker of nine units, the interface is 1123 lines, of which 357 are inside examples and scenario blocks: about a third. The other two thirds are the rows, the contract headers with their outcomes and rules, and the flows: the specification’s other half, which a suite does not hold at all.

The measurement

Twelve bugs were planted by hand in the tracker’s contract bodies, each a realistic writer’s mistake: a bound off by one, a dropped guard, a flipped comparison, a status forgotten in a condition (experiments/research/mutation-study.md). All twelve were caught. Ten of them died deterministically, independent of how much the verifier sampled: nine on an example the interface author had written, one on the checker’s rule that a declared outcome must be produced by some case. One died on a scenario. One needed the sampler to reach a particular world, and did at 3,000 iterations but not at 300. The specification in the interface is what killed, not the size of the sample.

The same tracker was built the conventional way by the same cheap writers from the same brief (apps/COMPARISON.md). That arm’s verification is 151 scripted steps, and nothing else: no rule checked in any state those steps do not visit.

What no language can do

What no language can do is remove the need to say what the program should do. The ones that claim to are hiding the tests somewhere. Vishy asks for the saying up front, in the interface, in a form the compiler can run against states nobody wrote down; the cost is that the interface has to be right, which is the subject of the authoring guide in the appendices.

State, contracts and outcomes

A unit is a piece of state and the only operations that may change it. This chapter is about those three words: state, the contracts that change it, and the outcomes a contract names.

unit Counter {
    state n: int = 0;
    state bumps: int = 0;
    contract bump() => Bumped: n += 1, bumps += 1;
    contract add(k: int(1, 10)) -> int {
        else => Added(n + k): n += k, bumps += 1;
    }
    contract reset() => Reset: n := 0;
    contract read() -> int => Value(n);
}
scenario a_morning {
    Counter.bump() => Bumped;
    Counter.add(5) => Added(6);
    Counter.reset() => Reset;
    Counter.read() => Value(0);
}

State

state n: int = 0; declares a field the unit owns, its type, and its starting value. A unit may have several fields; here n is the count and bumps counts how many times it was changed. Nothing outside the unit can read or write either. That sentence is the first of the language’s two removals, and it holds for every unit in every program.

Contracts

A contract is the unit’s operation. It has a header, add(k: int(1, 10)) -> int, and a body of cases. A case is a guard, an outcome, and the deltas that follow the colon: what changes. else is the case with no guard. When a contract has one case and no guard, the body collapses to a single line: contract bump() => Bumped: n += 1, bumps += 1; is the same as writing an else case in braces.

The deltas name exactly what changes and nothing else changes: n += 1, bumps += 1 adds one to both fields; n := 0 sets one field and leaves bumps alone. Every right-hand side reads the state as it was before the call, so Added(n + k) carries the new total while n += k writes it, and the two agree.

The parameter type int(1, 10) is a bound: add accepts one to ten and nothing else. A bound is enforced in three places at once: an example with an out-of-range argument is refused by the checker, the verifier only samples arguments inside it, and a served application refuses a request outside it. Bounds are how a rule about a quantity is written once.

Outcomes

Every case names an outcome: Bumped, Added, Reset, Value. An outcome is the contract’s answer, and the name is part of the program’s meaning: a caller switches on it, a scenario asserts it, a client library gets a type for it. Outcomes are of two kinds. A plain outcome, Bumped, says the case applied and the deltas happened. A carried outcome, Added(n + k) or Value(n), brings a value with it; the contract’s header then says the value’s type after ->. The next chapter adds the third kind, a failure, which changes nothing.

Two rules about names. The same outcome name means the same thing everywhere in a program: if Added carries an int here, no other contract may use Added as a failure or with another type. And the names are yours: the language reserves only the four outcomes it produces itself, which the chapter on rows introduces.

The scenario

scenario a_morning calls the contracts in order and states each answer. vishy run executes it:

scenario a_morning
  Counter.bump() => Bumped
  Counter.add(5) => Added(6)
  Counter.reset() => Reset
  Counter.read() => Value(0)
  Counter = Counter { n: 0, bumps: 2 }
4 step(s), all as expected

The final state shows both fields: n is back to zero after reset, and bumps is two, because reset did not name it in its deltas and so did not change it.

What the verifier does with no rules

verify: 0 examples, 4 scenario steps and 8000 sampled checks passed

Eight thousand sampled calls across the four contracts, from random states and from states reached by earlier calls: a random n, a random bumps, a random k inside its bound. Nothing in this program says what must be true, so the verifier can only confirm that every call ended in a declared outcome. The next chapter gives it something to look for.

Try

Change add’s parameter to k: int and its line to Added(n + k): n += k;. The program still checks, runs and verifies. Now the count can go down as well as up, and nothing in the program says it may not. Keep that in mind for the next chapter, where a rule is written and the verifier finds the call that breaks it.

Rules, and the state that breaks them

The last chapter’s counter had no rules, so the verifier had nothing to look for. This chapter adds the two ways a program says what must be true: a failure outcome, which is a rule about one call, and an invariant, which is a rule about the state at every moment. Then it deletes one and watches the verifier find the state that the missing rule would have prevented.

unit Account {
    state balance: money(2) = 0;
    contract deposit(amount: money(2)) {
        case amount <= 0 => fail BadAmount;
        else => Deposited: balance += amount;
    }
    contract withdraw(amount: money(2)) {
        case amount <= 0 => fail BadAmount;
        case amount > balance => fail Insufficient;
        else => Withdrawn: balance -= amount;
    }
    invariant balance >= 0;
}
scenario a_day {
    Account.deposit(10000) => Deposited;
    Account.withdraw(2500) => Withdrawn;
    Account.withdraw(9000) => Insufficient;
    Account.withdraw(0) => BadAmount;
}

Failure outcomes

withdraw has three cases. The first two are failures: fail BadAmount when the amount is not positive, fail Insufficient when it is more than the balance. A failure is an outcome like any other, with a name a caller can switch on, and one property that makes it different: a failing case changes nothing. It has no deltas and may not have any. So a contract’s failures are the rules about when it may be called, stated as answers rather than as errors.

Cases are tried in order and the first guard that holds decides, so the order is part of the rule: an amount of zero is BadAmount even when the balance is zero too, because that case comes first.

The invariant

invariant balance >= 0; is a rule the unit promises after every call to any of its contracts. It is not checked by the contracts; it is checked on them. A unit has one invariant, and several conditions are joined with &&.

Money

money(2) is an amount in minor units with two decimals: 10000 is a hundred units. Money adds to money of the same scale and multiplies by an integer, and the checker refuses to mix it with a plain int or with money of another scale. It is not a decimal type with rounding; it is an integer that knows what it counts.

Running it

scenario a_day
  Account.deposit(10000) => Deposited
  Account.withdraw(2500) => Withdrawn
  Account.withdraw(9000) => Insufficient
  Account.withdraw(0) => BadAmount
  Account = Account { balance: 7500 }
4 step(s), all as expected
verify: 0 examples, 4 scenario steps and 4000 sampled checks passed

Four scenario steps, and then four thousand sampled calls across the two contracts, from random states and from states reached by earlier calls, with a random amount each time, checking the invariant after every one. Nothing was found, because the two guards keep the balance from going below zero.

Deleting a rule

Remove the Insufficient case and its scenario step (the program is book/failures/account-noguard.vish). The checker still accepts the program; the scenario still passes. vishy verify does not:

verify FAILED: 2 failure(s):
[1] sampled check failed: Account.withdraw: Account.withdraw: invariant violated after case 2: state Account { balance: -18 } args (60,)
  (state Account { balance: 42 } args (60,))
[2] flow Account.withdraw: Account.withdraw: invariant violated after case 2: state Account { balance: -12 } args (25,)
  (world World { account: Account { balance: 13 },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (25,))

Read the first failure. The verifier started from a state with a balance of 42, called withdraw with 60, and the invariant failed after case 2 with a balance of minus 18. It shows the state before the call, the arguments, and the state after; anyone can reproduce it with the same seed. That is the whole method of the language, and it is the sentence to keep: say the rule, and let the verifier look for the state that breaks it.

The second failure is the same defect found a second way, through the one-call flow the scenario’s calls go through. Both name the contract, so the person or the model who owns that unit knows where to look.

What the verifier did not do

It did not prove that the balance can never be negative. It sampled a thousand states and a thousand arguments and found none that broke the rule in the correct program, and one in the broken one within the first few. A rule that only breaks in a state the sampler never draws is missed; the chapter on what the verifier cannot see says how deep the sampling goes and what stays outside it.

Try

Put the Insufficient case back and change the invariant to balance >= 100. The verifier fails on the very first sampled call, a deposit from the initial state: the balance starts at zero, which already breaks the new rule, and a failed case changes nothing, so the rule is still broken after it. A rule the program cannot keep from its first state is found before any guard is tested.

Examples

A scenario says what a sequence of calls does from the first state. An example says what one call does from a state you choose. Examples live inside the contract they test, and they are the contract’s acceptance tests: the person who writes the header writes them, and the body is not done until every one of them passes.

row Book { id: id, title: string(32), copies: int(0, 9) }
unit Library {
    state books: Book[8];
    state loans: int = 0;
    contract shelve(book: id, title: string(32), copies: int(1, 9)) {
        else => Shelved: books += [Book { id: book, title: title, copies: copies }];
        examples {
            {} (1, "Dune", 2) => Shelved { books: [Book { id: 1, title: "Dune", copies: 2 }] };
        }
    }
    contract lend(book: id) -> int {
        case books[book].copies == 0 => fail NoCopy;
        else => Lent(books[book].copies - 1): books[book].copies -= 1, loans += 1;
        examples {
            { books: [Book { id: 1, title: "Dune", copies: 2 }] } (1) => Lent(1) { books: [Book { id: 1, title: "Dune", copies: 1 }], loans: 1 };
            { books: [Book { id: 1, title: "Dune", copies: 1 }], loans: 4 } (1) => Lent(0) { books: [Book { id: 1, title: "Dune", copies: 0 }], loans: 5 };
            { books: [Book { id: 1, title: "Dune", copies: 0 }] } (1) => NoCopy;
        }
    }
    contract on_loan() -> int {
        else => OnLoan(loans);
        examples {
            { loans: 3 } () => OnLoan(3);
        }
    }
}
scenario one_copy {
    Library.shelve(1, "Dune", 1) => Shelved;
    Library.lend(1) => Lent(0);
    Library.lend(1) => NoCopy;
    Library.on_loan() => OnLoan(1);
}

The shape of an example

An example is one line: a state before the call, the arguments, the outcome that must come back, and, for a success, what the state must be after.

{ books: [Book { id: 1, title: "Dune", copies: 2 }] } (1) => Lent(1) { books: [...], loans: 1 };

Read it left to right: from a library holding two copies of one book, lend(1) must answer Lent carrying 1, the copies left, and afterwards the book has one copy and one loan is counted. Each example starts fresh from its own before-state; examples do not run in sequence and do not see each other.

The state literal

The braces before the arguments are a state literal: it names fields of the unit and gives them values. A field it does not name has its initial value. So { loans: 3 } is a library with no books and three loans, and {} is the unit as it starts. You write only the part of the state the example is about, and the rest is the empty library every reader already knows.

A field that is a table is given whole: books: [Book { … }] is the entire table, one row.

The after-state says what changed

The braces after the outcome are the after-state, and they are read differently: a field the after-state does not name is unchanged by the call. The second lend example starts with four loans and ends with five, and it must say so, loans: 5; if the call had not touched loans, the example would leave it out.

This is the second of the language’s two removals seen from the test side. A contract changes nothing its deltas do not name, and an example says nothing about what did not change. So an example with no after-state at all, like on_loan’s, is a claim that the call changed nothing. The verifier holds you to that claim.

A carried value

Lent(1) and OnLoan(3) give the carried value the call must produce. The value is compared exactly, so an example pins the formula down at one point: Lent(books[book].copies - 1) from two copies must be 1, computed on the state before the call.

A failure

{ books: [Book { id: 1, title: "Dune", copies: 0 }] } (1) => NoCopy; is a failure example. It has no after-state, because a failure changes nothing and so there is nothing to say about the state afterwards.

What the tools do with them

vishy verify runs every example before it samples anything:

verify: 5 examples, 4 scenario steps and 6000 sampled checks passed

Five examples: one for shelve, three for lend, one for on_loan. The scenario then runs from the empty library:

scenario one_copy
  Library.shelve(1, "Dune", 1) => Shelved
  Library.lend(1) => Lent(0)
  Library.lend(1) => NoCopy
  Library.on_loan() => OnLoan(1)
  Library = Library { books: [Book { id: 1, title: "Dune", copies: 0 }], loans: 1 }
4 step(s), all as expected

The checker reads the examples too, before anything runs. An example that names an outcome the contract does not have is refused, so a misspelled outcome is found at the line that misspells it (book/refusals/examples-outcome.vish):

book/refusals/examples-outcome.vish:9:74: error: `NoCopies` is not an outcome of `lend`
              { books: [Book { id: 1, title: "Dune", copies: 0 }] } (1) => NoCopies;
                                                                           ^^^^^^^^

An example that says too little

Here is a contract that returns a book, with an example that forgets to say what changed (book/failures/examples-silent.vish):

row Book { id: id, title: string(32), copies: int(0, 9) }
unit Library {
    state books: Book[8];
    state loans: int = 0;
    contract give_back(book: id) {
        else => Returned: books[book].copies += 1, loans -= 1;
        examples {
            { books: [Book { id: 1, title: "Dune", copies: 0 }], loans: 1 } (1) => Returned;
        }
    }
}

The checker accepts it. The verifier does not:

verify FAILED: 1 failure(s):
[1] Library.give_back example 1: the example gives no after-state, which means unchanged, but the state changed: before Library { books: [Book { id: 1, title: "Dune", copies: 0 }], loans: 1 } after Library { books: [Book { id: 1, title: "Dune", copies: 1 }], loans: 0 } (write the after-state, or `unchanged`)

The example has no after-state, so it claims that returning a book changes nothing. The call added a copy and took a loan away, and the verifier prints both states so you can see which. The fix is to write what changed: => Returned { books: [Book { id: 1, title: "Dune", copies: 1 }], loans: 0 };. A success example whose contract changed the state without the example saying so always fails this way, which is what keeps examples honest as documentation: a reader of an example sees every field the call touches.

Examples and the rest of the verifier

An example is one point. The sampled checks of the earlier chapters are thousands of points nobody wrote. The two do different work. The sampled checks look for a state that breaks a rule, and this program has no rule about what Lent carries, so they have nothing to hold that number to. The examples do: they fix the answer at the states you chose, and the verifier compares it exactly.

Try

Change the first lend example’s after-state from loans: 1 to loans: 2. The program still checks; vishy verify fails on that example and prints the state it expected next to the state it got, with loans: 1 in the second. Then delete , loans: 1 from the same after-state entirely: the example now claims the call left loans at its value before the call, zero, and the verifier reports the same kind of failure, expecting loans: 0 and getting loans: 1.

Values: numbers, ids, text, money, time

Every field, parameter and carried value has a type, and the types are few. Each one says what the value is for, and the checker refuses the operations that would mix one purpose with another. This chapter goes through them with one program, a small club.

enum Tier { basic, silver, gold }
row Member { id: id, name: string(16), tier: Tier, joined: instant, paid: money(2), sponsor: option<id> }
unit Club {
    state members: Member[8];
    state open: bool = true;
    state prices: map<Tier, money(2)> = map(Tier.basic => 1000, Tier.silver => 2500, Tier.gold => 5000);
    contract join(member: id, name: string(16), now: instant, sponsor: option<id>) {
        case !open => fail Closed;
        else => Joined: members += [Member { id: member, name: to_upper(trim(name)), tier: Tier.basic, joined: now, paid: 0, sponsor: sponsor }];
        examples {
            {} (7, " ada ", 1000, none) => Joined { members: [Member { id: 7, name: "ADA", tier: Tier.basic, joined: 1000, paid: 0, sponsor: none }] };
            { open: false } (7, "ada", 1000, some(3)) => Closed;
        }
    }
    contract promote(member: id, tier: Tier) {
        case tier <= members[member].tier => fail NotHigher;
        else => Promoted: members[member].tier := tier;
    }
    contract pay(member: id, months: int(1, 12)) -> money(2) {
        else => Charged(get_or(prices, members[member].tier, 0) * months): members[member].paid += get_or(prices, members[member].tier, 0) * months;
    }
    contract renews(member: id) -> instant => Renews(members[member].joined + days(365));
    contract card(member: id) -> string(32) => Card(concat(members[member].name, concat(" #", int_to_string(as_int(member)))));
    contract sponsored(member: id) -> bool => Sponsored(match members[member].sponsor { some s => s != member, none => false });
    contract shut() => Shut: open := false;
}
scenario a_member {
    Club.join(7, " ada ", 1000, some(3)) => Joined;
    Club.promote(7, Tier.basic) => NotHigher;
    Club.promote(7, Tier.silver) => Promoted;
    Club.pay(7, 3) => Charged(7500);
    Club.renews(7) => Renews(31536001000);
    Club.card(7) => Card("ADA #7");
    Club.sponsored(7) => Sponsored(true);
    Club.shut() => Shut;
    Club.join(8, "bo", 2000, none) => Closed;
}
scenario a_member
  Club.join(7, " ada ", 1000, Some(3)) => Joined
  Club.promote(7, basic) => NotHigher
  Club.promote(7, silver) => Promoted
  Club.pay(7, 3) => Charged(7500)
  Club.renews(7) => Renews(31536001000)
  Club.card(7) => Card("ADA #7")
  Club.sponsored(7) => Sponsored(true)
  Club.shut() => Shut
  Club.join(8, "bo", 2000, None) => Closed
  Club = Club { members: [Member { id: 7, name: "ADA", tier: silver, joined: 1000, paid: 7500, sponsor: Some(3) }], open: false, prices: {basic: 1000, silver: 2500, gold: 5000} }
9 step(s), all as expected

bool and int

open: bool is true or false, read with !, && and ||. int is a whole number with no fixed range.

int(1, 12) is an int with a bound: pay accepts one to twelve months and nothing else. The chapter on contracts named the three places a bound holds: the checker refuses an example or a scenario step outside it, the verifier samples only inside it, and a served program refuses a request outside it. Here is the first of the three (book/refusals/values-bound.vish):

book/refusals/values-bound.vish:6:17: error: example argument for `guests` is outside its type
              {} (6) => Visited { visits: 6 };
                  ^

A bound is the cheapest rule in the language: one line in a header, and a whole class of arguments is gone from every tool at once. Fields take bounds too, like copies: int(0, 9) in the last chapter’s books. When a limit on a field is one that every call must keep, say it in the unit’s invariant as well, because the invariant is what the verifier checks after each call.

id

id is a key: the name of a row. You may write an int literal where an id is expected, join(7, …), but an id is not a number. It has no arithmetic that gives an id back, so the classic “next id is the last plus one” does not type (book/refusals/values-id.vish):

book/refusals/values-id.vish:5:73: error: field `id` expects id, got int
      contract join(name: string(16)) => Joined: members += [Member { id: last + 1, name: name }], last := last + 1;
                                                                          ^^^^^^^^

last + 1 is an int, and an int is not an id. When you do need the number, as_int(member) gives it, as card does to print the member’s number, and as_id(n) turns an int back into an id. Where new ids come from, without arithmetic, is the last chapter of this part.

Text

string(16) is text of at most sixteen bytes, written in double quotes with the escapes \", \n, \t and \\. The builtins cover what a contract needs to normalise and assemble text: trim and to_upper in join, concat and int_to_string in card, and len, to_lower, starts_with, ends_with, contains, find, substr, split, join, replace and string_to_int besides.

Money

money(2) is an amount in minor units with two decimals: the gold price 5000 is fifty units. A money literal is a plain int of minor units, so there is no 50.00 to write. Money adds and subtracts money of the same scale and multiplies and divides by an int, which is how pay charges a price times a number of months. It never mixes with a plain number (book/refusals/values-money.vish):

book/refusals/values-money.vish:3:60: error: delta on `takings` expects money(2), got int(1,10)
      contract sell(tickets: int(1, 10)) => Sold: takings += tickets;
                                                             ^^^^^^^

Selling ten tickets does not add ten to the takings; ten tickets at a price does. The refusal is the checker asking which price.

Time

instant is a moment and duration a length of time, both in milliseconds. An instant plus a duration is an instant, which is how renews answers a year after joining: joined + days(365), and seconds, minutes and hours build the others. One instant minus another is a duration. Two instants cannot be added (book/refusals/values-instant.vish):

book/refusals/values-instant.vish:4:68: error: two instants can only be subtracted (giving a duration) or compared; instant + instant has no meaning
      contract renews(member: id, now: instant) -> instant => Renews(now + members[member].joined);
                                                                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

A contract does not read a clock. join takes now: instant as a parameter, like any other value; the last chapter of this part shows where the time comes from.

Enums

enum Tier { basic, silver, gold } is a type with three values, written Tier.gold. The variants are ordered as declared, so promote compares them: tier <= members[member].tier refuses a promotion to the same tier or a lower one, which the scenario’s second step shows. match reads one variant at a time, which the chapter on reading the state covers.

Options

option<id> is either none or some(x). sponsor is an optional member: most members join without one. You cannot use an option as the value inside it; you match it, one arm for each case: match members[member].sponsor { some s => s != member, none => false }. The name after some is the value inside, bound for that arm only.

Maps

map<Tier, money(2)> is an ordered map from tiers to prices, written map(Tier.basic => 1000, …). get_or(prices, tier, 0) reads with a default, get reads an option, put and remove_key build a new map, and has_key, keys, values and len inspect one. A map’s keys are ints, ids, strings or enums.

What the checker bought

verify: 2 examples, 9 scenario steps and 14000 sampled checks passed

The program has no rules, and it passed fourteen thousand sampled calls. The value of the types is in what did not get written: no money added to a count, no id computed from a number, no two moments summed. Each would have been a quiet wrong answer in a language that let it through; here each is a line number.

Try

Change paid: money(2) in the row to paid: money(3), as if the club had started keeping tenths of a cent. The checker refuses pay: the cell paid expects money(3) and the charge is money(2). Nothing converts between scales silently; the change has to be made where the price is.

Rows and tables

Most of what a unit owns is not a single number but a collection of records: customers, orders, bookings. This chapter is about the record, the row, and the collections a unit keeps rows in. It ends with the four outcomes the language produces on its own, which is why most contracts never test whether a key exists.

row Address { street: string(32), city: string(16) }
row Customer { id: id, name: string(16), home: Address, billing: option<Address>, tags: int[3] }
row Note { id: id, text: string(32) }
unit Customers {
    state customers: Customer[2];
    state notes: Note[4] ordered;
    state recent: id[3];
    contract add(customer: id, name: string(16), city: string(16)) {
        else => Added: customers += [Customer { id: customer, name: name, home: Address { street: "", city: city }, billing: none, tags: [] }], recent += [customer];
        examples {
            {} (1, "Ada", "Oslo") => Added { customers: [Customer { id: 1, name: "Ada", home: Address { street: "", city: "Oslo" }, billing: none, tags: [] }], recent: [1] };
        }
    }
    contract move_to(customer: id, city: string(16)) => Moved: customers[customer].home := with(customers[customer].home, city, city);
    contract bill_to(customer: id, street: string(32), city: string(16)) => Billing: customers[customer].billing := some(Address { street: street, city: city });
    contract tag(customer: id, t: int) => Tagged: customers[customer].tags := append(customers[customer].tags, t);
    contract note(customer: id, text: string(32)) => Noted: notes += [Note { id: customer, text: text }];
    contract city(customer: id) -> string(16) => City(customers[customer].home.city);
    contract room() -> int => Room(cap(customers) - len(customers));
}
scenario two_customers {
    Customers.add(1, "Ada", "Oslo") => Added;
    Customers.add(1, "Ada", "Oslo") => Exists;
    Customers.add(2, "Bo", "Rome") => Added;
    Customers.add(3, "Cy", "Lima") => Full;
    Customers.room() => Room(0);
    Customers.move_to(9, "Pisa") => Unknown;
    Customers.move_to(1, "Pisa") => Moved;
    Customers.city(1) => City("Pisa");
    Customers.bill_to(2, "Via Roma 1", "Rome") => Billing;
    Customers.tag(1, 7) => Tagged;
    Customers.note(1, "called") => Noted;
    Customers.note(1, "called again") => Noted;
}

Rows

row Customer { id: id, name: string(16), … } declares a record type: named fields, each with a type. A row is written by naming every field, Address { street: "", city: city }, and read with a dot, customers[customer].home.city.

A field may be any value from the last chapter, and three more things:

  • another row: home: Address is an address inside the customer, not a reference to one elsewhere;
  • an optional row: billing: option<Address> is either none or some(Address { … });
  • a list of scalars: tags: int[3] is a short list of numbers held in the row.

Rows nest, but a row may not contain itself, directly or through another row (book/refusals/rows-recursive.vish):

book/refusals/rows-recursive.vish:1:5: error: row `Part` contains itself (directly or through another row); rows nest but do not recurse (a `sealed row` may)
  row Part { id: id, name: string(16), parent: option<Part> }
      ^^^^

A tree of parts is a table of parts that name their parent by id. The message points at the other way out, a sealed row, which the chapter on functions covers.

Keyed tables

state customers: Customer[2]; is a keyed table: rows of Customer, found by their id field, at most two. Every row kept in a keyed table has an id: id field, and no two rows share one. customers[1] is the row with id 1, 1 in customers asks whether there is one, len(customers) counts them. Tables start empty.

The number in brackets is a bound, and it is small on purpose: it is the size the verifier works at. Sampled states here hold up to two customers, and a small table keeps every check fast.

Positional lists

state notes: Note[4] ordered; is a list: rows kept in the order they were added, found by position, not by id. Two notes may share an id, as the scenario’s two notes about customer 1 do; a list is a log, not a lookup. state recent: id[3]; is a list of plain values. A list grows with += at the end.

Nested and optional rows in a delta

A delta writes a table’s row or one field of a row, and a field inside a nested row is written by building the new nested row: move_to sets home to with(customers[customer].home, city, city), the old address with one field replaced. bill_to sets an optional row to some(Address { … }). tag replaces the list in the row with the list and one more tag, append(customers[customer].tags, t).

The four implicit outcomes

Run the scenario:

scenario two_customers
  Customers.add(1, "Ada", "Oslo") => Added
  Customers.add(1, "Ada", "Oslo") => Exists
  Customers.add(2, "Bo", "Rome") => Added
  Customers.add(3, "Cy", "Lima") => Full
  Customers.room() => Room(0)
  Customers.move_to(9, "Pisa") => Unknown
  Customers.move_to(1, "Pisa") => Moved
  Customers.city(1) => City("Pisa")
  Customers.bill_to(2, "Via Roma 1", "Rome") => Billing
  Customers.tag(1, 7) => Tagged
  Customers.note(1, "called") => Noted
  Customers.note(1, "called again") => Noted
  Customers = Customers { customers: [Customer { id: 1, name: "Ada", home: Address { street: "", city: "Pisa" }, billing: None, tags: [7] }, Customer { id: 2, name: "Bo", home: Address { street: "", city: "Rome" }, billing: Some(Address { street: "Via Roma 1", city: "Rome" }), tags: [] }], notes: [Note { id: 1, text: "called" }, Note { id: 1, text: "called again" }], recent: [1, 2] }
12 step(s), all as expected

No contract in the program has a case for a missing customer, a duplicate id or a full table. Three steps answered anyway:

  • Exists: the second add(1, …) inserted a row whose id was already present.
  • Full: the third customer did not fit in a table of two.
  • Unknown: move_to(9, …) named a customer that is not there.

These outcomes are the language’s own. A contract that uses customers[k] for an absent k answers Unknown; an insert of a present key answers Exists; an insert past the bound answers Full. Each is a failure: nothing changes. The fourth, WrongStatus, belongs to machines and has its own chapter. They are the only outcome names the language reserves, and examples and scenarios may name them like any other.

The reason is the second of the two removals. A contract says what changes; the question whether the thing to change exists is the same for every contract, so the language answers it once. You still write a case when you want a different answer, as the NoCopy case in the library did for a book with no copies left.

cap(t) and a hand-written bound

room() answers how many more customers fit: cap(customers) - len(customers). cap(customers) is the table’s declared bound, read as a value. You could write 2 - len(customers), and it would pass today; it would also be wrong the day someone changes the declaration, and only a step that happens to fill the table would notice. cap keeps the rule and the declaration one fact. The chapter on storage says what the bound means once the program is served.

verify: 1 examples, 12 scenario steps and 14000 sampled checks passed

Try

Change Customer[2] to Customer[3]. The run stops at the fourth step: it expected Full and got Added, and it prints the world at that moment with three customers in it. Change that step’s expected outcome to Added and run again: all twelve steps pass, and room() still answers Room(0), because cap(customers) followed the new bound. With a hand-written 2 - len(customers) the same step would have failed, answering Room(-1).

Deltas: saying what changes

A contract’s case ends with its deltas: the list, after the colon, of what the call changes. There is no other way for a contract to change anything, and nothing a delta does not name changes. That is the second of the language’s two removals, and this chapter is the full list of what a delta can say.

events { Restocked(item: id, qty: int) }
row Item { id: id, qty: int, price: money(2), on_sale: bool }
unit Stock {
    caps outbox;
    state items: Item[8];
    state peak: int = 0;
    state sales: int = 0;
    contract add(item: id, price: money(2)) => Added: items += [Item { id: item, qty: 0, price: price, on_sale: false }];
    contract restock(item: id, qty: int(1, 100)) {
        else => Restocked: items[item].qty += qty, peak max= items[item].qty + qty, emit Restocked(item, qty);
        examples {
            { items: [Item { id: 1, qty: 2, price: 500, on_sale: false }], peak: 4 } (1, 5)
                => Restocked { items: [Item { id: 1, qty: 7, price: 500, on_sale: false }], peak: 7 };
        }
    }
    contract sell(item: id) {
        case items[item].qty == 0 => fail SoldOut;
        else => Sold: items[item].qty -= 1, sales += 1;
    }
    contract reprice(item: id, price: money(2)) => Repriced: items[item].price := price;
    contract replace(item: id, price: money(2)) => Replaced: items[item] := Item { id: item, qty: 0, price: price, on_sale: false };
    contract drop(item: id) => Dropped: remove items[item];
    contract end_sale() => Ended: items.on_sale := false;
    contract sale_under(limit: money(2)) {
        else => OnSale: items.on_sale := for i where i.price <= limit => true;
        examples {
            { items: [Item { id: 1, qty: 3, price: 500, on_sale: false }, Item { id: 2, qty: 0, price: 900, on_sale: false }] } (600)
                => OnSale { items: [Item { id: 1, qty: 3, price: 500, on_sale: true }, Item { id: 2, qty: 0, price: 900, on_sale: false }] };
        }
    }
    contract halve_sale_prices() => Halved: items := for i where i.on_sale => with(i, price, i.price / 2);
    contract clear_empty() {
        else => Cleared: items -= where(i in items: i.qty == 0).id;
        examples {
            { items: [Item { id: 1, qty: 3, price: 500, on_sale: false }, Item { id: 2, qty: 0, price: 900, on_sale: false }], sales: 7 } ()
                => Cleared { items: [Item { id: 1, qty: 3, price: 500, on_sale: false }] };
        }
    }
}
scenario a_sale {
    Stock.add(1, 500) => Added;
    Stock.add(2, 900) => Added;
    Stock.restock(1, 5) => Restocked;
    Stock.sell(1) => Sold;
    Stock.sell(2) => SoldOut;
    Stock.reprice(9, 100) => Unknown;
    Stock.drop(9) => Dropped;
    Stock.replace(3, 700) => Replaced;
    Stock.sale_under(600) => OnSale;
    Stock.halve_sale_prices() => Halved;
    Stock.clear_empty() => Cleared;
}

Every right-hand side reads the state before the call

One rule governs every form: a delta’s expression is computed on the state as it was when the call began. In restock, items[item].qty += qty, peak max= items[item].qty + qty, the second delta reads the old quantity, so it adds qty itself to get the new one. The deltas of a case are a description of one step, not a sequence of statements.

Scalars

A scalar field takes five forms:

deltameaning
x := eset
x += e, x -= eadd, subtract (ints, money, durations)
x max= e, x min= ekeep the larger, keep the smaller

sales += 1 counts a sale. peak max= … keeps the highest stock level ever seen: it never goes down, whatever the new level is.

One row of a keyed table

deltameaning
t += [row, …]insert rows (Exists if a key is present, Full past the bound)
t[k].f := e, t[k].f += e, t[k].f -= eone field of one row
t[k] := rowput the whole row at key k
remove t[k], t -= [k, …]remove by key

reprice writes one field of a row, and the row must exist: reprice(9, 100) in the scenario answers Unknown, as the last chapter showed. Putting a whole row and removing one say what the table looks like afterwards, so they do not need the row to be there first. replace puts a whole row, every field given, at its key: replace(3, 700) finds no item 3 and inserts one. drop removes a row, and drop(9) of a row that is not there succeeds with nothing to remove. When “the row must exist” is part of the rule, write the case: case !(item in items) => fail Unknown;.

Every row at once

A column delta writes one field of every row: items.on_sale := false ends a sale on everything in stock. With for and where it writes only the rows a predicate selects, with the row bound to a name in both halves: items.on_sale := for i where i.price <= limit => true. The rows the predicate does not select keep their value, which the example of sale_under states exactly: item 1 at five hundred goes on sale, item 2 at nine hundred stays as it was.

A row replacement does the same with whole rows: items := for i where i.on_sale => with(i, price, i.price / 2) replaces each selected row with a new one, here the old row with its price halved. The ids must not change; it is an edit of rows in place, not a new table.

To remove the rows a predicate selects, subtract their ids: items -= where(i in items: i.qty == 0).id. The chapter on reading the state explains where.

A keyed table is never assigned whole. Rows enter with +=, leave with -= or remove, and change through one of the forms above, so every change to a table says which rows it touches.

Events

emit Restocked(item, qty) appends an event to the unit’s outbox, the record of what happened that the world outside may need to hear about. Events are declared at the top, events { Restocked(item: id, qty: int) }, and a unit that emits declares caps outbox. The run shows the outbox as part of the unit’s state:

scenario a_sale
  Stock.add(1, 500) => Added
  Stock.add(2, 900) => Added
  Stock.restock(1, 5) => Restocked
  Stock.sell(1) => Sold
  Stock.sell(2) => SoldOut
  Stock.reprice(9, 100) => Unknown
  Stock.drop(9) => Dropped
  Stock.replace(3, 700) => Replaced
  Stock.sale_under(600) => OnSale
  Stock.halve_sale_prices() => Halved
  Stock.clear_empty() => Cleared
  Stock = Stock { items: [Item { id: 1, qty: 4, price: 250, on_sale: true }], peak: 5, sales: 1, outbox: [Restocked { item: 1, qty: 5 }] }
11 step(s), all as expected

Item 1 went on sale and was halved to 250. Items 2 and 3 had no stock, so clear_empty removed them, item 3 only a few steps after replace put it there. peak is five, from the restock; sales is one, because the second sale failed and a failure changes nothing.

verify: 3 examples, 11 scenario steps and 19000 sampled checks passed

Two deltas, one cell

Because every delta reads the old state, two deltas that write the same cell of a table in one case would each compute from the old value, and one of the two writes would be lost. The verifier reports it as a conflict (book/failures/deltas-conflict.vish):

row Account { id: id, balance: money(2) }
unit Bank {
    state accounts: Account[8];
    contract transfer(from: id, to: id, amount: money(2)) {
        case amount <= 0 => fail BadAmount;
        else => Moved: accounts[from].balance -= amount, accounts[to].balance += amount;
        examples {
            { accounts: [Account { id: 1, balance: 500 }, Account { id: 2, balance: 0 }] } (1, 2, 200)
                => Moved { accounts: [Account { id: 1, balance: 300 }, Account { id: 2, balance: 200 }] };
        }
    }
}
verify FAILED: 1 failure(s):
[1] sampled check failed: Bank.transfer: Bank.transfer: conflicting deltas: `accounts[k].balance` is written twice in one case for the same key 2 (state Bank { accounts: [Account { id: 0, balance: 37 }, Account { id: 3, balance: -3 }, Account { id: 2, balance: 6 }, Account { id: 6, balance: 8 }] } args (2, 2, 44)); guard the case so the keys differ, or write a single delta
  (state Bank { accounts: [Account { id: 0, balance: 37 }, Account { id: 3, balance: -3 }, Account { id: 2, balance: 6 }, Account { id: 6, balance: 8 }] } args (2, 2, 44))

The example, from account 1 to account 2, passes. The sampler then called transfer(2, 2, 44): a transfer from an account to itself. Both deltas write the balance of account 2, one subtracting and one adding, and there is no single right answer for the order in which they apply. The verifier names the cell, the key and the arguments, and says what to do: guard the case so the keys differ, or write one delta. Here the guard is a rule the bank wants anyway, case from == to => fail SameAccount;, placed before the else; with it the program verifies.

Everything else is unchanged

Read any case of the stock unit and you know everything it can change. sell touches one row’s quantity and the sales count; it cannot touch a price, another row, or peak, because it does not name them. There is no method that changes something on the side and no statement three calls deep. Examples rely on it: an after-state lists what the deltas name, and the verifier checks that the rest stayed put.

Try

In restock, change peak max= items[item].qty + qty to peak max= items[item].qty, forgetting that the delta reads the quantity before the restock. The program still checks, and the scenario still passes, since it never asks for peak; its final state shows peak: 0. vishy verify fails on restock’s example, which starts at a peak of four and restocks two to seven: it expected peak: 7 and got peak: 4. The example was the only thing in the program that said what peak means.

Reading the state: expressions and quantifiers

Guards, carried values, invariants and the right-hand sides of deltas are all expressions: they read the state and compute a value, and they change nothing. This chapter is the vocabulary for reading a table. Its program is a job queue whose contracts only read, each with examples that pin the answer down.

enum Priority { low, normal, urgent }
row Job { id: id, priority: Priority, size: int(1, 100), added: instant, owner: option<id> }
unit Queue {
    state jobs: Job[8];
    fixture Three = { jobs: [
        Job { id: 1, priority: Priority.normal, size: 40, added: 100, owner: none },
        Job { id: 2, priority: Priority.urgent, size: 10, added: 300, owner: some(7) },
        Job { id: 3, priority: Priority.urgent, size: 25, added: 200, owner: none } ] };
    fn weight(j: Job) -> int = match j.priority { Priority.low => 1, Priority.normal => 2, Priority.urgent => 5 };
    contract total() -> int {
        else => Total(sum(jobs.size));
        examples { Three () => Total(75); }
    }
    contract big() -> int {
        else => Big(count(jobs.size > 20));
        examples { Three () => Big(2); }
    }
    contract free() -> id[8] {
        else => Free(where(j in jobs: j.owner == none).id);
        examples { Three () => Free([1, 3]); }
    }
    contract all_small() -> bool {
        else => AllSmall(all(j in jobs: j.size < 50));
        examples { Three () => AllSmall(true); }
    }
    contract first_urgent() -> id {
        case !any(j in jobs: j.priority == Priority.urgent) => fail NoneUrgent;
        else => FirstUrgent(first(j in jobs: j.priority == Priority.urgent).id);
        examples { Three () => FirstUrgent(2); {} () => NoneUrgent; }
    }
    contract next() -> id {
        case !any(j in jobs: j.owner == none) => fail Idle;
        else => let j = best(j in jobs: j.owner == none by (j.priority < Priority.urgent, j.added)); Next(j.id);
        examples { Three () => Next(3); {} () => Idle; }
    }
    contract load() -> int {
        else => Load(fold(j in jobs, acc = 0: acc + weight(j) * j.size));
        examples { Three () => Load(255); }
    }
    contract owner_of(job: id) -> id {
        case jobs[job].owner == none => fail Unowned;
        else => Owner(match jobs[job].owner { some o => o, none => job });
        examples { Three (2) => Owner(7); Three (1) => Unowned; }
    }
    contract doubled(job: id) -> int {
        else => Doubled(let j = with(jobs[job], size, jobs[job].size * 2) in weight(j) * j.size);
        examples { Three (1) => Doubled(160); }
    }
}

fixture Three = { … } names a state, three jobs, so that every example can start from it by name instead of spelling the jobs out again. The last chapter of this part covers fixtures.

Plain expressions

Arithmetic + - * / %, comparisons, && || !, and if c then a else b are what you expect. r.f reads a field of a row, t[k] reads the row with key k, and k in t asks whether there is one. len, min, max, abs, clamp and percent are builtins.

Columns

jobs.size is a column: the size of every job, in table order, as a list. A reduction turns it into one value: sum(jobs.size) is the total work, 75. Operators apply to a column element by element, so jobs.size > 20 is a column of bools, and count(jobs.size > 20) counts the true ones: two jobs are bigger than twenty. That is the whole idea of lifting: write the question about one row and put the column where the row would be.

Quantifiers

A quantifier binds a name to each row in turn and asks a question:

quantifieranswers
all(j in jobs: p)whether p holds for every job
any(j in jobs: p)whether it holds for at least one
count(j in jobs: p)how many
where(j in jobs: p)the jobs for which it holds, as a list
first(j in jobs: p)the first such job, in table order
best(j in jobs: p by (k1, k2))the such job with the smallest keys
fold(j in jobs, acc = e0: body)a value built up row by row

where(…).id is the ids of the matching rows: the free jobs are [1, 3]. first and best return a row, so there must be one: first_urgent and next guard with any and fail with an outcome of their own when there is none.

best sorts by the key tuple and takes the smallest, ties broken by id. A bool sorts false before true, so j.priority < Priority.urgent as the first key puts urgent jobs first, and j.added as the second picks the oldest among them. Job 2 is urgent but taken, job 3 is urgent and free, so next() answers 3 although job 1 has waited longer.

fold is the general case: an accumulator, a starting value, and a body computing the next accumulator from it and the row. load weighs each job by its priority and sums, 255.

Naming things: let and with

let x = e in body names a value inside an expression. A case may also bind before its outcome, as next does: let j = best(…); Next(j.id), so the selection is computed once and read twice.

with(r, f, e) is the row r with field f replaced by e: a new value, nothing changed. doubled asks what a job would weigh at twice its size without touching the job.

match

An option is read with match: match jobs[job].owner { some o => o, none => job }, where o is the value inside for that arm. An enum is read the same way, one arm per variant, as weight does. The match must cover every variant or end with an else arm, and the checker holds you to it (book/refusals/reading-match.vish):

book/refusals/reading-match.vish:5:32: error: match on `Priority` misses Priority.urgent (add the arms or an `else`)
      fn weight(j: Job) -> int = match j.priority { Priority.low => 1, Priority.normal => 2 };
                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This is the refusal that pays for itself the day someone adds a variant: every match without an else that does not handle it becomes a line number.

What is not there

There are no list comprehensions and no loops in an expression (book/refusals/reading-comprehension.vish):

book/refusals/reading-comprehension.vish:4:49: error: expected `,` or `]` in list
      contract sizes() -> int[8] => Sizes([j.size for j in jobs]);
                                                  ^^^

The column jobs.size is that list already, and where, count and fold cover the rest. The reason is what the verifier needs from an expression: every quantifier ranges over a table or a list with a bound, so every expression finishes, and finishes in a number of steps the bound limits. An expression cannot loop forever, cannot build a structure the checker does not know the shape of, and cannot change anything while it reads. Where a computation truly needs a loop, it goes in a function, the subject of the chapter after next.

verify: 12 examples, 0 scenario steps and 9000 sampled checks passed

Twelve examples, one or two per contract, and nine thousand sampled calls on random queues, none of which found first or best asked for a row that is not there.

Try

In next, swap the keys: by (j.added, j.priority < Priority.urgent). The program checks, and vishy verify fails on next’s example: it expected job 3 and got job 1, the oldest free job, which is only normal. The order of the keys is the policy, and one example is enough to hold it.

Machines

Many rows have a status that moves in one direction: an order is placed, then paid, then packed, then shipped, and may be cancelled until it ships. Written as guards, that rule is repeated in every contract that touches the status, and forgotten in one of them. A machine writes it once, as the legal moves of one field, and the language refuses every other move.

enum Status { placed, paid, packed, shipped, cancelled }
row Order { id: id, status: Status, total: money(2) }
unit Orders {
    state orders: Order[8];
    machine orders.status {
        start Status.placed;
        Status.placed -> Status.paid;
        Status.paid -> Status.packed;
        Status.packed -> Status.shipped;
        Status.placed -> Status.cancelled;
        Status.paid -> Status.cancelled;
        Status.packed -> Status.cancelled;
    }
    contract place(order: id, total: money(2)) {
        case total <= 0 => fail Empty;
        else => Placed: orders += [Order { id: order, status: Status.placed, total: total }];
    }
    contract advance(order: id, to: Status) {
        else => Moved: orders[order].status := to;
        examples {
            { orders: [Order { id: 1, status: Status.placed, total: 500 }] } (1, Status.paid)
                => Moved { orders: [Order { id: 1, status: Status.paid, total: 500 }] };
            { orders: [Order { id: 1, status: Status.packed, total: 500 }] } (1, Status.cancelled)
                => Moved { orders: [Order { id: 1, status: Status.cancelled, total: 500 }] };
            { orders: [Order { id: 1, status: Status.placed, total: 500 }] } (1, Status.shipped) => WrongStatus;
            { orders: [Order { id: 1, status: Status.paid, total: 500 }] } (1, Status.paid) => WrongStatus;
            { orders: [Order { id: 1, status: Status.shipped, total: 500 }] } (1, Status.cancelled) => WrongStatus;
        }
    }
}
scenario one_order {
    Orders.place(1, 2500) => Placed;
    Orders.advance(1, Status.shipped) => WrongStatus;
    Orders.advance(1, Status.paid) => Moved;
    Orders.advance(1, Status.paid) => WrongStatus;
    Orders.advance(1, Status.packed) => Moved;
    Orders.advance(1, Status.shipped) => Moved;
    Orders.advance(1, Status.cancelled) => WrongStatus;
}

The declaration

machine orders.status { … } names a field of a table’s rows and lists what may happen to it:

  • start Status.placed; is the only value a new row may have in that field;
  • Status.placed -> Status.paid; is one legal move, an edge, from one value to another.

Any other move is illegal. An order cannot go from placed to shipped, cannot come back from shipped, cannot be cancelled once it has shipped, because no edge says it may.

WrongStatus without a guard

advance has no guard at all. It sets the status to whatever it is asked, and the machine decides:

scenario one_order
  Orders.place(1, 2500) => Placed
  Orders.advance(1, shipped) => WrongStatus
  Orders.advance(1, paid) => Moved
  Orders.advance(1, paid) => WrongStatus
  Orders.advance(1, packed) => Moved
  Orders.advance(1, shipped) => Moved
  Orders.advance(1, cancelled) => WrongStatus
  Orders = Orders { orders: [Order { id: 1, status: shipped, total: 2500 }] }
7 step(s), all as expected

Skipping from placed to shipped answers WrongStatus. So does cancelling a shipped order at the end. WrongStatus is the fourth of the implicit outcomes from the chapter on rows: produced by the language, a failure that changes nothing, available to every contract of a unit with a machine, and nameable in examples and scenarios without a case declaring it.

That is why one contract, advance(order, to), replaces a contract per transition. Without the machine you would write pay, pack, ship and cancel, each with a guard on the current status; with it, the guards are the edges, and they are in one place. A contract that wants a different answer for an illegal move, fail NotPaidYet say, keeps its own case, tried before the delta.

Staying in place is a move

The fourth step asks to pay an order that is already paid, and the answer is WrongStatus. Assigning a field the value it already has is still a move under the machine, from paid to paid, and there is no such edge. This is deliberate: a second payment is usually a mistake worth hearing about, and a machine that wants to allow it says so with Status.paid -> Status.paid;.

Ranges

When the field is a bounded int rather than an enum, a range of values may share an edge (book/programs/machines-steps.vish):

row Ticket { id: id, step: int(1, 5) }
unit Desk {
    state tickets: Ticket[8];
    machine tickets.step { start 1; 1 -> 2; 2 -> 3; 3 -> 4; 1..3 -> 5; }
    contract open(ticket: id, step: int(1, 5)) {
        else => Opened: tickets += [Ticket { id: ticket, step: step }];
        examples {
            {} (1, 1) => Opened { tickets: [Ticket { id: 1, step: 1 }] };
            {} (1, 3) => WrongStatus;
        }
    }
    contract advance(ticket: id, to: int(1, 5)) {
        else => Moved: tickets[ticket].step := to;
        examples {
            { tickets: [Ticket { id: 1, step: 2 }] } (1, 5) => Moved { tickets: [Ticket { id: 1, step: 5 }] };
            { tickets: [Ticket { id: 1, step: 4 }] } (1, 5) => WrongStatus;
        }
    }
}

1..3 -> 5 is three edges at once: from any step from one to three, a ticket may jump to five. From step four it may not, which the last example states. The examples of open show start at work: a new ticket at step one is fine, and a new ticket at step three is WrongStatus. With an enum status, as in the orders, each edge is written on its own line.

The sampler assumes nothing the machine excludes

The verifier draws states at random, and it draws the status of each row like any other field: any value of its type. It does not ask whether the machine could have reached that row. So a contract may not rely on the machine to rule a state out. Here is a unit that does (book/failures/machines-assume.vish):

enum Status { placed, paid, shipped }
row Order { id: id, status: Status, total: money(2), paid: money(2) }
unit Orders {
    state orders: Order[8];
    machine orders.status { start Status.placed; Status.placed -> Status.paid; Status.paid -> Status.shipped; }
    contract place(order: id, total: money(2)) => Placed: orders += [Order { id: order, status: Status.placed, total: total, paid: 0 }];
    contract pay(order: id) => Paid: orders[order].status := Status.paid, orders[order].paid := orders[order].total;
    contract ship(order: id) {
        else => Shipped: orders[order].status := Status.shipped;
        ensures orders[order].paid == orders[order].total;
        examples {
            { orders: [Order { id: 1, status: Status.paid, total: 500, paid: 500 }] } (1)
                => Shipped { orders: [Order { id: 1, status: Status.shipped, total: 500, paid: 500 }] };
        }
    }
}

pay moves an order to paid and records the payment in the same call, so every paid order the scenarios could produce has paid == total, and ship says so in an ensures, a check on the state after the call. The verifier disagrees:

verify FAILED: 1 failure(s):
[1] sampled check failed: Orders.ship: Orders.ship: ensures of case 1 violated: state Orders { orders: [Order { id: 0, status: placed, total: -59, paid: 13 }, Order { id: 3, status: shipped, total: 25, paid: 20 }, Order { id: 4, status: shipped, total: 62, paid: 51 }, Order { id: 5, status: placed, total: -39, paid: 29 }, Order { id: 6, status: paid, total: 4, paid: 36 }] } args (3,) out Shipped
  (state Orders { orders: [Order { id: 0, status: placed, total: -59, paid: 13 }, Order { id: 3, status: paid, total: 25, paid: 20 }, Order { id: 4, status: shipped, total: 62, paid: 51 }, Order { id: 5, status: placed, total: -39, paid: 29 }, Order { id: 6, status: paid, total: 4, paid: 36 }] } args (3,))

It drew order 3 in status paid with 20 paid of a total of 25, a row no sequence of calls produces, and shipped it. The machine allowed the move from paid to shipped; nothing said a paid order has been paid in full. The rule lived in the author’s head and in the pairing of two deltas in pay, and the verifier cannot see either.

The fix is to state it as a rule about every state: add invariant all(o in orders: o.status == Status.placed || o.paid == o.total); to the unit. Now the verifier draws only states that keep the invariant, checks that pay and ship keep it too, and the program verifies.

verify: 5 examples, 7 scenario steps and 4000 sampled checks passed

Try

Add the edge Status.paid -> Status.paid; to the orders machine. The program checks, and the run stops at the fourth step: it expected WrongStatus for the second payment and got Moved. vishy verify also fails the example that says paying a paid order is WrongStatus. The machine is the rule, and the examples and the scenario are how you know it is the rule you meant.

Functions, total and sealed

An expression that is used twice, or that needs a name to be read, becomes a function. Vishy has two kinds. A core function is total: it always finishes, which is what lets contracts and rules call it and the verifier reason about it. A sealed function may do anything a general-purpose language does, recursion and unbounded loops included, and sits behind a boundary that keeps it away from the state.

Core functions

enum Tier { basic, gold }
fn fee(amount: money(2), tier: Tier) -> money(2) = if tier == Tier.gold then percent(amount, 1) else percent(amount, 3);
examples fee { (10000, Tier.basic) => 300; (10000, Tier.gold) => 100; }
fn digits(n: int(0, 999999)) -> int {
    var k = n;
    var d = 0;
    repeat 6 {
        if k == 0 { break; }
        k := k / 10;
        d := d + 1;
    }
    if d == 0 then 1 else d
}
examples digits { (0) => 1; (7) => 1; (42) => 2; (999999) => 6; }
unit Till {
    state takings: money(2) = 0;
    state fees: money(2) = 0;
    contract charge(amount: money(2), tier: Tier) -> money(2) {
        case amount <= 0 => fail BadAmount;
        else => Charged(fee(amount, tier)): takings += amount - fee(amount, tier), fees += fee(amount, tier);
        examples {
            {} (10000, Tier.basic) => Charged(300) { takings: 9700, fees: 300 };
        }
    }
    contract code_length(code: int(0, 999999)) -> int => Length(digits(code));
}
scenario a_sale {
    Till.charge(10000, Tier.gold) => Charged(100);
    Till.code_length(4711) => Length(4);
}

fn fee(…) -> money(2) = …; is a function whose body is one expression. examples fee { … } are its acceptance tests, written like a contract’s without the states: arguments and the result they must give. vishy verify runs them with the contracts’ examples, so the seven examples in this program are two for fee, four for digits and one for charge:

verify: 7 examples, 2 scenario steps and 4000 sampled checks passed

A contract calls a function like a builtin: Charged(fee(amount, tier)). A function may also be declared inside a unit, where it can read the unit’s state; the one above is at the top level and reads only its arguments.

Block bodies

digits needs a loop, so its body is a block: statements in braces, then the result expression.

  • let x = e; names a value; var x = e; names one that may change, with x := e;.
  • if c { … } else { … } chooses between statements.
  • repeat n { … } runs its body at most n times, and break; leaves it early.

repeat 6 is the loop’s bound, written where the loop is: a number of at most six digits needs at most six divisions. Every loop in a core function has one. There is no while, and a function may not call itself, directly or through another function (book/refusals/recursion.vish):

unit Counter {
    state n: int = 0;
    contract set(k: int(0, 10)) => Done: n := fact(k);
}
fn fact(k: int) -> int = if k <= 1 then 1 else k * fact(k - 1);
book/refusals/recursion.vish:5:4: error: `fact` calls itself; a core function is total, recursion belongs to a `sealed fn`
  fn fact(k: int) -> int = if k <= 1 then 1 else k * fact(k - 1);
     ^^^^

So every core function finishes, on every input, in a number of steps its bounds limit. That is what makes a rule that calls one checkable: the verifier calls it thousands of times on sampled states and never waits. Blocks are for functions only; a contract stays a list of cases and deltas.

Sealed functions

Some computation does not fit: a parser, a walk over a tree of unknown depth, a search that stops when it finds something. That is the sealed layer:

sealed row Node { name: string, kids: Node[] }
sealed fn size(n: Node) -> int = 1 + fold(k in n.kids, acc = 0: acc + size(k));
examples size { (Node { name: "a", kids: [Node { name: "b", kids: [] }, Node { name: "c", kids: [Node { name: "d", kids: [] }] }] }) => 4; }
sealed fn words(s: string) -> int = len(split(s, " "));
examples words { ("to be or not") => 4; }
sealed fn first_long(s: string, n: int) -> int {
    let ws = split(s, " ");
    var i = 0;
    while i < len(ws) {
        if len(ws[i]) >= n { return i; }
        i := i + 1;
    }
    -1
}
examples first_long { ("a tiny example", 5) => 2; ("a b c", 5) => -1; }
unit Notes {
    state longest: int = 0;
    contract note(text: string(64)) -> int {
        else => Words(words(text)): longest max= words(text);
        examples {
            { longest: 2 } ("to be or not") => Words(4) { longest: 4 };
        }
    }
}
scenario two_notes {
    Notes.note("hello world") => Words(2);
    Notes.note("hi") => Words(1);
}

A sealed fn may:

  • call itself, as size does to count the nodes of a tree;
  • loop with while, and leave early with return e;, as first_long does;
  • take and return string and lists with no bound, string rather than string(64).

A sealed row may contain itself: Node has a list of Nodes, which a core row may not, as the chapter on rows showed. Sealed rows exist only inside sealed functions.

The contract note calls words, a sealed function, on its string(64) argument, and stores the result in the unit. That is how the two layers meet: a core value goes in, a core value comes out, and the result is trusted only to its declared type.

scenario two_notes
  Notes.note("hello world") => Words(2)
  Notes.note("hi") => Words(1)
  Notes = Notes { longest: 2 }
2 step(s), all as expected
verify: 5 examples, 2 scenario steps and 2000 sampled checks passed

The boundary

Sealed code never touches unit state or events. A sealed function has no unit in scope and cannot emit; and nothing in a unit may hold a sealed value (book/refusals/functions-sealed-state.vish):

book/refusals/functions-sealed-state.vish:3:5: error: state field `root` of unit `Outline` cannot hold the sealed row `Node`; sealed rows live only in sealed functions
      state root: Node;
      ^^^^^^^^^^^^^^^^^

The rule is what keeps the rest of the book true. Everything the verifier reasons about, the state, the deltas, the rules, is core, with bounds it knows; the sealed layer is a pure function it can call and nothing else.

The step budget

A sealed function is not promised to finish, so the verifier runs it under a budget: ten million steps by default, one per call and one per loop iteration, set with the variable VISHY_FUEL. A function that runs out fails the example or the sampled check that reached it (book/failures/functions-fuel.vish):

sealed fn collatz(n: int) -> int {
    var k = n;
    var steps = 0;
    while k != 1 {
        if k % 2 == 0 { k := k / 2; } else { k := 3 * k + 1; }
        steps := steps + 1;
    }
    steps
}
examples collatz { (1) => 0; (6) => 8; (0) => 0; }
verify FAILED: 1 failure(s):
[1] examples collatz case 3: sealed function `collatz` did not finish within 10000000 steps

The third example starts the loop at zero, which halves to zero forever. In a core function the same loop would need a repeat bound and would stop; here the budget is what stops it, and the failure names the function and the example. The verifier never samples a sealed function on its own, since it has no bounded inputs to draw from: its examples are its tests, together with every call a contract makes during the sampled checks.

Try

Delete the example (0) => 0; from collatz and verify again: it passes, since the other two finish. Now run it as VISHY_FUEL=5 vishy verify book/failures/functions-fuel.vish 7 1000. The example (6) => 8 fails, because it needs nine steps, one for the call and one for each of its eight iterations.

The unit as a whole: fixtures, capabilities, ownership

The chapters of this part took a unit apart: its values, its tables, its deltas, its expressions, its machines and functions. This one puts a unit back together with the declarations that belong to the unit as a whole, and ends where the next part begins: at the edge of the unit, which nothing crosses.

events { Opened(ticket: id, at: instant) }
row Ticket { id: id, opened: instant, closed: option<instant>, open: bool }
unit Desk {
    caps ids, outbox, storage;
    state tickets: Ticket[8];
    derived tickets.open = for t => t.closed == none;
    derived open_count: int = count(t in tickets: t.closed == none);
    fixture Two = { tickets: [
        Ticket { id: 1, opened: 1000, closed: none },
        Ticket { id: 2, opened: 2000, closed: some(3000) } ] };
    invariant all(t in tickets: match t.closed { some c => c >= t.opened, none => true });
    commutes { close };
    contract open(now: instant) -> id {
        uses ids;
        else => let t = fresh(); Opened(t): tickets += [Ticket { id: t, opened: now, closed: none }], emit Opened(t, now);
        examples {
            {} (1000) => Opened(1) { tickets: [Ticket { id: 1, opened: 1000, closed: none, open: true }] };
            Two (5000) => Opened(3) Two with { tickets: [
                Ticket { id: 1, opened: 1000, closed: none, open: true },
                Ticket { id: 2, opened: 2000, closed: some(3000), open: false },
                Ticket { id: 3, opened: 5000, closed: none, open: true } ] };
        }
    }
    contract close(ticket: id, now: instant) {
        case tickets[ticket].closed != none => fail AlreadyClosed;
        else => Closed: tickets[ticket].closed := some(max(now, tickets[ticket].opened));
        examples {
            Two (1, 4000) => Closed Two with { tickets: [
                Ticket { id: 1, opened: 1000, closed: some(4000), open: false },
                Ticket { id: 2, opened: 2000, closed: some(3000), open: false } ] };
            Two (2, 4000) => AlreadyClosed;
            Two with { tickets: [Ticket { id: 5, opened: 9000, closed: none }] } (5, 4000)
                => Closed { tickets: [Ticket { id: 5, opened: 9000, closed: some(9000), open: false }] };
        }
    }
    contract waiting() -> int => Waiting(open_count);
}
flow open() uses clock, ids atomic { Desk.open(now); }
flow close(ticket: id) uses clock atomic { Desk.close(ticket, now); }
scenario a_morning {
    at 1000 open() => Opened(first);
    at 2500 open() => Opened(_);
    at 4000 close(first) => Closed;
    Desk.waiting() => Waiting(1);
}

Capabilities

A unit’s contracts compute from their arguments and the unit’s state, and nothing else. Anything more is a capability, declared on the unit with caps, so that what a unit can do to the world is written in one line at its top:

  • caps ids lets a contract that says uses ids; call fresh(), a new id above every id the unit knows. open mints the ticket’s id this way, so no caller chooses it. In an example the new id is one more than the largest present: 1 in an empty desk, 3 in the fixture Two.
  • caps outbox lets a contract emit events, as the chapter on deltas showed.
  • caps storage makes the unit’s state persist when the program is served, and changes nothing else; the chapter on storage shows it.

A contract that uses a capability its unit did not declare is refused (book/refusals/unit-emit.vish):

book/refusals/unit-emit.vish:5:105: error: `emit` needs `caps outbox` on unit `Desk`
      contract open(ticket: id, now: instant) => Opened: tickets += [Ticket { id: ticket, opened: now }], emit Opened(ticket);
                                                                                                          ^^^^^^^^^^^^^^^^^^^

The time is not among them. A contract does not read a clock: open and close take now: instant as an argument. The clock belongs to the flow, the transaction that calls the contract: flow open() uses clock, ids atomic { Desk.open(now); } binds now and passes it in, and a scenario sets it with at 1000. Flows are the next part’s subject; here the point is that a contract’s result depends only on what it is given, which is why an example can pin it down.

Fixtures

fixture Two = { … }; names a state, written like an example’s before-state. An example may start from it by name: Two (2, 4000) => AlreadyClosed; is the whole example for closing a closed ticket. Two with { … } is the fixture with some fields replaced, and it may stand on either side of an example: the third close example starts from Two with a single late ticket, and the first ends at Two with both tickets closed. A fixture is shared vocabulary for examples, so a reader learns the state once.

Derived fields

derived tickets.open = for t => t.closed == none; is a derived column: a field of every row, defined by an equation and kept up to date by the compiler after every call. No contract assigns it; a row literal in a delta leaves it out, and the run shows it filled in. derived open_count: int = count(…) is a derived scalar of the unit, read like any state field by waiting. A derived field says a fact once instead of in every contract that could change it.

The invariant

A unit has one invariant, several conditions joined with &&. Here it says a ticket is never closed before it was opened, which close keeps with max(now, opened). The verifier checks it after every call, from every state it samples.

scenario a_morning
  open() => Opened(1)
  open() => Opened(2)
  close(1) => Closed
  Desk.waiting() => Waiting(1)
  Desk = Desk { tickets: [Ticket { id: 1, opened: 1000, closed: Some(4000), open: false }, Ticket { id: 2, opened: 2500, closed: None, open: true }], open_count: 1, outbox: [Opened { ticket: 1, at: 1000 }, Opened { ticket: 2, at: 2500 }] }
4 step(s), all as expected
verify: 5 examples, 4 scenario steps, 6000 sampled checks and 1000 commutativity checks passed

Commutes

commutes { close }; declares that calls to close may happen in either order with the same result. The verifier takes a sampled state, applies pairs of such calls in both orders, and reports when the results differ: the thousand commutativity checks in the verdict. The chapter on order and commutativity says why that matters when many writers touch one unit.

Ownership

The first removal, stated at the start of this part, is the one this chapter ends on: nothing outside a unit reads or writes its state. Here is a second unit that wants to refuse a shift change while tickets are waiting, by looking at the desk (book/refusals/unit-reach.vish):

row Ticket { id: id, opened: instant, closed: option<instant> }
unit Desk {
    state tickets: Ticket[8];
    contract open(ticket: id, now: instant) => Opened: tickets += [Ticket { id: ticket, opened: now, closed: none }];
}
unit Staff {
    state on_duty: int = 0;
    contract leave() {
        case any(t in Desk.tickets: t.closed == none) => fail TicketsWaiting;
        else => Left: on_duty -= 1;
    }
}
book/refusals/unit-reach.vish:9:23: error: unknown name `Desk`
          case any(t in Desk.tickets: t.closed == none) => fail TicketsWaiting;
                        ^^^^

Inside a unit, another unit is not even a name, so there is nothing to make public and no modifier to relax. Only flows compose units: a flow asks the desk how many tickets are open, receives the answer as a value, and passes it to the staff unit’s contract as an argument. That is the next part of the book.

What the rule buys is everything in this part. A unit’s rules are about its own state, so they can be checked on the unit alone, from sampled states of that unit alone; its examples are complete, since no other code can change what they describe; and a person or a model given one unit to write needs to know nothing about the others but the flows’ arguments.

Try

In close, replace some(max(now, tickets[ticket].opened)) with some(now). The program checks. vishy verify fails the third close example first: the ticket opened at 9000 is now closed at 4000, and the invariant is broken after the call. The sampled checks and the flow walk find the same defect from random states.

Flows: one transaction across units

Everything so far lived inside one unit, and the first removal says a unit cannot see another. A program with two units needs one more thing: a place where they meet. That place is a flow, a named sequence of contract calls on several units that runs as one transaction. It is the only thing in the language that composes units.

row Ticket { id: id, seat: int(1, 3), holder: id, sold: instant }
unit Seats {
    state left: int(0, 3) = 3;
    contract take() -> int(1, 3) {
        case left == 0 => fail SoldOut;
        else => Taken(left): left -= 1;
    }
}
unit Tickets {
    state tickets: Ticket[3];
    contract issue(ticket: id, seat: int(1, 3), holder: id, at: instant) -> id {
        case count(t in tickets: t.holder == holder) >= 2 => fail Limit;
        else => Issued(ticket): tickets += [Ticket { id: ticket, seat, holder, sold: at }];
    }
}
flow buy() uses clock, ids, caller atomic {
    let ticket = fresh();
    let seat = Seats.take();
    Tickets.issue(ticket, seat, caller, now);
    ensures Seats.left == old(Seats.left) - 1;
}
scenario a_sale {
    at 1000 as 7 buy() => Issued(_);
    at 2000 as 7 buy() => Issued(_);
    at 3000 as 7 buy() => Limit;
    at 4000 as 8 buy() => Issued(_);
    at 5000 as 9 buy() => SoldOut;
}

A sequence of calls

flow buy() calls Seats.take, then Tickets.issue, in that order. Each step names a unit and one of its contracts. Neither unit knows the other exists; the flow knows both, and it is the only code that does.

Carried values travel

let seat = Seats.take(); binds the value that take carries, the seat number in Taken(left), and the next step passes it to issue. This is how a fact one unit holds reaches another: as a value, through a flow, never by reading.

A value can be bound only when every success case of the contract carries one. Add a case that answers a plain outcome and the name would be empty whenever that case applies, so the checker refuses the flow:

unit Seats {
    state left: int(0, 3) = 3;
    contract take() -> int(1, 3) {
        case left == 0 => fail SoldOut;
        case left == 1 => LastSeat: left -= 1;
        else => Taken(left): left -= 1;
    }
}
unit Tickets {
    state sold: int(0, 3) = 0;
    contract issue(seat: int(1, 3)) => Issued: sold += 1;
}
flow buy() atomic {
    let seat = Seats.take();
    Tickets.issue(seat);
}
book/refusals/flows-no-value.vish:14:9: error: `let seat = Seats.take(...)`: every success case of the contract must carry a value to be bound
      let seat = Seats.take();
          ^^^^

Which outcome the caller hears

The steps run in order, and the first one that fails ends the flow: its failure is the flow’s answer. When every step succeeds, the flow’s outcome is the last call’s. So buy answers Issued with the new ticket’s id, SoldOut when the first step finds no seat (and issue never runs), and Limit when the second step finds that the buyer already holds two tickets.

scenario a_sale
  buy() => Issued(10)
  buy() => Issued(0)
  buy() => Limit
  buy() => Issued(4)
  buy() => SoldOut
  Seats = Seats { left: 0 }
  Tickets = Tickets { tickets: [Ticket { id: 10, seat: 3, holder: 7, sold: 1000 }, Ticket { id: 0, seat: 2, holder: 7, sold: 2000 }, Ticket { id: 4, seat: 1, holder: 8, sold: 4000 }] }
5 step(s), all as expected

One transaction

The flow is atomic: when any step fails, the whole world is restored to what it was before the flow began. Step 3 shows it. Buyer 7 asks for a third ticket; take succeeds and a seat is gone; then issue answers Limit. The seat comes back with the failure, which is why buyer 8 still gets one at step 4, and why the final world holds three tickets for three seats. The other mode, serial, keeps each step’s writes when a later step fails; the section below on kept steps shows the middle ground, and the Try section shows what serial does here.

What a flow may use

Beyond its arguments, a flow sees only what its header’s uses names. clock binds now, the time of the call; a scenario sets it with at 1000, and the tickets’ sold times are the scenario’s. caller binds caller, the identity of whoever called; a scenario sets it with as 7. ids allows let ticket = fresh();, an id that no table in the world holds. The run drew 10, 0 and 4: a scenario cannot know in advance which id it will get, so it expects Issued(_), any value.

A promise about the whole flow

ensures Seats.left == old(Seats.left) - 1; is checked after the flow commits, where Seats.left is the value after and old(Seats.left) the value before. It is a rule about one flow, the way an invariant is a rule about every moment:

verify: 0 examples, 5 scenario steps and 3000 sampled checks passed

Among the sampled checks are runs of buy from sampled worlds, each followed by that check. A failed flow changes nothing, so the promise is only asked of a flow that ran to its end.

Stop and keep

Two more step forms cover flows that should neither simply succeed nor simply roll back.

row Parcel { id: id, delivered: bool }
row Attempt { parcel: id, at: instant }
unit Log {
    state attempts: Attempt[8] ordered;
    contract note(parcel: id, at: instant) => Noted: attempts += [Attempt { parcel, at }];
}
unit Parcels {
    state parcels: Parcel[4];
    contract add(parcel: id) => Added: parcels += [Parcel { id: parcel, delivered: false }];
    contract deliver(parcel: id) {
        case !(parcel in parcels) => fail NoSuchParcel;
        case parcels[parcel].delivered => stop AlreadyDelivered;
        else => Delivered: parcels[parcel].delivered := true;
    }
}
unit Courier {
    state owed: money(2) = 0;
    contract pay(fee: money(2)) => Paid: owed += fee;
}
flow attempt(parcel: id) uses clock atomic {
    keep Log.note(parcel, now);
    Parcels.deliver(parcel);
    Courier.pay(500);
}
scenario a_round {
    Parcels.add(1) => Added;
    at 1000 attempt(2) => NoSuchParcel;
    at 2000 attempt(1) => Paid;
    at 3000 attempt(1) => AlreadyDelivered;
}

A case may answer stop AlreadyDelivered. A stop outcome is a success that ends the flow at that call: its writes and the earlier steps’ writes commit, the later steps do not run, and it carries no value. Here a repeated delivery is not an error, and it must not pay the courier twice.

keep Log.note(parcel, now); is a kept step. It commits what the flow has written so far: a later failure restores the world to how it was right after the kept step, not to the start. So a refused attempt is still recorded.

scenario a_round
  Parcels.add(1) => Added
  attempt(2) => NoSuchParcel
  attempt(1) => Paid
  attempt(1) => AlreadyDelivered
  Log = Log { attempts: [Attempt { parcel: 2, at: 1000 }, Attempt { parcel: 1, at: 2000 }, Attempt { parcel: 1, at: 3000 }] }
  Parcels = Parcels { parcels: [Parcel { id: 1, delivered: true }] }
  Courier = Courier { owed: 500 }
4 step(s), all as expected

Three attempts, three lines in the log. The first was for a parcel that does not exist: deliver failed, the flow answered NoSuchParcel, and only the kept line survived. The second delivered and paid, and answered Paid, the last call’s outcome. The third stopped at AlreadyDelivered, a success, before the payment; the courier is owed 500, once. The scenario’s first step, Parcels.add(1), calls a contract with no flow around it, which the chapter on scenarios explains.

An outcome’s kind, success, failure or stop, is one for the whole program, because a caller switches on the name. A parcel that is already delivered cannot be both a harmless repeat and a refusal:

row Parcel { id: id, delivered: bool }
unit Parcels {
    state parcels: Parcel[4];
    contract deliver(parcel: id) {
        case parcels[parcel].delivered => stop AlreadyDelivered;
        else => Delivered: parcels[parcel].delivered := true;
    }
    contract recall(parcel: id) {
        case parcels[parcel].delivered => fail AlreadyDelivered;
        else => Recalled: remove parcels[parcel];
    }
}
book/refusals/flows-stop-kind.vish:9:48: error: outcome `AlreadyDelivered` is declared differently elsewhere (fail / carried value / type must agree across the program)
          case parcels[parcel].delivered => fail AlreadyDelivered;
                                                 ^^^^^^^^^^^^^^^^

Name the refusal differently, TooLate, and both contracts check.

A contract cannot do this itself

The line that is a step in a flow is refused inside a contract:

row Ticket { id: id, seat: int(1, 3) }
unit Seats {
    state left: int(0, 3) = 3;
    contract take() -> int(1, 3) {
        case left == 0 => fail SoldOut;
        else => Taken(left): left -= 1;
    }
}
unit Tickets {
    state tickets: Ticket[3];
    contract issue(ticket: id) -> id {
        else => let seat = Seats.take(); Issued(ticket): tickets += [Ticket { id: ticket, seat }];
    }
}
book/refusals/flows-call.vish:12:38: error: expected `;`, found `(`
          else => let seat = Seats.take(); Issued(ticket): tickets += [Ticket { id: ticket, seat }];
                                       ^

The checker reads Seats.take and stops at the parenthesis: a contract has no call to parse, only its own cases and deltas. This is the first removal again, and it pays: a contract changes one unit, so it is checked against that unit’s invariant alone, and everything that crosses units is in a flow, where the verifier checks the whole world.

Try

In flows-tickets.vish, change atomic to serial. The program still checks, but the scenario now fails at step 4: expected Issued(_), got SoldOut from Seats.take. Buyer 7’s refused third purchase took a seat, and with nothing restored, the seat was never given back.

World rules: invariants and derived state

A unit’s invariant speaks only of that unit’s state, because a unit cannot see any other. Some rules are about two units at once: a courier is paid once for each delivered parcel. Parcels knows nothing of payments and Courier knows nothing of parcels, so neither can state it. Such a rule is written at the top level of the program, where every unit’s state can be named, and it is called a world invariant.

row Parcel { id: id, delivered: bool }
unit Parcels {
    state parcels: Parcel[4];
    contract add(parcel: id) => Added: parcels += [Parcel { id: parcel, delivered: false }];
    contract deliver(parcel: id) {
        case !(parcel in parcels) => fail NoSuchParcel;
        case parcels[parcel].delivered => stop AlreadyDelivered;
        else => Delivered: parcels[parcel].delivered := true;
    }
}
unit Courier {
    state payments: int(0, 8) = 0;
    contract pay() => Paid: payments += 1;
}
// the courier is paid once for each delivered parcel
invariant Courier.payments == count(p in Parcels.parcels: p.delivered);
flow deliver(parcel: id) atomic { Parcels.deliver(parcel); Courier.pay(); }
scenario a_round {
    Parcels.add(1) => Added;
    deliver(1) => Paid;
    deliver(1) => AlreadyDelivered;
    deliver(2) => NoSuchParcel;
}

A rule over several units

invariant Courier.payments == count(p in Parcels.parcels: p.delivered); stands outside every unit and names each unit’s state as Unit.field. It must hold in the initial world (no parcels, no payments) and after every flow. It is not checked between the steps of a flow: after deliver has marked the parcel and before pay has run, the rule is broken for a moment, and that is allowed, because nobody sees the world halfway through a transaction.

scenario a_round
  Parcels.add(1) => Added
  deliver(1) => Paid
  deliver(1) => AlreadyDelivered
  deliver(2) => NoSuchParcel
  Parcels = Parcels { parcels: [Parcel { id: 1, delivered: true }] }
  Courier = Courier { payments: 1 }
4 step(s), all as expected
verify: 0 examples, 4 scenario steps and 5000 sampled checks passed

Who keeps it

No unit can keep a rule it cannot see. The flow that commands both units keeps it: deliver marks the parcel and then pays, as one transaction. When the parcel does not exist, deliver fails and nothing is paid. When the parcel was already delivered, deliver stops the flow as a success before the payment. Each way through the flow leaves the rule true, and that is what the verifier checked, walking the flows from worlds that satisfy the rule.

The order of the two steps is part of how the rule is kept. Swap them, paying first and delivering second (the program is book/failures/world-order.vish; the flow and the scenario are the lines that changed):

flow deliver(parcel: id) atomic { Courier.pay(); Parcels.deliver(parcel); }
scenario a_round {
    Parcels.add(1) => Added;
    deliver(1) => Delivered;
    deliver(1) => AlreadyDelivered;
    deliver(2) => NoSuchParcel;
}

It looks harmless, since an atomic flow undoes the payment if the delivery fails. But a stop is not a failure: it ends the flow as a success, and the steps before it commit.

verify FAILED: 3 failure(s):
[1] scenario a_round step 3 (deliver): flow deliver: world invariant 1 violated: after World { parcels: Parcels { parcels: [Parcel { id: 1, delivered: true }] }, courier: Courier { payments: 2 },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (1,) out AlreadyDelivered
  world World { parcels: Parcels { parcels: [Parcel { id: 1, delivered: true }] }, courier: Courier { payments: 2 },  __now: 0, __ids: 0, __seed: 0, __caller: 0 }
[2] reachable walk, flow deliver: flow deliver: world invariant 1 violated: after World { parcels: Parcels { parcels: [Parcel { id: 4, delivered: true }] }, courier: Courier { payments: 2 },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (4,) out AlreadyDelivered
[3] flow deliver: flow deliver: world invariant 1 violated: after World { parcels: Parcels { parcels: [Parcel { id: 4, delivered: true }] }, courier: Courier { payments: 2 },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (4,) out AlreadyDelivered
  (world World { parcels: Parcels { parcels: [Parcel { id: 4, delivered: true }] }, courier: Courier { payments: 1 },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (4,))

Read the first failure. The scenario’s third step delivers parcel 1 again; the flow deliver answered AlreadyDelivered, and world invariant 1 (the first in source order) is violated after it: one delivered parcel, two payments. The second and third failures find the same thing by walking the flows from sampled worlds, and the third shows the world before the call, with one delivered parcel and one payment. Every failure names the flow, because the flow is the only code that could have kept the rule. The scenario’s second step now expects Delivered, because a flow’s outcome is its last call’s.

Derived state

Some facts about several units are not rules to keep but numbers to compute: how many parcels are waiting, how many each courier has delivered. Both follow from the parcels. A derived field is defined by an equation and kept by the compiler, which recomputes it after every flow. No contract writes it.

row Parcel { id: id, courier: id, delivered: bool }
row Courier { id: id, deliveries: int(0, 4) }
unit Parcels {
    state parcels: Parcel[4];
    contract add(parcel: id, courier: id) => Added: parcels += [Parcel { id: parcel, courier, delivered: false }];
    contract deliver(parcel: id) -> id {
        case !(parcel in parcels) => fail NoSuchParcel;
        case parcels[parcel].delivered => fail AlreadyDelivered;
        else => Delivered(parcels[parcel].courier): parcels[parcel].delivered := true;
    }
}
unit Couriers {
    state couriers: Courier[2];
    state waiting: int(0, 4);
    contract hire(courier: id) => Hired: couriers += [Courier { id: courier }];
    contract tally(courier: id) -> int(0, 4) {
        case !(courier in couriers) => fail NoSuchCourier;
        else => Tally(couriers[courier].deliveries);
    }
}
// a scalar: the parcels not yet delivered
derived Couriers.waiting = count(p in Parcels.parcels: !p.delivered);
// a column: for each courier, the parcels it has delivered
derived Couriers.couriers.deliveries = for c => count(p in Parcels.parcels: p.courier == c.id && p.delivered);
flow deliver(parcel: id) atomic { let courier = Parcels.deliver(parcel); Couriers.tally(courier); }
scenario a_round {
    Couriers.hire(7) => Hired;
    Parcels.add(1, 7) => Added;
    Parcels.add(2, 7) => Added;
    Parcels.add(3, 7) => Added;
    deliver(1) => Tally(0);
    deliver(2) => Tally(1);
    Couriers.tally(7) => Tally(2);
}

derived Couriers.waiting = … is a scalar: the unit declares the field as state, and the equation at the top level says what its value is. derived Couriers.couriers.deliveries = for c => … is a column: for each row c of the couriers table, the value of its deliveries field. The row that hire inserts leaves deliveries out, because the value is not the contract’s to give.

scenario a_round
  Couriers.hire(7) => Hired
  Parcels.add(1, 7) => Added
  Parcels.add(2, 7) => Added
  Parcels.add(3, 7) => Added
  deliver(1) => Tally(0)
  deliver(2) => Tally(1)
  Couriers.tally(7) => Tally(2)
  Parcels = Parcels { parcels: [Parcel { id: 1, courier: 7, delivered: true }, Parcel { id: 2, courier: 7, delivered: true }, Parcel { id: 3, courier: 7, delivered: false }] }
  Couriers = Couriers { couriers: [Courier { id: 7, deliveries: 2 }], waiting: 1 }
7 step(s), all as expected
verify: 0 examples, 7 scenario steps and 8000 sampled checks passed

The final world shows both: courier 7 has two deliveries, and one parcel is waiting. Nothing in the program ever wrote either number.

The value at the flow’s start

Read the fifth step. deliver(1) marks parcel 1 delivered and then, in the same flow, asks for courier 7’s tally; the answer is 0. Inside a flow, a unit reads its world-derived column as it was when the flow began, and the column is recomputed when the flow commits. The next flow answers 1, and the direct call after it answers 2. So a later step that reads a derived value sees the world before the flow, not the work of the steps before it; a fact that must be fresh travels as a carried value instead.

Never written

A derived field is the one piece of state that changes without a delta naming it, and in exchange no delta may name it:

row Parcel { id: id, courier: id, delivered: bool }
row Courier { id: id, deliveries: int(0, 4) }
unit Parcels {
    state parcels: Parcel[4];
    contract deliver(parcel: id) => Delivered: parcels[parcel].delivered := true;
}
unit Couriers {
    state couriers: Courier[2];
    contract credit(courier: id) => Credited: couriers[courier].deliveries += 1;
}
derived Couriers.couriers.deliveries = for c => count(p in Parcels.parcels: p.courier == c.id && p.delivered);
book/refusals/world-derived-write.vish:9:65: error: `couriers.deliveries` is a world-derived column and cannot be assigned
      contract credit(courier: id) => Credited: couriers[courier].deliveries += 1;
                                                                  ^^^^^^^^^^

The equation is the field’s only writer, so the rule “deliveries counts the delivered parcels” cannot be broken by a contract that forgot to update it; there is nothing to forget.

Try

In world-courier.vish, start the courier at one payment: state payments: int(0, 8) = 1;. The verifier fails, and among its four failures is the initial world violates a world invariant. A world rule must hold before the first flow, not only after it.

Scenarios

The verifier samples states nobody wrote down, and that finds the call that breaks a rule. It cannot know what the program is for. A scenario says that: a short story of calls from the initial world, each with the outcome it must produce. It is the program’s behavioural specification, what the product does in the order a person would do it, and every vishy verify runs it.

row Book { id: id, lent: bool }
row Loan { id: id, book: id, reader: id, due: instant }
unit Shelf {
    state books: Book[4];
    contract add(book: id) => Added: books += [Book { id: book, lent: false }];
    contract lend(book: id) {
        case !(book in books) => fail NoSuchBook;
        case books[book].lent => fail OnLoan;
        else => Lent: books[book].lent := true;
    }
    contract take_back(book: id) => Returned: books[book].lent := false;
}
unit Loans {
    state loans: Loan[4];
    contract open(loan: id, book: id, reader: id, due: instant) -> id => Opened(loan): loans += [Loan { id: loan, book, reader, due }];
    contract close(loan: id, reader: id) -> id {
        case !(loan in loans) => fail NoSuchLoan;
        case loans[loan].reader != reader => fail NotYours;
        else => Closed(loans[loan].book): remove loans[loan];
    }
}
flow borrow(book: id) uses clock, ids, caller atomic {
    let loan = fresh();
    Shelf.lend(book);
    Loans.open(loan, book, caller, now + days(14));
}
flow give_back(loan: id) uses caller atomic {
    let book = Loans.close(loan, caller);
    Shelf.take_back(book);
}
scenario a_fortnight {
    Shelf.add(1) => Added;
    at 1000 as 7 borrow(1) => Opened(loan);
    as 8 borrow(1) => OnLoan;
    as 8 give_back(loan) => NotYours;
    as 7 give_back(loan) => Returned;
    at 2000 as 8 borrow(1) => Opened(_);
    as 8 borrow(2) => NoSuchBook;
}

Steps

Each step is a call and the outcome it must produce: as 8 borrow(1) => OnLoan;. The steps run in order, starting from the initial world, each on the world the previous one left.

A step calls a flow by name, or a contract directly. Shelf.add(1) has no flow written around it; the scenario runs it as a flow of one call, atomic like any other. The checker counts it among the flows: the program declares two, and the answer says three.

ok: 2 row(s), 0 event(s), 0 fn(s), 2 unit(s), 5 contract(s), 3 flow(s)

The clock and the caller

A flow that says uses clock reads the time as now, and one that says uses caller reads who called as caller. In a scenario both are written in the step: at 1000 sets the clock and as 7 sets the caller, so the story runs the same way every time. Reader 7 borrows at 1000, reader 8 is refused the same book, and each give_back is judged by who asks.

Carried values: a value, any value, a name

When an outcome carries a value, a step can state it exactly, Opened(10); accept any value with _, Opened(_); or bind it to a name, Opened(loan), which later steps pass as an argument.

Binding is how a scenario follows an id it cannot know. borrow draws the loan’s id with fresh(), an id no table holds, and a scenario has no way to predict which one that will be. Step 2 binds it as loan; steps 4 and 5 give it back, once as the wrong reader and once as the right one.

What run prints

vishy run executes every scenario and prints the trace:

scenario a_fortnight
  Shelf.add(1) => Added
  borrow(1) => Opened(10)
  borrow(1) => OnLoan
  give_back(10) => NotYours
  give_back(10) => Returned
  borrow(1) => Opened(0)
  borrow(2) => NoSuchBook
  Shelf = Shelf { books: [Book { id: 1, lent: true }] }
  Loans = Loans { loans: [Loan { id: 0, book: 1, reader: 8, due: 1209602000 }] }
7 step(s), all as expected

Each step is printed with the outcome it produced, and a bound name is shown by its value: the loan was 10, so give_back(loan) appears as give_back(10). After the steps comes every unit’s state as the story left it. Read it as the story’s record: the book is lent again, and the one open loan is reader 8’s, due fourteen days after 2000, in milliseconds. The verifier counts the same seven steps:

verify: 0 examples, 7 scenario steps and 8000 sampled checks passed

The first wrong step

Write the scenario the way a programmer used to auto-increment keys would, expecting the first loan to be number 1 (the program is book/failures/scenarios-first-id.vish):

scenario a_fortnight {
    Shelf.add(1) => Added;
    at 1000 as 7 borrow(1) => Opened(1);
    as 8 borrow(1) => OnLoan;
    as 8 give_back(1) => NotYours;
    as 7 give_back(1) => Returned;
    at 2000 as 8 borrow(1) => Opened(_);
    as 8 borrow(2) => NoSuchBook;
}
verify FAILED: 1 failure(s):
[1] scenario a_fortnight step 2 (borrow): expected Opened(1), got Opened(10) from Loans.open
  world World { shelf: Shelf { books: [Book { id: 1, lent: true }] }, loans: Loans { loans: [Loan { id: 10, book: 1, reader: 7, due: 1209601000 }] },  __now: 1000, __ids: 0, __seed: 1442695040888963407, __caller: 7 }

The verdict names the scenario, the step, the flow, what the step expected and what came instead, and the contract call it came from: Loans.open, the flow’s last call, whose outcome is the flow’s. Then it prints the world as it stood right after that step, with the loan under its real id, 10.

The scenario stops at the first wrong step. Steps 4 and 5 give back a loan numbered 1, which does not exist, and would be wrong too, yet the verdict has one failure: steps 3 to 7 did not run. Each would be judged on a world the story did not expect, and its answer would only repeat the first mistake in other words. One wrong step, one failure, and it is the one to read.

The fix is the name. Opened(loan) binds whatever id was drawn, and the steps that follow use it.

What a scenario is for

A rule about every state belongs in an invariant or a failure case, where the verifier checks it from states nobody wrote. A scenario holds what rules cannot say: the order of a day’s events and the outcome a person sees at each one. So scenarios are few, one per behaviour the product promises, and each is short.

Try

In scenarios-library.vish, change step 2’s Opened(loan) to Opened(_). The checker refuses the program at step 4: arguments must be literals or names bound by an earlier step. Nothing bound loan any more.

Views

Contracts change state and flows compose them. A program also has to be read, and reading is where the first removal seems to get in the way: nothing outside a unit can read it. The answer is the same as for world invariants. A unit cannot read another unit, but the top level of the program can name every unit’s state, and a read is declared there, as a view: a named expression over the world, with the rule of who may see what written inside the expression.

row Book { id: id, title: string(32), lent: bool }
row Loan { id: id, book: id, reader: id }
unit Shelf {
    state books: Book[4];
    contract add(book: id, title: string(32)) => Added: books += [Book { id: book, title, lent: false }];
    contract lend(book: id) {
        case !(book in books) => fail NoSuchBook;
        case books[book].lent => fail OnLoan;
        else => Lent: books[book].lent := true;
    }
}
unit Loans {
    state loans: Loan[4];
    contract open(loan: id, book: id, reader: id) -> id => Opened(loan): loans += [Loan { id: loan, book, reader }];
}
flow borrow(book: id) uses ids, caller atomic {
    let loan = fresh();
    Shelf.lend(book);
    Loans.open(loan, book, caller);
}
// what is on the shelf: anyone may ask
view available() = where(b in Shelf.books: !b.lent);
// how many copies of a title the library owns
view copies(title: string(32)) = count(b in Shelf.books: b.title == title);
// the books the caller holds, and nobody else's
view my_books() uses caller = where(l in Loans.loans: l.reader == caller).book;
scenario reading_room {
    Shelf.add(1, "Dune") => Added;
    Shelf.add(2, "Emma") => Added;
    available() => Value([Book { id: 1, title: "Dune", lent: false }, Book { id: 2, title: "Emma", lent: false }]);
    as 7 borrow(1) => Opened(_);
    available() => Value([Book { id: 2, title: "Emma", lent: false }]);
    copies("Dune") => Value(1);
    as 7 my_books() => Value([1]);
    as 8 my_books() => Value([]);
}

A read over the world

view name(params) = expr; is the whole form. The expression names units’ state as Unit.field, exactly as a world invariant does, and its value may be a list of rows (available), a number (copies), or a list of ids (my_books, the book column of the matching loans). Nothing in the header says the type; the checker infers it from the expression.

Who may see it

view my_books() uses caller binds caller, the identity of whoever asks, as a flow does, and the expression uses it: the loans whose reader is the caller, and no one else’s. The rule of who sees what is part of the view, not a separate permission to forget. Reader 7 asks and gets book 1; reader 8 asks the same view and gets nothing.

A view in a scenario

A scenario step can call a view and state its value: available() => Value([…]);, with as 7 setting the caller when the view uses one. The value is written as a literal, rows and all:

scenario reading_room
  Shelf.add(1, "Dune") => Added
  Shelf.add(2, "Emma") => Added
  available() => Value([Book { id: 1, title: "Dune", lent: false }, Book { id: 2, title: "Emma", lent: false }])
  borrow(1) => Opened(10)
  available() => Value([Book { id: 2, title: "Emma", lent: false }])
  copies("Dune") => Value(1)
  my_books() => Value([1])
  my_books() => Value([])
  Shelf = Shelf { books: [Book { id: 1, title: "Dune", lent: true }, Book { id: 2, title: "Emma", lent: false }] }
  Loans = Loans { loans: [Loan { id: 10, book: 1, reader: 7 }] }
8 step(s), all as expected

Computed when asked, kept nowhere

A view is evaluated over the world as it stands when it is asked. Its value is stored nowhere: the state the run prints at the end holds the units’ fields and nothing of the views. The two available() steps answer differently because the world changed between them, not because anything updated the view. There is no read model to keep in step with the state, because there is no second copy.

verify: 0 examples, 8 scenario steps and 8000 sampled checks passed

The views are among the sampled checks. The program has three contracts, two flows (borrow, and the scenario’s direct call to Shelf.add) and three views, and the verifier ran each a thousand times, the views on worlds it reached and with sampled arguments. For a view it checks that the evaluation does not break, as reading an absent key would. It cannot know whether the value is the right one; the scenario’s steps say that.

What a view may not do

A view has no deltas. An assignment where its expression should be is refused:

row Book { id: id, title: string(32), lent: bool }
unit Shelf {
    state books: Book[4];
    contract lend(book: id) {
        case books[book].lent => fail OnLoan;
        else => Lent: books[book].lent := true;
    }
}
view first_book() = Shelf.books[1].lent := true;
book/refusals/views-write.vish:9:41: error: expected `;`, found `:=`
  view first_book() = Shelf.books[1].lent := true;
                                          ^^

Nor does it call a contract:

row Book { id: id, title: string(32), lent: bool }
unit Shelf {
    state books: Book[4];
    contract lend(book: id) {
        case books[book].lent => fail OnLoan;
        else => Lent: books[book].lent := true;
    }
}
view first_book() = Shelf.lend(1);
book/refusals/views-call.vish:9:31: error: expected `;`, found `(`
  view first_book() = Shelf.lend(1);
                                ^

Both stop where the expression ends: after Shelf.books[1].lent or Shelf.lend, a view’s body is over. Everything that changes the world goes through a contract, inside a flow; a view only looks.

At the door

When the program is served, a view is published with a GET route, route GET "/books" => available();. The door loads the stored world, evaluates the view with the request’s arguments (and the caller from the request’s token when the view uses one), and answers the value; it opens no transaction and writes nothing. A list is paged with limit and offset query parameters. The checker holds both ends of that: a route to a view must be GET, and a served view may not take a parameter with either paging name.

book/refusals/views-post.vish:7:7: error: a route to view `available` must be GET; a view reads and never writes
  route POST "/books" => available();
        ^^^^
book/refusals/views-limit.vish:7:1: error: a view served by a route cannot name a parameter `limit` or `offset`; the door uses those for pages
  route GET "/books" => first_books(limit);
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The chapter on storage and the door shows a served view answering.

Try

Add a view that reads one book’s title, view title(book: id) = Shelf.books[book].title;. The program checks, but the verifier fails: view title: no row with that key, followed by the world it sampled. Asked for a book that is not on the shelf, the view has no answer, and a view that can break on a reachable world is a failure like any other.

Order and commutativity

Most units are called by people, one request at a time, in the order things happened. Some are fed by another system instead: a scanner, a payment provider, a telephone switch, each sending events as they occur. A feed cannot promise order. Events arrive late, out of order, and, because a sender retries until it hears back, sometimes twice. A unit fed by events has to end in the same state whatever order they come in.

That property has a name. Two calls commute when making them in either order leaves the same state. A unit can declare that some of its contracts commute, and the verifier looks for the pair that does not.

Declaring it

A parcel tracker keeps, for each parcel, the first and the last time a scanner saw it:

row Parcel { id: id, first: int(0, 1000), last: int(0, 1000) }
unit Tracking {
    state parcels: Parcel[4];
    commutes { scanned };
    contract scanned(parcel: id, at: int(1, 1000)) {
        case parcel in parcels => Seen: parcels[parcel].last := at;
        else => Seen: parcels += [Parcel { id: parcel, first: at, last: at }];
    }
}

commutes { scanned }; declares that calls to scanned commute with each other. A set may name several contracts of the unit, commutes { picked_up, delivered }; the verifier draws the same contract twice as readily as two different ones, so a set of one is a real question.

What the verifier does with it

From a sampled state, the verifier draws two calls from the set, each with its own sampled arguments, and makes them in both orders on two copies of the state. Two successes that leave different states are a counterexample, and so is one order that succeeds while the other fails. When both orders fail, there is nothing to compare, and the verifier moves on.

verify FAILED: 1 failure(s):
[1] Tracking: scanned(9, 226) and scanned(9, 231) do not commute: the two orders leave different states
  scanned(9, 226) then scanned(9, 231): Tracking { parcels: [Parcel { id: 1, first: 876, last: 223 }, Parcel { id: 3, first: 369, last: 25 }, Parcel { id: 5, first: 281, last: 42 }, Parcel { id: 9, first: 226, last: 231 }] }
  scanned(9, 231) then scanned(9, 226): Tracking { parcels: [Parcel { id: 1, first: 876, last: 223 }, Parcel { id: 3, first: 369, last: 25 }, Parcel { id: 5, first: 281, last: 42 }, Parcel { id: 9, first: 231, last: 226 }] }
  state Tracking { parcels: [Parcel { id: 1, first: 876, last: 223 }, Parcel { id: 3, first: 369, last: 25 }, Parcel { id: 5, first: 281, last: 42 }] }

Read it. The state has no parcel 9, and the two calls are scans of parcel 9 at 226 and at 231. In the order they happened, the row says first 226, last 231. In the other order, the scan at 231 creates the row and the scan at 226 overwrites last: the parcel was last seen before it was first seen. With :=, the last scan to arrive wins, not the last scan to happen. The other three rows are whatever the sampler drew, and they are the same in both states; only parcel 9 differs.

Updates that commute by construction

x max= e sets x to the larger of x and e, and x min= e to the smaller. A maximum does not care about order: the larger of three times is the same whichever two were compared first. So a case whose deltas are max= and min= commutes by construction. The fix is the first case’s line; the example and the scenario are new too, and the next sections read them.

row Parcel { id: id, first: int(0, 1000), last: int(0, 1000) }
unit Tracking {
    state parcels: Parcel[4];
    commutes { scanned };
    contract scanned(parcel: id, at: int(1, 1000)) {
        case parcel in parcels => Seen: parcels[parcel].first min= at, parcels[parcel].last max= at;
        else => Seen: parcels += [Parcel { id: parcel, first: at, last: at }];
        examples {
            { parcels: [Parcel { id: 1, first: 100, last: 300 }] } (1, 300) => Seen;
        }
    }
}
scenario a_late_feed {
    Tracking.scanned(1, 300) => Seen;
    Tracking.scanned(1, 100) => Seen;
    Tracking.scanned(1, 300) => Seen;
}
scenario a_late_feed
  Tracking.scanned(1, 300) => Seen
  Tracking.scanned(1, 100) => Seen
  Tracking.scanned(1, 300) => Seen
  Tracking = Tracking { parcels: [Parcel { id: 1, first: 100, last: 300 }] }
3 step(s), all as expected
verify: 1 examples, 3 scenario steps, 2000 sampled checks and 1000 commutativity checks passed

The scenario sends a scan at 300, a late one at 100, and the one at 300 again. The row ends with the truth, first 100 and last 300. The verifier’s line has a new count, a thousand commutativity checks, next to the examples, the scenario steps and the sampled checks.

Idempotence: the same call twice

A repeated event is the feed’s other habit. A call is idempotent when making it twice leaves the state that making it once leaves. max= and min= are idempotent as well: the larger of 300 and 300 is 300. The example says so for one state: the scan at 300, sent again to a row that already has it, answers Seen and names no after-state, which means nothing changed, and the verifier holds the example to that.

The commutativity check does not stand in for that example. Count the scans as well, with scans += 1 (the program is book/failures/order-count.vish). Addition commutes, so every commutativity check still passes. But a scan delivered twice is counted twice, and the example catches it:

verify FAILED: 1 failure(s):
[1] Tracking.scanned example 1: the example gives no after-state, which means unchanged, but the state changed: before Tracking { parcels: [Parcel { id: 1, first: 100, last: 300, scans: 2 }] } after Tracking { parcels: [Parcel { id: 1, first: 100, last: 300, scans: 3 }] } (write the after-state, or `unchanged`)

Commutativity says the order of the feed does not matter; idempotence says its repetitions do not. A unit fed by events needs both, and each is checked by its own statement: commutes for the order, and an example that repeats a call for the repetition. A number that counts events is right only if the feed never repeats itself; the first and last times are right whatever it does.

Why it is worth declaring

Without the declaration, a unit fed out of order is correct only if something upstream sorts the events and removes the duplicates first: a buffer, a sequence number, a queue with exactly-once delivery, each its own program with its own failures. With it, the unit takes events as they come, and the verifier has looked for an order that matters and found none. The feed’s disorder stops being a problem to engineer around and becomes a property of the unit, stated in one line and checked.

Try

In order-scans.vish, refuse late scans instead of absorbing them: add case parcel in parcels && at < parcels[parcel].last => fail Late; as the first case. The verifier now reports two failures. The scenario’s late scan answers Late at step 2, and the commutativity check reports scanned(9, 226) and scanned(9, 231) do not commute: one order succeeds and the other fails (Late). Refusing an event for arriving late makes the outcome depend on the order, which is the one thing a feed does not control.

Interfaces and parts

Until now one writer wrote the whole program. This part of the book is about many writers on one program at once, people or models, and it rests on the first of the language’s two removals: a unit cannot be read from outside. The appendix Building with a team tells the story on the room-booking program, with the roles, the sequence and what to do when a step fails. This chapter is the mechanism, on a smaller program: a tool library with a shelf of tools and a box of members’ cards.

Two kinds of owner

A program written by many hands has one interface owner and any number of unit owners. The interface owner writes the interface: the one shared file, holding everything the units have in common and every promise each unit makes. A unit owner writes a part: the contract bodies of one unit, and nothing else.

The interface

row Tool { id: id, kind: string(32), copies: int(1, 9), lent: int(0, 9) }
row Card { id: id, holding: int(0, 3) }

unit Shelf {
    state tools: Tool[16];
    // rule 1: a tool never has more copies lent than the shelf owns.
    invariant all(t in tools: t.lent <= t.copies);
    fixture Drill = { tools: [Tool { id: 1, kind: "drill", copies: 2, lent: 1 }] };
    contract add(tool: id, kind: string(32), copies: int(1, 9)) {
        // Added: a new tool with none lent. An existing id is Exists.
        outcomes Added;
        examples {
            {} (1, "drill", 2) => Added { tools: [Tool { id: 1, kind: "drill", copies: 2, lent: 0 }] };
            Drill (1, "saw", 1) => Exists;
        }
    }
    contract lend(tool: id) {
        // Lent: one more copy is lent. NoneLeft when every copy is lent. An absent tool is Unknown.
        outcomes Lent, fail NoneLeft;
        examples {
            Drill (1) => Lent { tools: [Tool { id: 1, kind: "drill", copies: 2, lent: 2 }] };
            { tools: [Tool { id: 1, kind: "drill", copies: 2, lent: 2 }] } (1) => NoneLeft;
            Drill (9) => Unknown;
        }
    }
}

unit Cards {
    state cards: Card[16];
    // rule 2: a card holds at most three tools at once.
    invariant all(c in cards: c.holding <= 3);
    fixture Ann = { cards: [Card { id: 7, holding: 1 }] };
    contract issue(card: id) {
        // Issued: a new card holding nothing. An existing id is Exists.
        outcomes Issued;
        examples {
            {} (7) => Issued { cards: [Card { id: 7, holding: 0 }] };
            Ann (7) => Exists;
        }
    }
    contract borrow(card: id) {
        // Borrowed: the card holds one more. AtLimit when it holds three. An absent card is Unknown.
        outcomes Borrowed, fail AtLimit;
        examples {
            Ann (7) => Borrowed { cards: [Card { id: 7, holding: 2 }] };
            { cards: [Card { id: 7, holding: 3 }] } (7) => AtLimit;
            Ann (8) => Unknown;
        }
    }
}

// Both rules are kept by the contracts; the flow is atomic, so a tool with none left leaves the card as it was.
flow borrow(card: id, tool: id) atomic { Cards.borrow(card); Shelf.lend(tool); }

view free(tool: id) = where(t in Shelf.tools: t.id == tool && t.lent < t.copies);

scenario a_saturday {
    Shelf.add(1, "drill", 1) => Added;
    Cards.issue(7) => Issued;
    borrow(7, 1) => Lent;
    free(1) => Value([]);
    borrow(7, 1) => NoneLeft;
    borrow(8, 1) => Unknown;
}

It holds the rows; every unit’s state, its invariant and its fixtures; and every contract as a stub. A stub is a contract with no cases: its header with bounded parameters, the rule it keeps as a one-sentence comment, the line outcomes Lent, fail NoneLeft; that declares its outcomes and which of them are failures, and the examples that pin the rule down. After the units come the flows, the views and the scenarios; a served program’s routes go here too, as the room-booking interface in the appendix shows. Nothing in the file says how any contract does its work.

vishy check --interface checks it as an interface:

vishy check --interface book/programs/interface/writers-lending.vish
ok: 2 row(s), 0 event(s), 0 fn(s), 2 unit(s), 4 contract(s), 3 flow(s)

It refuses an interface that starts doing a writer’s job. Here Cards.borrow has cases where its outcomes should be (the program is book/refusals/interface/writers-body.vish):

book/refusals/interface/writers-body.vish:5:5: error: `Cards.borrow` has cases; an interface declares outcomes, not bodies (`outcomes Ok, fail Full;`)
      contract borrow(card: id) {
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^

It also refuses a declared failure without an example that produces it; the Contract anti-patterns page shows that refusal and why it is the rule that pays most.

A part

unit Shelf {
contract add(tool: id, kind: string(32), copies: int(1, 9)) {
    else => Added: tools += [Tool { id: tool, kind: kind, copies: copies, lent: 0 }];
}
contract lend(tool: id) {
    case tools[tool].lent >= tools[tool].copies => fail NoneLeft;
    else => Lent: tools[tool].lent += 1;
}
}

A part is a second declaration of a unit that holds only contract bodies: no state, no rows, no invariant. Its headers must equal the interface’s, and its cases must produce exactly the declared outcomes, no more and no fewer. It needs no examples of its own, because the interface’s examples are appended to it; it may add some.

Why a part can be written alone

The shelf’s writer never sees the cards. Nothing in Shelf may read Cards, and nothing in Cards may read Shelf: the checker refuses a read across units (What Vishy is not shows the refusal). The two meet only in the flow borrow, which is the interface owner’s. So everything a Shelf body can touch is in the interface: its own state, its rows, its rule and its examples. That is the whole of what its writer needs, and the whole of what can make its answer wrong.

What a writer receives

vishy brief book/programs/interface/writers-lending.vish Shelf

prints the brief: the sections of the language reference a part needs, the prelude, the shared declarations with their comments, the world rules that name the unit, and the unit’s block as the interface owner wrote it. Nothing about the other units. It is not reprinted here; most of it is the reference, the same for every unit.

The local check

vishy part book/programs/interface/writers-lending.vish Shelf book/programs/parts/writers-lending.Shelf.vish
vishy part book/programs/interface/writers-lending.vish Cards book/programs/parts/writers-lending.Cards.vish
verify: 5 examples, 0 scenario steps and 1000 sampled checks passed
verify: 5 examples, 0 scenario steps and 1000 sampled checks passed

Each writer checks their part against the interface and then runs their unit alone: the interface’s examples, and five hundred sampled calls per contract against the unit’s invariant. No flows, no scenarios, no world rules: those belong to the whole program. Two writers, two verdicts, and neither waited for the other. When the check fails, it names the contracts to rewrite.

The writer stage

vishy write is a unit owner that is a program: it sends one request per unit to a cheap model, all at once, checks each part with the same local check as it lands, and sends back only the contracts a failed check names, up to three rounds. When every part passes, it assembles and verifies the whole program, and a whole-program failure that names a contract goes back to that unit’s writer. It needs a model endpoint and a key, so this book describes it and does not run it; the appendix gives the handbook’s timings. A unit the stage cannot finish goes to a person, whose part is held to the same vishy part.

Assembly

vishy assemble book/programs/interface/writers-lending.vish book/programs/parts/writers-lending.Shelf.vish book/programs/parts/writers-lending.Cards.vish book/programs/writers-lending.vish

writes the interface and the parts as one program, book/programs/writers-lending.vish: the interface’s text, then each part, which the compiler merges into its unit’s stub. Assembly refuses while any contract is still a stub. The assembled program is checked, run and verified like any other:

scenario a_saturday
  Shelf.add(1, "drill", 1) => Added
  Cards.issue(7) => Issued
  borrow(7, 1) => Lent
  free(1) => Value([])
  borrow(7, 1) => NoneLeft
  borrow(8, 1) => Unknown
  Shelf = Shelf { tools: [Tool { id: 1, kind: "drill", copies: 1, lent: 1 }] }
  Cards = Cards { cards: [Card { id: 7, holding: 1 }] }
6 step(s), all as expected
verify: 10 examples, 6 scenario steps and 8000 sampled checks passed

The scenario’s second borrow shows the flow’s promise: the card was charged, the shelf had none left, and the card is back at one tool because the flow is atomic. Assembly is where the writers’ work first meets; the next chapter is what the verifier does there.

Try

In the cards part, change fail AtLimit to fail TooMany. vishy part refuses: Cards.borrow produces TooMany, which the interface does not declare. The message lists the declared outcomes and the implicit ones, and the check names borrow as the contract to rewrite. Delete the AtLimit case instead, and it refuses again, because AtLimit is declared and no case produces it. A writer cannot invent an answer or drop one; the next chapter shows what that second refusal is worth.

What the verifier does

vishy verify decides whether a program is accepted, whoever wrote its parts. This chapter is what it runs, in order, what its number counts, and how much each layer catches, measured. The program is the tool library again, whole: the bodies written, a second flow, a rule across the two units, a view, and a notice board that has nothing to do with lending.

row Tool { id: id, copies: int(1, 9), lent: int(0, 9) }
row Card { id: id, holding: int(0, 3) }

unit Shelf {
    state tools: Tool[16];
    invariant all(t in tools: t.lent <= t.copies);
    contract add(tool: id, copies: int(1, 9)) => Added: tools += [Tool { id: tool, copies, lent: 0 }];
    contract lend(tool: id) {
        case tools[tool].lent >= tools[tool].copies => fail NoneLeft;
        else => Lent: tools[tool].lent += 1;
        examples {
            { tools: [Tool { id: 1, copies: 2, lent: 1 }] } (1) => Lent { tools: [Tool { id: 1, copies: 2, lent: 2 }] };
            { tools: [Tool { id: 1, copies: 2, lent: 2 }] } (1) => NoneLeft;
        }
    }
    contract take_back(tool: id) {
        case tools[tool].lent == 0 => fail NotLent;
        else => Returned: tools[tool].lent -= 1;
        examples {
            { tools: [Tool { id: 1, copies: 2, lent: 1 }] } (1) => Returned { tools: [Tool { id: 1, copies: 2, lent: 0 }] };
            { tools: [Tool { id: 1, copies: 2, lent: 0 }] } (1) => NotLent;
        }
    }
}

unit Cards {
    state cards: Card[16];
    invariant all(c in cards: c.holding <= 3);
    contract issue(card: id) => Issued: cards += [Card { id: card, holding: 0 }];
    contract borrow(card: id) {
        case cards[card].holding >= 3 => fail AtLimit;
        else => Borrowed: cards[card].holding += 1;
        examples {
            { cards: [Card { id: 7, holding: 1 }] } (7) => Borrowed { cards: [Card { id: 7, holding: 2 }] };
            { cards: [Card { id: 7, holding: 3 }] } (7) => AtLimit;
        }
    }
    contract give_back(card: id) {
        case cards[card].holding == 0 => fail HoldsNothing;
        else => GaveBack: cards[card].holding -= 1;
        examples {
            { cards: [Card { id: 7, holding: 1 }] } (7) => GaveBack { cards: [Card { id: 7, holding: 0 }] };
            { cards: [Card { id: 7, holding: 0 }] } (7) => HoldsNothing;
        }
    }
}

unit Board {
    state notice: string(64) = "";
    contract post(text: string(64)) => Posted: notice := text;
}

// rule: every copy lent is held on some card.
invariant sum(Shelf.tools.lent) == sum(Cards.cards.holding);

flow borrow(card: id, tool: id) atomic { Cards.borrow(card); Shelf.lend(tool); }
flow bring_back(card: id, tool: id) atomic { Cards.give_back(card); Shelf.take_back(tool); }

view on_loan() = where(t in Shelf.tools: t.lent > 0);

scenario a_saturday {
    Shelf.add(1, 1) => Added; Cards.issue(7) => Issued; Cards.issue(8) => Issued;
    borrow(7, 1) => Lent;
    borrow(8, 1) => NoneLeft;
    bring_back(8, 1) => HoldsNothing;
    bring_back(7, 1) => Returned;
    borrow(8, 1) => Lent;
    on_loan() => Value([Tool { id: 1, copies: 1, lent: 1 }]);
    Board.post("closed on Sunday") => Posted;
}
verify: 8 examples, 10 scenario steps and 13000 sampled checks passed

Examples first

Every example runs its contract once on the state and arguments it names, and compares the outcome and the state after. Eight here. An example that fails, fails at any seed and any sample count.

Then scenarios

Every scenario runs from the initial world through the flows and stops at the first step whose answer differs from the one written. Ten steps here.

Then sampled checks on units

For every contract, a thousand times (the last number on the command line): a state of its unit, arguments drawn inside their bounds, one call. The state is drawn in one of two ways, half the time each:

  • field by field at random, kept only if it satisfies the unit’s invariant, because a state the unit could never be in proves nothing;
  • by a random walk of the unit’s own contracts, eight to sixty-four calls from its initial state, which reaches states that only a history produces.

Ids come from a small pool, so calls meet present and absent keys. After every call the verifier checks the unit’s invariant and every ensures the case or the contract states, and fails the call if no case matched, if two deltas wrote the same cell, or if anything panicked, such as first on nothing. A table’s bound and a machine act inside the call: a delta that would take a table past its bound ends as Full, a move the machine forbids as WrongStatus. Both are failed outcomes, which change nothing.

Then sampled checks on worlds

For every flow, a thousand times: a world reached by a random walk of the flows, sixteen to ninety-six flow calls from the initial world, then one call with drawn arguments. (In a program without world rules, half of these worlds are put together from units drawn one by one.) After every flow the verifier checks the world rules, the flow’s ensures, and atomicity: an atomic flow that failed must leave the world exactly as it was. The walk’s own steps are checked too, and a rule broken on the way is reported as a reachable walk. Every view is evaluated a thousand times on walked worlds, and must not panic.

The number

13000 sampled checks is thirteen jobs of a thousand: seven contracts, five flows and one view. The five flows are the two declared ones and the three that the scenario’s direct calls create, Shelf.add, Cards.issue and Board.post. Examples and scenario steps are counted once each. Commutativity checks, when a unit asks for them, are counted apart (Order and commutativity).

Then reachability

Last, every outcome a contract’s cases can produce must have been produced at least once, by an example or a sampled call; an outcome nothing produced is a case nobody tested, and the verdict fails.

Only what a flow can touch

After a flow, the verifier re-checks only the world rules that mention a unit the flow can change, and a flow’s walk draws only from the flows of its component, the group of units that flows and world rules join together. vishy graph prints the components:

3 unit(s), 5 flow(s), 1 world invariant(s), 0 world-derived field(s): 2 component(s)
component 1: units Shelf, Cards | flows borrow, bring_back, Shelf.add, Cards.issue | 1 invariant(s)
component 2: units Board | flows Board.post | 0 invariant(s)
flow borrow: touches Shelf, Cards | re-checks 1 of 1 invariant(s), recomputes 0 of 0 derived | walks 4 of 5 flow(s)
flow bring_back: touches Shelf, Cards | re-checks 1 of 1 invariant(s), recomputes 0 of 0 derived | walks 4 of 5 flow(s)
flow Shelf.add: touches Shelf | re-checks 1 of 1 invariant(s), recomputes 0 of 0 derived | walks 4 of 5 flow(s)
flow Cards.issue: touches Cards | re-checks 1 of 1 invariant(s), recomputes 0 of 0 derived | walks 4 of 5 flow(s)
flow Board.post: touches Board | re-checks 0 of 1 invariant(s), recomputes 0 of 0 derived | walks 1 of 5 flow(s)
note: the largest component has 2 of 3 units; each component verifies independently.

Board.post touches no unit the rule mentions, so it re-checks none of it, and its walks are made of notices only. A flow costs what it can reach, not what the program holds.

A failure it finds

Delete Shelf.take_back(tool); from bring_back (the program is book/failures/verifier-forgot.vish). Every contract is still right; the flow is wrong:

verify FAILED: 3 failure(s):
[1] scenario a_saturday step 7 (bring_back): flow bring_back: world invariant 1 violated: after World { shelf: Shelf { tools: [Tool { id: 1, copies: 1, lent: 1 }] }, cards: Cards { cards: [Card { id: 7, holding: 0 }, Card { id: 8, holding: 0 }] }, board: Board { notice: "" },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (7, 1) out GaveBack
  world World { shelf: Shelf { tools: [Tool { id: 1, copies: 1, lent: 1 }] }, cards: Cards { cards: [Card { id: 7, holding: 0 }, Card { id: 8, holding: 0 }] }, board: Board { notice: "" },  __now: 0, __ids: 0, __seed: 0, __caller: 0 }
[2] reachable walk, flow bring_back: flow bring_back: world invariant 1 violated: after World { shelf: Shelf { tools: [Tool { id: 10, copies: 6, lent: 0 }, Tool { id: 4, copies: 3, lent: 1 }, Tool { id: 12, copies: 4, lent: 0 }, Tool { id: 14, copies: 3, lent: 1 }] }, cards: Cards { cards: [Card { id: 10, holding: 1 }, Card { id: 14, holding: 0 }, Card { id: 13, holding: 0 }] }, board: Board { notice: "" },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (14, 13) out GaveBack
[3] flow bring_back: flow bring_back: world invariant 1 violated: after World { shelf: Shelf { tools: [Tool { id: 15, copies: 5, lent: 0 }, Tool { id: 2, copies: 8, lent: 0 }, Tool { id: 0, copies: 8, lent: 1 }, Tool { id: 6, copies: 4, lent: 0 }, Tool { id: 1, copies: 6, lent: 0 }, Tool { id: 3, copies: 5, lent: 0 }, Tool { id: 10, copies: 5, lent: 1 }] }, cards: Cards { cards: [Card { id: 6, holding: 0 }, Card { id: 10, holding: 0 }, Card { id: 1, holding: 1 }, Card { id: 2, holding: 0 }, Card { id: 3, holding: 0 }] }, board: Board { notice: "" },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (10, 0) out GaveBack
  (world World { shelf: Shelf { tools: [Tool { id: 15, copies: 5, lent: 0 }, Tool { id: 2, copies: 8, lent: 0 }, Tool { id: 0, copies: 8, lent: 1 }, Tool { id: 6, copies: 4, lent: 0 }, Tool { id: 1, copies: 6, lent: 0 }, Tool { id: 3, copies: 5, lent: 0 }, Tool { id: 10, copies: 5, lent: 1 }] }, cards: Cards { cards: [Card { id: 6, holding: 0 }, Card { id: 10, holding: 1 }, Card { id: 1, holding: 1 }, Card { id: 2, holding: 0 }, Card { id: 3, holding: 0 }] }, board: Board { notice: "" },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (10, 0))

One defect, found three ways: by the scenario’s seventh step, by a walk, and by the flow’s own sampled check. Each report names the flow, the world rule by its number in the source, the world after the call and the arguments. A failure that names a flow goes to the interface owner.

How much each layer catches

Flip one comparison in Cards.borrow, >= 3 to > 3 (the program is book/failures/verifier-flipped.vish):

verify FAILED: 4 failure(s):
[1] Cards.borrow example 2: Cards.borrow: invariant violated after case 2: state Cards { cards: [Card { id: 7, holding: 4 }] } args (7,)
[2] sampled check failed: Cards.borrow: Cards.borrow: invariant violated after case 2: state Cards { cards: [Card { id: 2, holding: 3 }, Card { id: 3, holding: 0 }, Card { id: 5, holding: 2 }, Card { id: 6, holding: 3 }, Card { id: 7, holding: 2 }, Card { id: 10, holding: 3 }, Card { id: 11, holding: 4 }, Card { id: 12, holding: 3 }, Card { id: 14, holding: 3 }] } args (11,)
  (state Cards { cards: [Card { id: 2, holding: 3 }, Card { id: 3, holding: 0 }, Card { id: 5, holding: 2 }, Card { id: 6, holding: 3 }, Card { id: 7, holding: 2 }, Card { id: 10, holding: 3 }, Card { id: 11, holding: 3 }, Card { id: 12, holding: 3 }, Card { id: 14, holding: 3 }] } args (11,))
[3] flow borrow: Cards.borrow: invariant violated after case 2: state Cards { cards: [Card { id: 6, holding: 0 }, Card { id: 14, holding: 4 }] } args (14,)
  (world World { shelf: Shelf { tools: [Tool { id: 6, copies: 8, lent: 3 }, Tool { id: 14, copies: 4, lent: 0 }, Tool { id: 15, copies: 9, lent: 0 }] }, cards: Cards { cards: [Card { id: 6, holding: 0 }, Card { id: 14, holding: 3 }] }, board: Board { notice: "" },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (14, 6))
[4] Cards.borrow: outcome `AtLimit` is never produced by an example or a sampled call (1000 sampled calls): an unreachable case, or a state the sampler does not draw; add an example that produces it

Four reports. The first is an example, the interface author’s { cards: [Card { id: 7, holding: 3 }] } (7) => AtLimit;, and it would fail at any sample count. Drop the guard altogether instead, and the program never runs: the stub declares fail AtLimit, and the checker refuses a body in which no case produces it (the program is book/refusals/verifier-dropped-guard.vish, the stub and the part assembled):

book/refusals/verifier-dropped-guard.vish:18:5: error: `Cards.borrow` declares `fail AtLimit` and no case produces it; a case must produce each declared outcome
      contract borrow(card: id) {
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^

A mutation study measured this on a larger program, the issue tracker of Where the tests went: twelve bugs of the kind writers make, planted by hand in its contract bodies, each verified at 300 and at 3,000 iterations on one seed (experiments/research/mutation-study.md):

what killed itbugswhich
the checker1a dropped guard: a declared failure no case produced
an example9caps off by one, flipped comparisons, a dropped += 1, a dropped duplicate guard, rows left behind
a scenario1a milestone closed with an issue still open
a sampled walk1a status forgotten in “is open”; killed at 3,000, missed at 300

Ten of twelve died whatever the sample count, and every example that killed one was the interface author’s, written because every declared failure needs one. An earlier run against a number pool, whose interface had few examples, killed nine of twelve; the three survivors were rules nobody had written down. The power is in the specification, examples per declared outcome and outcomes declared, and the sample count matters at the margin: one bug in twelve. It is one study, one seed, twelve bugs chosen by hand; the full matrix across seeds and sizes has not been run.

Try

In book/failures/verifier-flipped.vish, delete the AtLimit example. Three reports remain, two sampled calls and the unproduced AtLimit: the rule holding <= 3 still catches the flipped comparison, now only through sampling. Then delete that invariant too. vishy verify passes, at a thousand and at three thousand, with the bug in plain sight. The samples found it only while a rule said what to look for. The next chapter is about what nothing says.

What the verifier cannot see

The verifier checks that a program’s bodies, examples, scenarios and rules agree with each other in every state it samples. That leaves four kinds of mistake it cannot see, by construction, whatever it samples. This chapter shows each one passing, then says what closes it and what does not. The list is the one in the repository’s docs/blind-spots.md, with a run for each.

A rule that is wrong or missing

Here is the lending program of the last chapter with another scenario in place of its own (the program is book/programs/verifier-anycard.vish):

scenario a_mixup {
    Shelf.add(1, 1) => Added; Shelf.add(2, 1) => Added;
    Cards.issue(7) => Issued; Cards.issue(8) => Issued;
    borrow(7, 1) => Lent;
    borrow(8, 2) => Lent;
    bring_back(8, 1) => Returned;
    on_loan() => Value([Tool { id: 2, copies: 1, lent: 1 }]);
}
scenario a_mixup
  Shelf.add(1, 1) => Added
  Shelf.add(2, 1) => Added
  Cards.issue(7) => Issued
  Cards.issue(8) => Issued
  borrow(7, 1) => Lent
  borrow(8, 2) => Lent
  bring_back(8, 1) => Returned
  on_loan() => Value([Tool { id: 2, copies: 1, lent: 1 }])
  Shelf = Shelf { tools: [Tool { id: 1, copies: 1, lent: 0 }, Tool { id: 2, copies: 1, lent: 1 }] }
  Cards = Cards { cards: [Card { id: 7, holding: 1 }, Card { id: 8, holding: 0 }] }
  Board = Board { notice: "" }
8 step(s), all as expected
verify: 8 examples, 8 scenario steps and 12000 sampled checks passed

Card 8 borrowed the saw and brought back the drill that card 7 borrowed. Every rule the program states still holds: no tool has more copies lent than it owns, no card holds more than three, and the copies lent equal the tools held. The rule the library means, that a member brings back only what they borrowed, is written nowhere, so no example, scenario or sample can find it broken; the scenario above even asserts the wrong behaviour, and passes.

Nothing in the toolchain closes this. The interface author closes it by writing the rule, here a loan row per card and tool kept by the flows, and the reviewer closes it by reading the interface against the brief. In the mutation study’s earlier run, the three planted bugs that survived out of twelve were exactly this: rules nobody had written.

A rule can also be one you believe you wrote. A bound on a stored field is the range the sampler draws from, not a rule checked after a call:

row Card { id: id, holding: int(0, 3) }
unit Cards {
    state cards: Card[16];
    contract issue(card: id) => Issued: cards += [Card { id: card, holding: 0 }];
    contract borrow(card: id) {
        case cards[card].holding > 3 => fail AtLimit;
        else => Borrowed: cards[card].holding += 1;
    }
}
scenario a_greedy_member {
    Cards.issue(7) => Issued;
    Cards.borrow(7) => Borrowed; Cards.borrow(7) => Borrowed; Cards.borrow(7) => Borrowed; Cards.borrow(7) => Borrowed;
}
scenario a_greedy_member
  Cards.issue(7) => Issued
  Cards.borrow(7) => Borrowed
  Cards.borrow(7) => Borrowed
  Cards.borrow(7) => Borrowed
  Cards.borrow(7) => Borrowed
  Cards = Cards { cards: [Card { id: 7, holding: 4 }] }
5 step(s), all as expected
verify: 0 examples, 5 scenario steps and 4000 sampled checks passed

The field is typed int(0, 3) and the card holds four. Only an invariant is checked after every call; add invariant all(c in cards: c.holding <= 3); and the fifth step fails.

Doing nothing is safe

Every check the sampler runs is a safety check: after the call, does the rule still hold? A contract that changes nothing breaks no rule. Here lend answers Lent and lends nothing (the program is book/failures/verifier-lazy.vish):

row Tool { id: id, copies: int(1, 9), lent: int(0, 9) }
unit Shelf {
    state tools: Tool[16];
    invariant all(t in tools: t.lent <= t.copies);
    fixture Drill = { tools: [Tool { id: 1, copies: 2, lent: 1 }] };
    contract lend(tool: id) {
        case tools[tool].lent >= tools[tool].copies => fail NoneLeft;
        else => Lent;
        examples {
            Drill (1) => Lent { tools: [Tool { id: 1, copies: 2, lent: 2 }] };
            { tools: [Tool { id: 1, copies: 2, lent: 2 }] } (1) => NoneLeft;
        }
    }
}
verify FAILED: 1 failure(s):
[1] Shelf.lend example 1: expected state Shelf { tools: [Tool { id: 1, copies: 2, lent: 2 }] }, got Shelf { tools: [Tool { id: 1, copies: 2, lent: 1 }] }

One failure, and it is the example’s. The thousand sampled calls found nothing, because the lazy body keeps the invariant perfectly. What closes this hole is the rule that examples say what changed: a success example states the state after the call, and a body that does not produce it fails, whatever the sample. The other half is reachability, from the last chapter: a success case whose guard is never true never produces its outcome, and the verdict says so. What does not close it is more sampling.

A carried value with no oracle

A carried value, Count(n) or Price(p), has nothing to be held to in a sampled state: the invariant speaks about the state, not the answer. It is checked where an example or a scenario step states the expected number, and on every sampled call only when the contract states a property of result with ensures. The Contract anti-patterns page shows a count that includes closed rooms passing its examples and two thousand sampled checks, then failing on the first sampled state with a closed room once the property is written. So ensures closes the hole for the property it states, and a scenario assertion closes it on the states the scenario visits. Neither closes it for a property nobody states.

Trusted code

Under vishy verify, an effect’s Rust body never runs. The verifier answers an effect call from the effect’s fixtures, and for any other arguments with a random value of its type:

row Card { id: id, fees: int(0, 9999) }
unit Cards {
    state cards: Card[16];
    contract issue(card: id) => Issued: cards += [Card { id: card, fees: 0 }];
    contract charge(card: id, cents: int(0, 999)) => Charged: cards[card].fees += cents;
}
// the late fee: ten cents a day after the first fourteen days
effect late_fee(days: int(0, 60)) -> int(0, 999)
  impl { days * 10 }
  fixtures { (20) => 60; (10) => 0; }
flow check_in(card: id, days: int(0, 60)) atomic { let fee = late_fee(days); Cards.charge(card, fee); }
scenario a_late_drill { Cards.issue(7) => Issued; check_in(7, 20) => Charged; check_in(7, 10) => Charged; }
scenario a_late_drill
  Cards.issue(7) => Issued
  check_in(7, 20) => Charged
  check_in(7, 10) => Charged
  Cards = Cards { cards: [Card { id: 7, fees: 60 }] }
3 step(s), all as expected
verify: 0 examples, 3 scenario steps and 4000 sampled checks passed

The fixtures say twenty days cost 60 and ten days cost nothing, which is the rule in the comment. The body says days * 10, which is 200 for twenty days; the service runs the body. Nothing compares the two. The random answers close part of the hole: the program’s rules must hold for any answer the effect gives. The body itself is ordinary Rust, tested the ordinary way. The same holds for the runtime: the generated store, door and JSON are compiler output, held by the repository’s regression scripts, not by the verifier, and a served flow does not re-check the world rules on each request.

What more samples buy

More samples reach deeper states: the one planted bug in the mutation study that depended on sampling was found at 3,000 iterations and missed at 300, which is why the handbook asks for three seeds at 3,000 before anything is served. More samples never buy a rule nobody wrote, a value nobody stated, a state larger than a table’s declared size, or a line of an effect’s Rust. And the verdict is never a proof: it is a thousand tries per contract, flow and view, that found nothing.

What to write, therefore

  • An example for every declared outcome, and for every success the state after it.
  • A scenario for every rule the product promises, reaching each failure through a flow.
  • An ensures wherever a contract’s answer, or a function’s result, matters.
  • An invariant for every bound you mean as a rule.

Try

In book/failures/verifier-lazy.vish, shorten the first example to Drill (1) => Lent;. The lazy body passes: an example with no after-state means nothing changed, and nothing did. Now give lend its real delta, else => Lent: tools[tool].lent += 1;, and the same example fails: it gives no after-state, which means unchanged, but the state changed. An example without an after-state is a claim, and the verifier holds you to it.

Storage and the door

Until now every program lived for one command, and its state was gone when the command ended. This chapter keeps the state between calls and puts a door in front of it: HTTP routes that call flows and answer in JSON.

events { Opened(day: int) }
row Loan { id: id, holder: id, days: int(1, 28) }
unit Desk {
    caps storage, outbox;
    state day: int = 0;
    state open: bool = false;
    contract open_day() {
        case open => fail AlreadyOpen;
        else => Opened: open := true, day += 1, emit Opened(day + 1);
    }
}
unit Loans {
    caps storage;
    state loans: Loan[8];
    fixture Three = { loans: [Loan { id: 1, holder: 7, days: 7 }, Loan { id: 2, holder: 7, days: 7 }, Loan { id: 3, holder: 7, days: 7 }] };
    contract lend(loan: id, holder: id, days: int(1, 28)) {
        case count(l in loans: l.holder == holder) >= 3 => fail TooMany;
        else => Lent: loans += [Loan { id: loan, holder, days }];
        examples { Three (4, 7, 14) => TooMany; }
    }
}
flow open_day() atomic { Desk.open_day(); }
flow lend(loan: id, days: int(1, 28)) uses caller atomic { Loans.lend(loan, caller, days); }
view my_loans() uses caller = where(l in Loans.loans: l.holder == caller);
route POST "/open" => open_day();
route POST "/loans" => lend(loan, days);
route GET "/me/loans" => my_loans();
scenario a_morning {
    open_day() => Opened;
    open_day() => AlreadyOpen;
    as 7 lend(1, 14) => Lent;
    as 7 lend(1, 7) => Exists;
    as 8 lend(2, 21) => Lent;
    as 7 my_loans() => Value([Loan { id: 1, holder: 7, days: 14 }]);
}

A lending shelf: Desk opens and says so with an event, Loans lends at most three loans to each caller, and a view lists the caller’s own. The scenario runs as it always has:

scenario a_morning
  open_day() => Opened
  open_day() => AlreadyOpen
  lend(1, 14) => Lent
  lend(1, 7) => Exists
  lend(2, 21) => Lent
  my_loans() => Value([Loan { id: 1, holder: 7, days: 14 }])
  Desk = Desk { day: 1, open: true, outbox: [Opened { day: 1 }] }
  Loans = Loans { loans: [Loan { id: 1, holder: 7, days: 14 }, Loan { id: 2, holder: 8, days: 21 }] }
6 step(s), all as expected
verify: 1 examples, 6 scenario steps and 5000 sampled checks passed

What caps storage changes

caps storage; on a unit makes its state persist. Nothing else in the unit changes: its contracts, examples and verdict are the same with or without it. What changes is what the compiler writes for you. vishy schema book/programs/storage-shelf.vish prints the tables, on 30 Sept 2026:

CREATE TABLE IF NOT EXISTS desk_scalars (tenant TEXT NOT NULL PRIMARY KEY, `day` INTEGER NOT NULL DEFAULT 0, `open` INTEGER NOT NULL DEFAULT 0);
CREATE TABLE IF NOT EXISTS loans_loans (tenant TEXT NOT NULL, `id` INTEGER NOT NULL, `holder` INTEGER NOT NULL, `days` INTEGER NOT NULL, PRIMARY KEY (tenant, id));
CREATE TABLE IF NOT EXISTS tokens (tenant TEXT NOT NULL, token TEXT NOT NULL, caller INTEGER NOT NULL, PRIMARY KEY (tenant, token));

One row of scalars per unit, one table per keyed table, and a tokens table because a flow says uses caller. Every row carries a tenant, a string naming whose data it is; a request never sees another tenant’s rows. You write neither the schema nor the code that reads and writes it.

The generated service

vishy service book/programs/storage-shelf.vish out sqlite writes a Rust crate; cargo build --release in out makes one binary, service. Every call to it names a store (a SQLite file, or a Turso URL) and a tenant. Three commands start it:

./target/release/service shelf.db acme migrate
./target/release/service shelf.db acme token 7
./target/release/service shelf.db acme serve 127.0.0.1:8080

migrate creates the tables and answers migrated (store at version 0: tables created where missing); it is safe to run again. token 7 prints a bearer token for caller 7 and keeps it in tokens. serve answers until stopped. Each flow is one transaction: the units it touches are loaded, and written back only when it succeeded. The same binary runs a flow from the command line, service shelf.db acme open_day; docs/deploy.md covers supervisors and backups.

The door’s answers

The check that keeps this guide true builds this service, starts it, and calls the first POST route with an empty JSON body:

POST /open
{"events":[{"day":1,"event":"Opened"}],"ok":true,"outcome":"Opened"}

A success is 200 with ok, the outcome the contract named, and the events the flow emitted, each an object with its name under event. The rest of the door, from one session with the same service on 30 Sept 2026 (a token minted for caller 7):

POST /open  {}                               409 {"events":[],"ok":false,"outcome":"AlreadyOpen"}
POST /loans {"loan":1,"days":14}  no token    401 {"error":"unauthorized: a bearer token is required","flow":"lend"}
POST /loans {"loan":1,"days":30}  token       400 {"error":"`days`: expected an integer in 1..28, got 30","flow":"lend"}
POST /loans {"loan":1,"days":14}  token       200 {"events":[],"ok":true,"outcome":"Lent"}
POST /loans {"loan":2,"days":7}   token       200 {"events":[],"ok":true,"outcome":"Lent"}
GET  /me/loans?limit=1&offset=1   token       200 {"offset":1,"ok":true,"total":2,"value":[{"days":7,"holder":7,"id":2}]}

A failed outcome is 409 with the same shape as a success: the program working as written, and nothing stored. A flow that uses caller answers 401 without a token; the caller comes from the token, never from the body. A value outside a bound is 400 naming the parameter and the bound, int(1, 28) written once and enforced here as in the verifier. A GET route to a view reads the stored world with no transaction, and a list comes in pages: limit and offset in the query, total in the answer. Every error body names the flow.

One answer this program cannot give: a panic inside a flow answers 500 with internal: and the message, and writes nothing. A verified contract does not panic; the Rust inside an effect can, and the next chapter shows it.

Bounds are for the verifier

Loan[8] does not mean eight loans; it is how large the verifier lets the table grow in its sampled states. The generated service has no such limit: on its command line, ten loans across four callers were all Lent. A rule that really is about the table’s size says cap(loans), which is the bound while verifying and unbounded in a generated service, so a test size never becomes a production limit.

The development door

Building a service takes a minute. vishy serve <inputs> [addr] --db <file> [--tenant <t>] [--mint <caller>] serves the program straight from source, in the same tables, and --mint 7 prints a token for caller 7 into the file. On this program the six requests above gave the same six answers.

It watches its inputs between requests. Saving the program with >= 2 in place of >= 3 logged {"kind":"reload","ok":true,"ms":19,"version":0}, and the next loan for caller 7 answered TooMany. A save swaps the program in between two requests, so no request is lost; on a thirty-unit program the reload was measured on 30 Sept 2026 at 420 to 513 ms. A save that does not check is logged and the old program keeps serving; so is a change to a stored row without version N;, which the chapter on versions explains.

An input written as a quoted pattern, 'parts/*.vish', is expanded again on every check, so a part saved later is picked up. A program that still has a stub is refused: with one part moved out of the folder, the reload answered the program still has stubs (a part is missing from the door's inputs): Bookings.book, Bookings.cancel, Bookings.clear_room, and moving it back reloaded. A flow that fails after a keep step saves what the kept step wrote: served from a file, a refused attempt answered 409 and was still listed after a restart.

One difference remains between the two doors on 30 Sept 2026: vishy serve still stops a table at its verification bound, answering Full on a ninth loan here, where the generated service lent ten.

Try

Add case len(loans) >= cap(loans) => fail ShelfFull; as the first case of lend. vishy verify still passes: the sampler fills the shelf to eight and reaches the new case. Then read the rule again as the service will: cap(loans) has no bound there, so the case never fires in production, and a real limit is written as a number.

Effects and deliveries

A Vishy program has no input or output of its own: no file, no clock it did not declare, no network. The world comes in and goes out through two declared openings. An effect is a function written in Rust that a flow calls, for a fact the program cannot compute. A delivery says where an emitted event goes once the flow has committed. This chapter has one of each.

rust email_address = "0.2";
// the crate's parser decides whether an address is well formed; runs only in the service
effect well_formed(addr: string(64)) -> bool
    impl { email_address::EmailAddress::is_valid(&addr) }
    fixtures { ("ada@example.com") => true; ("ada at example") => false; }
events { Subscribed(subscriber: id) }
row Subscriber { id: id, addr: string(64) }
unit List {
    caps storage, outbox;
    state subscribers: Subscriber[16];
    contract add(subscriber: id, addr: string(64), well_formed: bool) {
        case !well_formed => fail BadAddress;
        else => Subscribed: subscribers += [Subscriber { id: subscriber, addr }], emit Subscribed(subscriber);
    }
}
flow subscribe(subscriber: id, addr: string(64)) atomic {
    let ok = well_formed(addr);
    List.add(subscriber, addr, ok);
}
deliver Subscribed to webhook env "CRM_HOOK";
deliver Subscribed to mail "list@example.com";
scenario two_addresses {
    subscribe(1, "ada@example.com") => Subscribed;
    subscribe(2, "ada at example") => BadAddress;
}

An effect

effect well_formed(addr: string(64)) -> bool is declared like a function, with a typed header, and has two bodies. impl { … } is the Rust that runs in a service. fixtures { … } are literal answers for literal arguments, used when the program is verified. rust email_address = "0.2"; names the crate the Rust uses, and the generated service’s manifest gets exactly that line.

That is how a Rust crate enters a Vishy program: inside an effect, behind a header the rest of the program can read. The program sees (string(64)) -> bool. It never sees the parser. (The language has one other place for Rust, a function with an impl body, kept as an escape hatch; vishy verify refuses to run one, and this guide never uses it.)

Flows call effects; contracts do not

subscribe calls the effect, binds its answer with let, and passes it to List.add as an ordinary argument. The contract decides what an unusable address means, fail BadAddress; the effect only reports a fact. A contract that asks the effect itself is refused:

effect well_formed(addr: string(64)) -> bool
    impl { addr.contains('@') }
    fixtures { ("ada@example.com") => true; }
row Subscriber { id: id, addr: string(64) }
unit List {
    state subscribers: Subscriber[16];
    contract add(subscriber: id, addr: string(64)) {
        case !well_formed(addr) => fail BadAddress;
        else => Subscribed: subscribers += [Subscriber { id: subscriber, addr }];
    }
}
book/refusals/effects-in-contract.vish:8:15: error: unknown function `well_formed`
          case !well_formed(addr) => fail BadAddress;
                ^^^^^^^^^^^

Inside a contract the effect’s name does not exist. A contract reads its unit’s state and its arguments and nothing else, which is what lets the verifier call it from any state it likes. The first removal holds here too: nothing outside the unit can reach in, and a contract cannot reach out.

In verification the effect never runs

scenario two_addresses
  subscribe(1, "ada@example.com") => Subscribed
  subscribe(2, "ada at example") => BadAddress
  List = List { subscribers: [Subscriber { id: 1, addr: "ada@example.com" }], outbox: [Subscribed { subscriber: 1 }] }
2 step(s), all as expected
verify: 0 examples, 2 scenario steps and 2000 sampled checks passed

The scenario’s two calls got their answers from the fixtures. In the sampled checks, where no fixture matches the argument, the verifier samples the effect’s answer by its type, so List.add is checked with both true and false for any address. The Rust is never compiled or called. To see how little the verdict depends on it, replace the impl body with panic!("the address checker is down"): on 30 Sept 2026 vishy verify still answered verify: 0 examples, 2 scenario steps and 2000 sampled checks passed.

What the verifier trusts is the header and the fixtures. An effect that can answer anything its type allows has to be met by a contract that handles anything its type allows.

In the service the Rust runs

Built with vishy service and cargo build --release, the same program ran its Rust. On the service’s command line, 30 Sept 2026:

$ service e.db acme subscribe 1 ada@example.com
Subscribed
event Subscribed { subscriber: 1 }
$ service e.db acme subscribe 3 'ada@@example.com'
BadAddress

No fixture mentions ada@@example.com; the crate’s parser refused it, and the contract turned that into the program’s own failure (exit status 4 on the command line, 409 at a door). The version whose Rust panics, given a route and served, answered each request with 500 {"error":"internal: the address checker is down","flow":"subscribe"}, served the next request, and afterwards dump showed no subscriber and outbox no event: a panic inside a flow writes nothing.

Deliveries

emit Subscribed(subscriber) puts an event in the unit’s outbox; caps outbox allows it. Where the event then goes is one line per destination:

  • deliver Event to webhook "https://…"; posts it to a URL,
  • deliver Event to webhook env "NAME"; reads the URL from an environment variable when it delivers,
  • deliver Event to mail "ops@example.com"; posts {to, subject, event} to the endpoint in VISHY_MAIL_HOOK,
  • deliver Event to effect name; calls a declared effect (body: string(n)) -> bool with the event as JSON.

The program declares the destinations; the service does the delivering. When the flow commits, the service writes one outbox row per event and destination in the same transaction as the state change, so an event is never lost and never recorded without its cause. A drainer, run by serve every 500 ms or by deliver once, posts each row. From the same session:

$ service e.db acme outbox
1 Subscribed -> webhook-env:CRM_HOOK attempts 0 delivered_at - error -
2 Subscribed -> mail:list@example.com attempts 0 delivered_at - error -
$ service e.db acme deliver
… "attempt":1,"ok":false,"error":"no destination for `webhook-env:CRM_HOOK` (set the environment variable)","next_in_ms":1000}
… "attempt":1,"ok":false,"error":"no destination for `mail:list@example.com` (set the environment variable)","next_in_ms":1000}
delivered 0, failed 2 (retried later)
$ CRM_HOOK=http://127.0.0.1:18741/crm VISHY_MAIL_HOOK=http://127.0.0.1:18741/mail service e.db acme deliver
… "attempt":2,"ok":true}
… "attempt":2,"ok":true}
delivered 2, failed 0 (retried later)

The listener received {"event":"Subscribed","subscriber":1} with the header Idempotency-Key: acme-1, the tenant and the row, the same on every retry. A failure waits and tries again, doubling the wait from one second to a few minutes at most, for up to twenty attempts, and then stays listed by outbox for a person to read. Delivery is at least once; the key is how a receiver drops a repeat. An effect destination is delivered when it answers true and retried when it answers false or panics, which makes any crate a destination: a queue client, a broker, a mail library, with the Rust inside the effect and the choice of destination one line of Vishy.

The verifier delivers nothing. What it can check is that the right events were emitted: the run above ends with outbox: [Subscribed { subscriber: 1 }], and a contract can say ensures emitted([…]).

Try

Change the fixture ("ada at example") => false to => true. The scenario’s second step now gets Subscribed, and vishy verify fails naming that step and the world it reached. The fixtures are what the verifier believes about the world, so a wrong fixture is a wrong belief, found where it matters.

Versions and migrations

A stored program outlives its first text. The rows in the store were written by one version of the program, and the next version has to read them. In most stacks that is a migration script written by hand beside the code, and nothing checks that the two agree. In Vishy the program says which version it is, and the compiler carries the store from one version to the next: it derives what it can, and it refuses to guess the rest.

version N; on day one

version 1; at the top of a program numbers the shape of what it stores. A program without the line runs, and is stored as version 0; the first change to a stored row is then refused until a version exists. Here the day-one program had no version line, and the next one adds a field renewed: bool to Loan. vishy check new.vish --from=old.vish checks a program against the one that wrote the store:

book/refusals/migrate/versions-noversion.vish:1:1: error: the new program declares no `version N;` (needed to migrate a store)
  row Loan { id: id, holder: id, days: int(1, 28), note: string(32), renewed: bool }
  ^

The development door refuses the same save with the same sentence. The fix is one line, so the advice is to write it before there is anything to fix: with version 1; from the first day, every later change to a stored shape is a number going up, and the store records which program wrote it.

Version 2

The shelf’s first version stored a loan’s length in days, kept a free-text note, and had a Desk unit:

version 1;
row Loan { id: id, holder: id, days: int(1, 28), note: string(32) }
unit Loans {
    caps storage;
    state loans: Loan[8];
    contract lend(loan: id, holder: id, days: int(1, 28)) => Lent: loans += [Loan { id: loan, holder, days, note: "" }];
}
unit Desk {
    caps storage;
    state open: bool = false;
    contract open_day() => Opened: open := true;
}
flow lend(loan: id, holder: id, days: int(1, 28)) atomic { Loans.lend(loan, holder, days); }
flow open_day() atomic { Desk.open_day(); }

The second counts weeks, drops the note, adds renewed, and has no desk:

version 2;
row Loan { id: id, holder: id, weeks: int(1, 4), renewed: bool }
migrate Loan from 1 { weeks = (old.days + 6) / 7; drop note; }
    examples {
        { id: 1, holder: 7, days: 10, note: "blue cover" } => { id: 1, holder: 7, weeks: 2, renewed: false };
        { id: 2, holder: 8, days: 28, note: "" } => { id: 2, holder: 8, weeks: 4, renewed: false };
    }
migrate drop Desk;
unit Loans {
    caps storage;
    state loans: Loan[8];
    contract lend(loan: id, holder: id, weeks: int(1, 4)) => Lent: loans += [Loan { id: loan, holder, weeks, renewed: false }];
}
flow lend(loan: id, holder: id, weeks: int(1, 4)) atomic { Loans.lend(loan, holder, weeks); }

migrate Loan from 1 { … } says how a version 1 row becomes a version 2 row. Inside it, old is the row as version 1 declared it, typed against that declaration, so old.days is checked and old.weeks would be an error. weeks = (old.days + 6) / 7; computes the new field. drop note; says out loud that the notes are being thrown away. renewed needs no line: a field added with a default (false, 0, the empty string, none, an enum’s first variant) is derived. So are a widened bound, a new table and a new stored scalar. migrate drop Desk; allows the tables of a unit that no longer exists to be dropped.

The examples give an old row and the row it must become, and they run when the program is checked:

migrate: 3 step(s) from version 1 to 2; 2 example(s) passed; unit Desk dropped
ok: 1 row(s), 0 event(s), 0 fn(s), 1 unit(s), 1 contract(s), 1 flow(s)

The steps are the loans table rebuilt (computing weeks, leaving out note, filling renewed) and the desk’s tables dropped. The verifier does not sample migrations; these examples are their test.

Data loss is never derived

Remove a field and say nothing, and the check stops:

book/refusals/migrate/versions-dataloss.vish:1:1: error: `Loan.note` is gone and its data would be dropped; say so with `migrate Loan from 1 { drop note; }`
  version 2;
  ^

The message names the line that would make the loss a decision. When a field of the same type appears beside the one that vanished, the compiler cannot tell a rename from a loss, and says both lines; the anti-patterns chapter shows that case. Either way nothing is lost that no line of the program asked to lose, which is the second removal applied to the store: nothing changes that the program did not name.

A store carried once

vishy service new.vish out --from=old.vish writes a service whose migrate carries a store at the old version forward exactly once. With the version 1 service and the version 2 service built side by side, on 30 Sept 2026:

$ v1/service shelf.db acme migrate
migrated (fresh store at version 1)
$ v1/service shelf.db acme lend 1 7 10
Lent
$ v1/service shelf.db acme lend 2 8 28
Lent
$ v2/service shelf.db acme migrate
migrated from version 1 to 2
$ v2/service shelf.db acme dump
World { loans: Loans { loans: [Loan { id: 1, holder: 7, weeks: 2, renewed: false }, Loan { id: 2, holder: 8, weeks: 4, renewed: false }] }, … }
$ v2/service shelf.db acme migrate
migrated (store already at version 2)
$ v1/service shelf.db acme migrate
migrate: API misuse: `the store is at version 2, this service is version 1; generate it with `vishy service … --from=<the version 2 program>` to migrate`

The rows came across as the examples promised, running migrate again changed nothing, and the old binary refused the newer store with both versions named. The development door does the same in place, for its one tenant, when a changed program is saved; in the storage chapter’s session, saving the shelf with version 1; and a new field reloaded with the loans carried across and "renewed":false on each.

What a per-row expression cannot say is not covered, by design: tables Vishy did not create, a migration that needs another table or data from outside, a backfill in batches while the service keeps answering. Those are written by hand, as an effect or a script. A recomputed field rebuilds the whole table, so on a very large one run it in a quiet window.

Clients: vishy diff

version N; numbers the store. Callers see a different number: the version in a package’s package.vishy, which the chapter on many files introduces. vishy diff old new compares the two packages’ public surfaces, classifies the change, and refuses a version number that does not cover it. The storage chapter’s shelf as a package at 1.0.0, then with a view added, then with lend narrowed to days: int(1, 14), both at 1.1.0:

$ vishy diff shelf-1.0 shelf-1.1
+ view shelf::loans_of(holder: id)
Minor change: shelf 1.0.0 -> 1.1.0
version covers the change
$ vishy diff shelf-1.0 shelf-1.1-weeks
- flow shelf::lend(loan: id, days: int(1,28)) uses caller
+ flow shelf::lend(loan: id, days: int(1,14)) uses caller
Major change: shelf 1.0.0 -> 1.1.0
error: the interface change is Major but the version went 1.0.0 -> 1.1.0; a major bump is required

A new view breaks no caller. A narrower bound breaks every caller that sends 20, so it is major. Regenerate the clients with vishy client whenever the answer is not a patch.

Try

In versions-shelf.vish, change the first example’s weeks: 2 to weeks: 1 and check it with --from. The check stops with migrate Loan from 1 example 1: expected Loan { id: 1, holder: 7, weeks: 1, renewed: false }, got Loan { id: 1, holder: 7, weeks: 2, renewed: false }: ten days is two weeks by the rule that was written, and the example is what caught the disagreement before any store did.

Programs of many files

However many files a program is written in, it is one world: one set of units, one verifier’s verdict, one service. Files are how people share the writing. Namespaces keep their names apart, and packages bring in code from another directory or another repository.

Several files, one program

Every command takes any mix of files and directories. vishy check a.vish b.vish joins the files in the order given; vishy check dir/ takes every .vish file in the directory, in name order. A flow in one file calls a unit in another, and a diagnostic names the file and the line it is in. The check that keeps this guide true runs one file at a time, so the programs below that span files are shown as runs from 30 Sept 2026, and the refusals that fit in one file are included as usual.

A namespace

namespace pricing;
enum Tier { basic, gold }
// the fee a tier pays on an amount: three percent, one for gold
fn fee(amount: money(2), tier: Tier) -> money(2) = if tier == Tier.gold then percent(amount, 1) else percent(amount, 3);
examples fee { (10000, Tier.basic) => 300; (10000, Tier.gold) => 100; }
row Customer { id: id, tier: Tier }
unit Tiers {
    caps storage;
    state customers: Customer[8];
    contract set(customer: id, tier: Tier) {
        case customer in customers => Changed: customers[customer].tier := tier;
        else => Added: customers += [Customer { id: customer, tier }];
    }
    contract fee_for(customer: id, amount: money(2)) -> money(2) {
        case customer in customers => Fee(fee(amount, customers[customer].tier));
        else => Fee(fee(amount, Tier.basic));
    }
}
flow set_tier(customer: id, tier: Tier) atomic { Tiers.set(customer, tier); }
flow quote(customer: id, amount: money(2)) atomic { Tiers.fee_for(customer, amount); }
scenario gold_pays_less {
    set_tier(7, Tier.gold) => Added;
    quote(7, 10000) => Fee(100);
    quote(8, 10000) => Fee(300);
}

namespace pricing; at the top puts everything the file declares in the namespace pricing. Inside it, names are written bare, as in every program so far. A file’s namespace is the one its first line names, else the directory it was given in, so vishy check pricing/ shop/ makes two namespaces from two directories; a file given directly with no such line is in the root namespace, where every earlier chapter’s program lived.

scenario gold_pays_less
  pricing__set_tier(7, gold) => Added
  pricing__quote(7, 10000) => Fee(100)
  pricing__quote(8, 10000) => Fee(300)
  pricing::Tiers = pricing__Tiers { customers: [pricing__Customer { id: 7, tier: gold }] }
3 step(s), all as expected
verify: 2 examples, 3 scenario steps and 4000 sampled checks passed

The trace shows how the compiler spells a name inside the joined program: the namespace, two underscores, the name. That is why no name of yours may contain two underscores:

book/refusals/files-underscores.vish:1:5: error: `Price::Old`: names may not contain `__` (reserved)
  row Price__Old { id: id, cents: money(2) }
      ^^^^^^^^^^

A namespace exports its rows, enums, events, functions, units, flows and effects. It does not export state or contracts, which belong to their unit, so there is no pub to write: the first removal already says that nothing outside a unit reads its state.

use

Outside its namespace a name has a full name, pricing::Tier, and the compiler prints it that way. To write it in another namespace, import it. A second file:

namespace shop;
use pricing::Tiers as Pricing;
use pricing::*;
row Order { id: id, total: money(2), fee: money(2) }
unit Orders {
    state orders: Order[8];
    contract place(order: id, total: money(2), fee: money(2)) => Placed: orders += [Order { id: order, total, fee }];
}
flow place(order: id, customer: id, total: money(2)) atomic { let f = Pricing.fee_for(customer, total); Orders.place(order, total, f); }
scenario gold_order { set_tier(7, Tier.gold) => Added; place(1, 7, 10000) => Placed; }

use pricing::Tiers as Pricing; brings in one name under an alias, use pricing::*; brings in every name the namespace exports, and use pricing::fee; would bring in one name as it is. The flow place in shop asks a unit of pricing for a fee and hands it to its own unit. Checked and verified together:

$ vishy check pricing.vish shop.vish
ok: 2 row(s), 0 event(s), 1 fn(s), 2 unit(s), 3 contract(s), 3 flow(s)
$ vishy verify pricing.vish shop.vish 7 1000
verify: 2 examples, 5 scenario steps and 6000 sampled checks passed

Three refusals keep the names honest. A namespace nobody gave the compiler is unknown; here is the shop’s import of pricing checked without the pricing file:

namespace shop;
use pricing::fee;
use pricing::Tier;
row Order { id: id, total: money(2), charged: money(2) }
unit Orders {
    state orders: Order[8];
    contract place(order: id, total: money(2), tier: Tier) => Placed: orders += [Order { id: order, total, charged: total + fee(total, tier) }];
}
book/refusals/files-unknown.vish:2:5: error: unknown namespace `pricing` (namespaces come from `namespace x;` or the directories given to vishy)
  use pricing::fee;
      ^^^^^^^

A namespace does not import itself; its own names are already bare:

book/refusals/files-self.vish:2:1: error: a namespace does not import itself
  use pricing::Tier;
  ^^^^^^^^^^^^^^^^^^

And a name already visible is not imported over. With shop declaring an enum Tier of its own and also writing use pricing::Tier;, the two-file check answered:

three/shop.vish:2:1: error: `Tier` is already visible here as `shop::Tier`; give the import an alias
  use pricing::Tier;
  ^^^^^^^^^^^^^^^^^^

use pricing::Tier as PriceTier; checked. One name means one thing in a file, the same rule the chapter on outcomes gave for outcome names.

An interface and its parts

An interface with its parts is one program of several files, joined the same way: vishy check interface.vish parts/*.vish on the team-rooms interface and its two parts answered ok: 2 row(s), 0 event(s), 0 fn(s), 2 unit(s), 6 contract(s), 4 flow(s). The chapter on interfaces and parts says how they merge; the storage chapter shows the development door picking up a part saved later.

Packages

A directory with a package.vishy file is a package: its files are one namespace, named after the package unless the manifest names another with namespace = "…", and its dependencies come with it.

name = "shop"
version = "0.1.0"
dep pricing = path "../pricing"
dep billing = path "../billing"

dep pricing = path "../pricing" is another package on disk. dep rates = git "https://example.com/rates.git" tag "v2.0.1" is one in a repository, cloned at its tag into $VISHY_HOME/deps (by default ~/.vishy/deps). Dependencies are resolved all the way down, and a program has one version of each name. With shop needing pricing 1.0.0 and billing needing pricing 1.1.0, vishy check shop answered:

`pricing` is needed at 1.0.0 and at 1.1.0

There is no second copy to hide behind. The chapter on versions shows how vishy diff decides what the next version number of a package must be.

Try

In the shop file, delete the line use pricing::*; and check the two files again. The alias for Tiers still works, but the scenario’s first step is refused with unknown flow or view `set_tier` : a flow of another namespace is visible only when something imports it, and the check says which name was missing where.

The tools

Everything in this guide was done with one program, vishy. This page lists its commands in the order a developer meets them, one line each. Every option, and every command’s exact output, is in docs/toolchain.md.

commandwhat it answers
vishy check <inputs>Does the program make sense? Types, names, examples, and a count of what it declares. With --from=<old>, also the migration from the program that wrote the store; with --interface, whether every contract is still a stub.
vishy run <inputs>What do the scenarios do? Each step’s outcome, then each unit’s state.
vishy verify <inputs> [seed] [iterations]Does every rule hold? Examples, scenarios, then sampled calls from random and reachable states.
vishy graph <inputs>Which units are joined, by flows and by rules across units, and what the verifier re-checks after each flow. --json for other tools.
vishy tree <file>The file’s declarations as the compiler parsed them, one per line in a bracketed form.
vishy tokens <file>The words the compiler reads the file as, one per line with their kind and position.
vishy ir <inputs>Counts of the compiler’s internal form of the program; it can write that form to a file, and verify --from-ir runs the verifier from such a file with no program read.
vishy schema <inputs>The SQLite tables of every unit with caps storage.
vishy service <inputs> <dir> [sqlite]A Rust crate that runs the program as a stored, served application; --from=<old> makes its migrate carry an older store forward.
vishy client <inputs> <dir>A typed TypeScript client, an OpenAPI description and a Rust client crate, from the program’s routes.
vishy serve <inputs> [addr] --db <file>The development door: the routes served from source against a store, reloaded on every save.
vishy interface <inputs>The public surface: rows, enums, events, signatures with their outcomes, flows, routes, effects.
vishy brief <inputs> <Unit>What the writer of one unit is shown: the language, the shared declarations and the unit’s stubs.
vishy part <inputs> <Unit> <part.vish>One part checked alone against its interface: its examples and sampled checks.
vishy write <inputs> --out <dir>The writer stage: every unit’s part requested from a model at once, checked as it lands, assembled, verified and repaired.
vishy assemble <inputs> <out.vish>The interface and its parts written as one file; refused while any contract is a stub.
vishy diff <old> <new>Whether a package’s change is a patch, minor or major, and whether its new version number covers it.
vishy blocks <part.vish>The contract blocks found in a part, with their byte ranges; for debugging a part that will not split.

<inputs> is any mix of files, directories and packages, as the chapter on many files describes. One command reaches outside the machine: vishy write sends requests to a model endpoint with a key, and the toolchain page says where it looks for both.

Every promise is a script

The repository keeps one check script per feature under tests/, 75 of them on 30 Sept 2026. Each runs the commands its feature’s documentation states, on a real program, and fails when an answer changes: the door’s status codes, a migration carried forward exactly once, a delivery retried with the same key, a refusal’s wording. The documentation pages are held the same way, so a command written on a page is a command that was run. This guide is one more of them: tests/check_book.sh regenerates every answer these chapters include, builds and calls the door of every program with a POST route, and fails on the first line that differs.

A front end written in the language

The part of the compiler that reads a program and checks it is being written a second time, in Vishy. --front vishy on tree, check, verify or any command that reads a program runs the parser and the checker written in Vishy beside the ones written in Rust, and refuses when they disagree, naming the file and the first line that differs. On the storage chapter’s shelf program the answer is the ordinary one:

$ vishy check --front vishy book/programs/storage-shelf.vish
ok: 1 row(s), 1 event(s), 0 fn(s), 2 unit(s), 2 contract(s), 2 flow(s)

The two parsers are held equal on every program file in the repository, 2,632 files on 30 Sept 2026, and the two checkers the same way. The front end exists twice, once in the language it reads, and every file checked is a test of both.

Try

Run vishy tree book/programs/storage-shelf.vish and then the same with --front vishy. The two print the same lines: the first is the Rust parser’s reading of the program, the second the Vishy parser’s, compiled from the language this guide teaches.

If you come from domain-driven design

You already have the vocabulary. The mapping:

you sayVishy says
aggregate, aggregate rootunit with its state
entity, value objectrow; a row inside a row
command, and the invariant it must keepcontract, its outcomes, and the unit’s invariant
the command’s failurefail Outcome
application service, transactionflow (atomic across units)
read modelview
domain event, outboxemit, events, deliver
schema migrationversion and migrate
the aggregate boundary, by conventionthe aggregate boundary, enforced by the compiler

The surprise is the last row. A contract that reads another unit does not compile; there is no repository to reach through and no convention to keep. A fact one aggregate needs from another is carried by the flow as a value. The chapter on flows shows the shape; “What Vishy is not” shows the refusal.

The first program you would write, an order that is placed, paid and shipped, with its lifecycle as a machine:

enum Status { placed, paid, shipped }
row Order { id: id, customer: id, total: money(2), status: Status }
unit Orders {
    state orders: Order[16];
    machine orders.status { start Status.placed; Status.placed -> Status.paid; Status.paid -> Status.shipped; }
    invariant all(o in orders: o.total > 0);
    contract place(order: id, customer: id, total: money(2)) {
        case total <= 0 => fail EmptyOrder;
        else => Placed: orders += [Order { id: order, customer: customer, total: total, status: Status.placed }];
        examples {
            {} (1, 7, 2500) => Placed { orders: [Order { id: 1, customer: 7, total: 2500, status: Status.placed }] };
            {} (1, 7, 0) => EmptyOrder;
        }
    }
    contract pay(order: id, amount: money(2)) {
        case amount != orders[order].total => fail WrongAmount;
        else => Paid: orders[order].status := Status.paid;
        examples {
            { orders: [Order { id: 1, customer: 7, total: 2500, status: Status.placed }] } (1, 2500) => Paid { orders: [Order { id: 1, customer: 7, total: 2500, status: Status.paid }] };
            { orders: [Order { id: 1, customer: 7, total: 2500, status: Status.placed }] } (1, 100) => WrongAmount;
        }
    }
    contract ship(order: id) => Shipped: orders[order].status := Status.shipped;
}
flow place(order: id, customer: id, total: money(2)) atomic { Orders.place(order, customer, total); }
flow pay(order: id, amount: money(2)) atomic { Orders.pay(order, amount); }
flow ship(order: id) atomic { Orders.ship(order); }
scenario one_order {
    place(1, 7, 2500) => Placed;
    ship(1) => WrongStatus;
    pay(1, 100) => WrongAmount;
    pay(1, 2500) => Paid;
    ship(1) => Shipped;
}
scenario one_order
  place(1, 7, 2500) => Placed
  ship(1) => WrongStatus
  pay(1, 100) => WrongAmount
  pay(1, 2500) => Paid
  ship(1) => Shipped
  Orders = Orders { orders: [Order { id: 1, customer: 7, total: 2500, status: shipped }] }
5 step(s), all as expected
verify: 4 examples, 5 scenario steps and 6000 sampled checks passed

Three things to notice. The lifecycle is a machine, and ship before pay fails with WrongStatus without a guard being written. The invariant total > 0 is kept by place refusing EmptyOrder, and the verifier would find the state that breaks it if that case were missing. And the examples on each command are the acceptance tests, in the command.

If you come from SQL

The mapping:

you sayVishy says
table, rowrow and a keyed table, Employee[32], inside a unit
constraint, foreign key, checkinvariant, inside the unit or across units
transactionflow
viewview
computed column, materialised viewderived column
schema migrationversion and migrate
stored procedure, triggercontract: what may happen to the rows, with its outcomes

The surprise is that behaviour is in the language and checked before it runs. There is no query language: a view is an expression over the tables, and a rule is checked by the verifier in every sampled state, not by the database at commit time.

The first program you would write, departments and staff with a foreign key and a budget:

row Employee { id: id, name: string(64), dept: id, salary: money(2) }
row Dept { id: id, name: string(64), budget: money(2), payroll: money(2) }
unit Depts {
    caps storage;
    state depts: Dept[8];
    contract create(dept: id, name: string(64), budget: money(2)) {
        case budget <= 0 => fail BadBudget;
        else => Created: depts += [Dept { id: dept, name: name, budget: budget }];
        examples { {} (10, "Research", 1000000) => Created { depts: [Dept { id: 10, name: "Research", budget: 1000000 }] }; {} (10, "Research", 0) => BadBudget; }
    }
    // what the department can still spend; an absent department is Unknown
    contract room(dept: id) -> money(2) => Room(depts[dept].budget - depts[dept].payroll);
}
unit Staff {
    caps storage;
    state staff: Employee[32];
    contract hire(emp: id, name: string(64), dept: id, salary: money(2), room: money(2)) {
        case salary <= 0 => fail BadSalary;
        case salary > room => fail OverBudget;
        else => Hired: staff += [Employee { id: emp, name: name, dept: dept, salary: salary }];
        examples {
            {} (1, "Ada", 10, 500000, 1000000) => Hired { staff: [Employee { id: 1, name: "Ada", dept: 10, salary: 500000 }] };
            {} (1, "Ada", 10, 0, 1000000) => BadSalary;
            {} (1, "Ada", 10, 500000, 400000) => OverBudget;
        }
    }
}
// a computed column over the other table, kept by the compiler after every change
derived Depts.depts.payroll = for d => fold(e in where(x in Staff.staff: x.dept == d.id), acc = 0: acc + e.salary);
// the foreign key and the budget rule, as world invariants over both tables
invariant all(e in Staff.staff: any(d in Depts.depts: d.id == e.dept));
invariant all(d in Depts.depts: d.payroll <= d.budget);
flow create_dept(dept: id, name: string(64), budget: money(2)) atomic { Depts.create(dept, name, budget); }
// the department decides whether there is room (and whether it exists) before the hire is recorded
flow hire(emp: id, name: string(64), dept: id, salary: money(2)) atomic { let room = Depts.room(dept); Staff.hire(emp, name, dept, salary, room); }
view payroll(dept: id) = match lookup(Depts.depts, dept) { some d => some(d.payroll), none => none };
route POST "/depts" => create_dept(dept, name, budget);
route POST "/staff" => hire(emp, name, dept, salary);
route GET "/depts/{dept}/payroll" => payroll(dept);
scenario a_hire {
    create_dept(10, "Research", 1000000) => Created;
    hire(1, "Ada", 10, 500000) => Hired;
    hire(2, "Bob", 10, 600000) => OverBudget;
    hire(3, "Cy", 11, 100) => Unknown;
    payroll(10) => Value(some(500000));
}
scenario a_hire
  create_dept(10, "Research", 1000000) => Created
  hire(1, "Ada", 10, 500000) => Hired
  hire(2, "Bob", 10, 600000) => OverBudget
  hire(3, "Cy", 11, 100) => Unknown
  payroll(10) => Value(Some(500000))
  Depts = Depts { depts: [Dept { id: 10, name: "Research", budget: 1000000, payroll: 500000 }] }
  Staff = Staff { staff: [Employee { id: 1, name: "Ada", dept: 10, salary: 500000 }] }
5 step(s), all as expected
verify: 5 examples, 5 scenario steps and 6000 sampled checks passed

What the verifier taught while this program was written, in order. First, hire could reference a department that did not exist, because Staff cannot look into Depts: the foreign key is a world invariant, and the flow keeps it by asking Depts.room first, which fails Unknown for an absent department before anything is recorded. Second, payroll <= budget could break in two ways, a negative budget and a hire past the budget, so create refuses BadBudget and the flow carries the department’s remaining room into hire, which refuses OverBudget. Third, a view over a missing key panics, so payroll answers an option. Each of those is a state the verifier produced and showed.

Two honest notes for a SQL reader. payroll is a derived column because sum today takes an int column and a fold over money needs a typed context, which a derived column’s declared type gives and a view’s inferred type does not; that limit is recorded. And a stored program loads the units a flow touches, not the rows it needs, so tables of thousands of rows per tenant are fine and millions are not; the limits appendix has the numbers.

If you come from Elm or Redux

The mapping:

you sayVishy says
modela unit’s state
message, actiona contract call
update, reducerthe contract’s cases and deltas
the result of an updatethe contract’s outcomes
several updates that must happen togethera flow, atomic
a selector, a derived value for the viewa view

The surprise is that this is the same architecture on the server, with rules and a verifier over every reachable state. A reducer computes the next state from a message; a contract does the same and, in addition, names how it can refuse, keeps an invariant, and is checked from thousands of states it was never sent.

The first program you would write, a todo list:

row Todo { id: id, text: string(64), done: bool }
unit Todos {
    state todos: Todo[16];
    state filter_done: bool = false;
    contract add(todo: id, text: string(64)) {
        case len(text) == 0 => fail Empty;
        else => Added: todos += [Todo { id: todo, text: text, done: false }];
        examples { {} (1, "milk") => Added { todos: [Todo { id: 1, text: "milk", done: false }] }; {} (1, "") => Empty; }
    }
    contract toggle(todo: id) => Toggled: todos[todo].done := !todos[todo].done;
    contract set_filter(done: bool) => Set: filter_done := done;
    contract clear_done() => Cleared: todos -= where(t in todos: t.done).id;
}
flow finish_and_clear(todo: id) atomic { Todos.toggle(todo); Todos.clear_done(); }
view visible() = where(t in Todos.todos: !Todos.filter_done || t.done);
scenario a_list {
    Todos.add(1, "milk") => Added;
    Todos.add(2, "bread") => Added;
    Todos.toggle(1) => Toggled;
    visible() => Value([Todo { id: 1, text: "milk", done: true }, Todo { id: 2, text: "bread", done: false }]);
    finish_and_clear(2) => Cleared;
    visible() => Value([]);
}
scenario a_list
  Todos.add(1, "milk") => Added
  Todos.add(2, "bread") => Added
  Todos.toggle(1) => Toggled
  visible() => Value([Todo { id: 1, text: "milk", done: true }, Todo { id: 2, text: "bread", done: false }])
  finish_and_clear(2) => Cleared
  visible() => Value([])
  Todos = Todos { todos: [], filter_done: false }
6 step(s), all as expected
verify: 2 examples, 6 scenario steps and 8000 sampled checks passed

toggle is the reducer you know, written as a delta. finish_and_clear is two updates in one atomic step, which no reducer can express without a third message. visible is the selector, and the scenario asserts its value directly, so the view is tested with the model.

If you come from a client team: the wire contract

A team with a server and two client platforms reviews the same four things on every endpoint, by hand, twice: is a field null or absent; which revision of the API is this; is the answer one row or a page; what does the status field mean. They review them because the wire contract lives only in the server’s source, so every client re-derives it, and a reviewer checks each derivation.

In Vishy there are no data-transfer objects to write. vishy client projects the interface, the same declaration the verifier checked, into a TypeScript client, an OpenAPI description and a Rust crate, so both ends of the wire are projections of one text. Each of the four questions is answered once, in the language:

  • Null or absent. An option<T> is null or the value, never absent. Only an optional argument at the door may be omitted. A string(64) is a maxLength, an int(1, 366) a minimum and a maximum, and the door refuses a value outside them with 400 naming the parameter.
  • Revision. One, version N; in the program, with migrations for the store and vishy diff classifying an interface change as patch, minor or major and refusing a version number that does not cover it.
  • Row or page. One list shape everywhere: a view that returns a list answers {ok, value, total, offset} and takes limit and offset.
  • Result status. Every flow answers {ok, outcome, value?, events}; a client switches on outcome; a failed outcome is 409 with the same shape, and the client type is the union of the outcomes the flow can produce.

The SQL page’s program has three routes. vishy client book/programs/sql.vish out writes this TypeScript, unedited:

// Generated by `vishy client` from the interface. Do not edit; regenerate.
// Wire shape: 200 or 409 carry {ok, outcome, value?, events}; other statuses throw VishyError.

export interface Employee { id: number; name: string; dept: number; salary: number }
export interface Dept { id: number; name: string; budget: number; payroll: number }

export type Event = never;

export class VishyError extends Error {
  status: number; body: unknown;
  constructor(status: number, body: unknown) { super(`HTTP ${status}: ${JSON.stringify(body)}`); this.status = status; this.body = body; }
}

export type CreateDeptResult =
  | { ok: true; outcome: "Created"; events: Event[] }
  | { ok: false; outcome: "BadBudget"; events: Event[] }
  | { ok: false; outcome: "Unknown"; events: Event[] }
  | { ok: false; outcome: "Exists"; events: Event[] }
  | { ok: false; outcome: "Full"; events: Event[] }
  | { ok: false; outcome: "WrongStatus"; events: Event[] };
export type HireResult =
  | { ok: true; outcome: "Hired"; events: Event[] }
  | { ok: false; outcome: "BadSalary"; events: Event[] }
  | { ok: false; outcome: "OverBudget"; events: Event[] }
  | { ok: false; outcome: "Unknown"; events: Event[] }
  | { ok: false; outcome: "Exists"; events: Event[] }
  | { ok: false; outcome: "Full"; events: Event[] }
  | { ok: false; outcome: "WrongStatus"; events: Event[] };
export type PayrollResult = { ok: true; value: number | null };

export class VishyClient {
  baseUrl: string; token: string | undefined;
  constructor(baseUrl: string, token?: string) { this.baseUrl = baseUrl.replace(/\/+$/, ""); this.token = token; }

  /** `route POST "/depts" => create_dept`  */
  create_dept(dept: number, name: string, budget: number): Promise<CreateDeptResult> { return this.call("POST", `/depts`, { dept, name, budget }); }
  /** `route POST "/staff" => hire`  */
  hire(emp: number, name: string, dept: number, salary: number): Promise<HireResult> { return this.call("POST", `/staff`, { emp, name, dept, salary }); }
  /** `route GET "/depts/{dept}/payroll" => view payroll`  */
  payroll(dept: number): Promise<PayrollResult> { return this.call("GET", `/depts/${encodeURIComponent(String(dept))}/payroll`, {  }); }

  private async call(method: string, path: string, args: Record<string, unknown>): Promise<any> {
    const headers: Record<string, string> = { "content-type": "application/json" };
    if (this.token) headers["authorization"] = `Bearer ${this.token}`;
    const entries = Object.entries(args).filter(([, v]) => v !== undefined && v !== null);
    let url = this.baseUrl + path; let body: string | undefined;
    if (method === "GET" || method === "DELETE") {
      const q = new URLSearchParams(); for (const [k, v] of entries) q.set(k, Array.isArray(v) ? v.join(",") : String(v));
      const qs = q.toString(); if (qs) url += (url.includes("?") ? "&" : "?") + qs;
    } else { body = JSON.stringify(Object.fromEntries(entries)); }
    const res = await fetch(url, { method, headers, body });
    const json = await res.json().catch(() => null);
    if (res.status === 200 || res.status === 409) return json;
    throw new VishyError(res.status, json);
  }
}

HireResult is every outcome hire can answer, the declared ones and the four implicit ones, each with ok already decided; a switch on outcome narrows the type. PayrollResult carries number | null because the view answers an option. And the OpenAPI description of the /depts route, the program’s first POST route, from the same run:

{
 "POST": "/depts",
 "requestBody": {
  "properties": {
   "budget": {
    "format": "int64",
    "type": "integer"
   },
   "dept": {
    "format": "int64",
    "type": "integer"
   },
   "name": {
    "maxLength": 64,
    "type": "string"
   }
  },
  "required": [
   "dept",
   "name",
   "budget"
  ],
  "type": "object"
 },
 "responses": {
  "200": "the flow succeeded",
  "400": "a missing or malformed argument",
  "409": "the flow failed with a declared outcome; nothing was written"
 }
}

The bound on name is there as maxLength; the 409 says what a failed outcome means. A generator for any platform reads this file. The Rust crate in rust/ is the same projection for a Rust caller across a network.

What we do not have, plainly: no Swift or Kotlin projection. The path today is openapi.json through a standard generator for those platforms; a native projection is a toolchain addition for when a customer needs it (ROADMAP.md, “Not doing”). The toolchain appendix and the handbook say how the clients are regenerated when the interface changes.

What are anti-patterns?

An anti-pattern is a common mistake, or a sign that a program has a problem it has not paid for yet. The pages in this part name the ones that Vishy programs fall into, with what each one costs and how to write the same thing well.

Three things to hold while reading them:

  • Matching one does not mean the program must be rewritten. An anti-pattern is a reason to look, not a verdict. Every item says when it does not apply.
  • No program is free of them. The ones here were all found in programs that a model or a person had written carefully, and several in programs that had passed verification.
  • Every example runs. Each item’s Example and Refactoring is a program file that the guide’s check compiles, runs and verifies whenever the compiler changes. Where the language catches the anti-pattern, the item shows the exact line it prints: the checker’s refusal, or the verifier’s failure with the state and the arguments that broke the rule. Where nothing catches it, the item says so, and that is the point of it: those are the mistakes the person or the model writing the program has to catch, and the chapter “What the verifier cannot see” and the three-kinds-of-code table on What Vishy is not say why.

Every item has the same four parts, and a fifth when there is something to say:

  • Name, a unique identifier, so a review can cite it.
  • Problem, how it harms and what it costs: a round of writer repair, a bug found late, a rule enforced nowhere.
  • Example, a program with the anti-pattern and the prose that points at it, followed by what the tools said about it.
  • Refactoring, the changed program, and what the tools say now.
  • Additional remarks, the fifth, for the cases where the item does not apply.

The categories, in the order the guide takes them:

  1. Contract anti-patterns: mistakes inside one contract or one row, where a bound, an outcome, an example or a property is missing or wrong. These are the cheapest to fix and the most often caught by the tools.
  2. Design anti-patterns: mistakes in how units, flows and views are cut, where a fact lives in the wrong place or a rule is kept by nobody.
  3. Boundary anti-patterns: mistakes at the edge of the core, where sealed functions and effects take on work the verifier could have seen.
  4. Process anti-patterns: mistakes in how a team or a writer stage uses the tools, where a verdict is read as more or less than it says.

The items come from the experiments that shaped the language, from the applications in the repository and from the days on which the tools’ rules were written down. Each is stated as a rule of thumb, and the rule’s source is the run that taught it, not an opinion.

Contract anti-patterns

Mistakes inside one contract or one row. Seven items, each with a program the guide’s check runs.

Unbounded quantity

Problem

A quantity declared int can be any integer, so every rule about it (never negative, never more than the shelf holds) is a guard that some contract must write, and every writer of every contract that touches it must remember to write. Miss one and the bad value reaches the state; the verifier finds it only if a sample happens to draw it before the run ends. In the experiments that shaped the language, the one bug found late was a negative quantity that reached a stock level through an unbounded field. A bound, int(1, 999), makes the bad value unwritable in an example, undrawable by the sampler, and refused at the door with a 400 that names the parameter, before any contract runs.

Example

row Level { id: id, on_hand: int }
unit Stock {
    state levels: Level[8];
    invariant all(l in levels: l.on_hand >= 0);
    contract receive(sku: id, qty: int) {
        case !(sku in levels) => Received: levels += [Level { id: sku, on_hand: qty }];
        else => Received: levels[sku].on_hand += qty;
    }
    contract ship(sku: id, qty: int) {
        case levels[sku].on_hand < qty => fail Short;
        else => Shipped: levels[sku].on_hand -= qty;
    }
}
scenario a_day { Stock.receive(1, 10) => Received; Stock.ship(1, 4) => Shipped; Stock.ship(1, 7) => Short; }

qty is a plain int. The invariant says a level is never negative, and nothing keeps a negative quantity out of receive. The verifier draws one:

verify FAILED: 2 failure(s):
[1] sampled check failed: Stock.receive: Stock.receive: invariant violated after case 1: state Stock { levels: [Level { id: 2, on_hand: 14 }, Level { id: 3, on_hand: 1 }, Level { id: 4, on_hand: 11 }, Level { id: 6, on_hand: -15 }] } args (6, -15)
  (state Stock { levels: [Level { id: 2, on_hand: 14 }, Level { id: 3, on_hand: 1 }, Level { id: 4, on_hand: 11 }] } args (6, -15))
[2] flow Stock.receive: Stock.receive: invariant violated after case 1: state Stock { levels: [Level { id: 1, on_hand: 3 }, Level { id: 15, on_hand: -49 }] } args (15, -49)
  (world World { stock: Stock { levels: [Level { id: 1, on_hand: 3 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (15, -49))

The first failure is a sampled call of receive with a quantity of minus fifteen from a state of three levels; the second is the same defect reached through the one-call flow. This is the good case: the invariant was written, so the verifier had a rule to break. Without the invariant, the negative level would have been stored and served.

Refactoring

row Level { id: id, on_hand: int(0, 9999) }
unit Stock {
    state levels: Level[8];
    invariant all(l in levels: l.on_hand >= 0);
    contract receive(sku: id, qty: int(1, 999)) {
        case !(sku in levels) => Received: levels += [Level { id: sku, on_hand: qty }];
        else => Received: levels[sku].on_hand += qty;
    }
    contract ship(sku: id, qty: int(1, 999)) {
        case levels[sku].on_hand < qty => fail Short;
        else => Shipped: levels[sku].on_hand -= qty;
    }
}
scenario a_day { Stock.receive(1, 10) => Received; Stock.ship(1, 4) => Shipped; Stock.ship(1, 7) => Short; }
verify: 0 examples, 3 scenario steps and 4000 sampled checks passed

Both quantities carry the bound, and the row’s field carries its own, so the sampler never draws what the bound forbids and the invariant has nothing to find. The guard the anti-pattern would have needed in every contract is written once, in the type.

Additional remarks

The compiler accepts int and string without a bound, and this guide asks you never to write one in a program that matters. A bound is also a decision: when the requirement does not give the number, choose one and record it as an assumption on the declaration, which the appendix on authoring an interface says how to do.

A rule left only in prose

Problem

A comment states a rule; nothing in the program does. “The screen checks it before calling”, “amount checks are another unit’s job”: in two experiments a sentence like that in a contract’s comment became a guard that a writer invented, differently in each unit, and in the third it became nothing at all. The verifier confirms a program against the rules it can read, and a comment is not one of them, so a program that breaks a prose rule passes.

Example

row Booking { id: id, room: id, people: int }
unit Bookings {
    state bookings: Booking[8];
    // people is at least 1; the screen checks it before calling.
    contract book(booking: id, room: id, people: int) {
        else => Booked: bookings += [Booking { id: booking, room, people }];
    }
}
scenario zero_people { Bookings.book(1, 1, 0) => Booked; }

The comment says a booking has at least one person. The scenario books a room for nobody, and expects success:

scenario zero_people
  Bookings.book(1, 1, 0) => Booked
  Bookings = Bookings { bookings: [Booking { id: 1, room: 1, people: 0 }] }
1 step(s), all as expected
verify: 0 examples, 1 scenario steps and 2000 sampled checks passed

Nothing catches it. The verifier has no rule to check, the scenario states the wrong behaviour and passes, and a served door, which enforces only the declared bounds, would take a request for zero people the same way. This is the first of the verifier’s blind spots: a rule nobody wrote is implemented faithfully.

Refactoring

row Booking { id: id, room: id, people: int(1, 200) }
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, people: int(1, 200)) {
        else => Booked: bookings += [Booking { id: booking, room, people }];
    }
}
scenario one_person { Bookings.book(1, 1, 1) => Booked; }
verify: 0 examples, 1 scenario steps and 2000 sampled checks passed

The rule is now the type, and the comment is gone because there is nothing left to say in prose. The old scenario step is refused by the checker before anything runs (the program with that step is book/refusals/anti-prose-zero.vish):

book/refusals/anti-prose-zero.vish:8:44: error: scenario `zero_people` step 1: argument `people` is outside its type
  scenario zero_people { Bookings.book(1, 1, 0) => Booked; }
                                             ^

Additional remarks

A rule that is not a bound is an outcome (fail BadAmount, with an example that produces it) or an invariant. The test for whether a rule is still prose: delete the comment and ask what the checker or the verifier would now miss. If the answer is nothing, the rule was already in the program; if the answer is the rule, it was only in the comment.

A declared failure without an example

Problem

An interface declares that a contract can fail TooBig and gives no example of when. The writer who fills the body has the outcome’s name and a sentence, and writes a guard from the sentence, or none at all. A missing guard fails locally and at once when there is an example, as a failed example that names the contract; without one it fails when a deep sample happens to reach the state, or never. The example is the rule for when the failure occurs, and the interface check refuses a declared failure that has none.

Example

row Room { id: id, seats: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) {
        // Added when the room is new; TooBig when it would have more seats than the building allows.
        outcomes Added, fail TooBig;
        examples { {} (1, 4) => Added { rooms: [Room { id: 1, seats: 4 }] }; }
    }
}
flow add_room(room: id, seats: int(1, 200)) atomic { Rooms.add(room, seats); }

Checked as an interface (vishy check --interface), which is how an interface is checked before writers start:

book/refusals/interface/anti-no-example.vish:6:30: error: `Rooms.add` declares `fail TooBig` without an acceptance example that produces it; write the example (it is the rule for when the failure occurs)
          outcomes Added, fail TooBig;
                               ^^^^^^

Refactoring

row Room { id: id, seats: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) {
        // Added when the room is new; TooBig when it would have more than 120 seats, the building's largest hall.
        outcomes Added, fail TooBig;
        examples {
            {} (1, 4) => Added { rooms: [Room { id: 1, seats: 4 }] };
            {} (1, 121) => TooBig;
        }
    }
}
flow add_room(room: id, seats: int(1, 200)) atomic { Rooms.add(room, seats); }
ok: 1 row(s), 0 event(s), 0 fn(s), 1 unit(s), 1 contract(s), 1 flow(s)

The example says which seat count is too big, so the comment can name the number and the writer’s guard is checked against it on the first run.

Additional remarks

Only the interface check refuses this; vishy check on a whole program accepts a stub without an example, because a program may carry stubs that a part will fill. Run the interface check on the interface, always. A failure that a rule makes impossible (a balanced ledger is never Unbalanced) is not declared at all; the flow says in a comment why the outcome is unreachable, which the design page returns to.

Two examples that disagree

Problem

Two examples of one contract start from the same state with the same arguments and expect different outcomes. No body can satisfy both, so the writer’s part fails whichever way it is written, and the repair rounds spend themselves on a contract that cannot be fixed. The disagreement is a sign about the interface, not the body: the contract is being asked to decide on a fact it does not hold.

Example

row Booking { id: id, holder: id }
unit Bookings {
    state bookings: Booking[8];
    fixture One = { bookings: [Booking { id: 1, holder: 100 }] };
    contract cancel(booking: id) {
        case !(booking in bookings) => fail Unknown;
        case bookings[booking].holder != 100 => fail NotHolder;
        else => Cancelled: bookings -= [booking];
        examples {
            One (1) => Cancelled { bookings: [] };
            One (1) => NotHolder;
        }
    }
}

cancel is told only which booking. One example expects the holder to succeed, the other expects a stranger to be refused, and nothing in the state or the arguments tells the two calls apart. The body here is one a writer might try, comparing the holder with the fixture’s; the verifier fails the second example:

verify FAILED: 1 failure(s):
[1] Bookings.cancel example 2: expected NotHolder, got Cancelled (state before Bookings { bookings: [Booking { id: 1, holder: 100 }] } after Bookings { bookings: [] })

The interface check does not catch this: it types the examples and accepts both. The verifier catches it as soon as a body exists, on whichever example the body does not satisfy.

Refactoring

row Booking { id: id, holder: id }
unit Bookings {
    state bookings: Booking[8];
    fixture One = { bookings: [Booking { id: 1, holder: 100 }] };
    contract cancel(booking: id, holder: id) {
        case !(booking in bookings) => fail Unknown;
        case bookings[booking].holder != holder => fail NotHolder;
        else => Cancelled: bookings -= [booking];
        examples {
            One (1, 100) => Cancelled { bookings: [] };
            One (1, 101) => NotHolder;
            One (9, 100) => Unknown;
        }
    }
}
verify: 3 examples, 0 scenario steps and 1000 sampled checks passed

The missing fact, who is asking, becomes a parameter; the flow that calls cancel passes caller in. The two examples now start from different arguments and agree with each other, and a third one shows the absent booking.

Additional remarks

Two examples from the same state and arguments that expect the same outcome are merely redundant. The rule is about the outcome: when the expected outcomes differ, find the fact that should tell the calls apart and carry it in.

A carried value without a property

Problem

A contract that carries a value, Count(n) or Price(p), is checked against the examples that state the expected number and against nothing else: deltas are held to the invariant in every sampled state, but a carried value has no rule to be held to. A wrong formula that happens to agree with the examples passes. This is the third of the verifier’s blind spots, and it is closed by the interface author, with a contract-level ensures that states the property; then every sampled call checks it, whatever the writer wrote.

Example

row Room { id: id, closed: bool }
unit Rooms {
    state rooms: Room[8];
    fixture Two = { rooms: [Room { id: 1, closed: false }, Room { id: 2, closed: false }] };
    contract close(room: id) {
        else => Closed: rooms[room].closed := true;
        examples { Two (1) => Closed { rooms: [Room { id: 1, closed: true }, Room { id: 2, closed: false }] }; }
    }
    contract open_count() -> int {
        else => Count(len(rooms));
        examples { {} () => Count(0); Two () => Count(2); }
    }
}

open_count returns the number of rooms, closed ones included. Both examples are on states where no room is closed, so both pass, and the verifier has no other rule to try:

verify: 3 examples, 0 scenario steps and 2000 sampled checks passed

Nothing catches it. Adding the property to the same wrong body (the program is book/failures/anti-no-ensures-caught.vish) gives the verifier its rule, and the first sampled state with a closed room breaks it:

verify FAILED: 1 failure(s):
[1] sampled check failed: Rooms.open_count: Rooms.open_count: ensures of case 1 violated: state Rooms { rooms: [Room { id: 1, closed: true }, Room { id: 3, closed: true }, Room { id: 4, closed: false }, Room { id: 5, closed: true }, Room { id: 7, closed: false }, Room { id: 8, closed: true }] } args () out Count(6)
  (state Rooms { rooms: [Room { id: 1, closed: true }, Room { id: 3, closed: true }, Room { id: 4, closed: false }, Room { id: 5, closed: true }, Room { id: 7, closed: false }, Room { id: 8, closed: true }] } args ())

Six rooms, four of them closed, and the contract answered six.

Refactoring

row Room { id: id, closed: bool }
unit Rooms {
    state rooms: Room[8];
    fixture Two = { rooms: [Room { id: 1, closed: false }, Room { id: 2, closed: false }] };
    contract close(room: id) {
        else => Closed: rooms[room].closed := true;
        examples { Two (1) => Closed { rooms: [Room { id: 1, closed: true }, Room { id: 2, closed: false }] }; }
    }
    contract open_count() -> int {
        ensures result == count(r in rooms: !r.closed);
        else => Count(count(r in rooms: !r.closed));
        examples { {} () => Count(0); Two () => Count(2); }
    }
}
verify: 3 examples, 0 scenario steps and 2000 sampled checks passed

The ensures line belongs in the interface, above the stub, so that it survives the merge with the writer’s part and holds whatever body arrives. When there is no formula to state, state the bounds and relations that must hold: result >= 0, result <= len(old(rooms)).

Additional remarks

A carried value whose examples cover every state it can see (a contract on a unit with a single scalar, say) is checked by them, and the property adds nothing. The rule is for values computed over tables, where the exampled states are a few of many.

A derived amount whose domain is not in the row’s type

Problem

A world-derived column takes its value when a flow commits, from other units’ state. Inside a unit’s own sampled check no flow runs, so the sampler draws the column freely, and the only thing it respects is the field’s declared type. A field declared int that the world keeps at zero or more is drawn negative, and a correct contract that relies on the world’s rule fails on a state the world can never reach. The cost is a false failure that sends a writer to repair a body that was right, and it was found by a sample at three thousand iterations on a program that had passed at one thousand.

Example

row Payment { id: id, captured: int(0, 9999), refunded: int }
row Refund { id: id, payment: id, amount: int(1, 9999) }
unit Payments {
    state payments: Payment[8];
    fixture One = { payments: [Payment { id: 1, captured: 5000, refunded: 0 }] };
    contract capture(payment: id, amount: int(1, 9999)) {
        else => Captured: payments += [Payment { id: payment, captured: amount, refunded: 0 }];
        examples { {} (1, 5000) => Captured One; }
    }
    contract remaining(payment: id) -> int {
        ensures result <= payments[payment].captured;
        case !(payment in payments) => fail Unknown;
        else => Remaining(payments[payment].captured - payments[payment].refunded);
        examples { One (1) => Remaining(5000); One (9) => Unknown; }
    }
}
unit Refunds {
    state refunds: Refund[8];
    contract refund(refund: id, payment: id, amount: int(1, 9999)) {
        else => Refunded: refunds += [Refund { id: refund, payment, amount }];
    }
}
derived Payments.payments.refunded = for p => sum(where(r in Refunds.refunds: r.payment == p.id).amount);
invariant all(p in Payments.payments: p.refunded <= p.captured);
flow refund(refund: id, payment: id, amount: int(1, 9999)) atomic { let left = Payments.remaining(payment); Refunds.refund(refund, payment, amount); }

refunded is derived from the refunds and, in the world, never exceeds captured or falls below zero: the flow that refunds asks remaining first, and the world invariant says so. But the row declares it int. In the unit’s check, the sampler draws a refund of minus fifty:

verify FAILED: 1 failure(s):
[1] sampled check failed: Payments.remaining: Payments.remaining: ensures of case 2 violated: state Payments { payments: [Payment { id: 1, captured: 3046, refunded: -50 }, Payment { id: 4, captured: 1611, refunded: 25 }, Payment { id: 6, captured: 2330, refunded: 8 }, Payment { id: 7, captured: 8904, refunded: 39 }, Payment { id: 8, captured: 7643, refunded: 22 }] } args (1,) out Remaining(3096)
  (state Payments { payments: [Payment { id: 1, captured: 3046, refunded: -50 }, Payment { id: 4, captured: 1611, refunded: 25 }, Payment { id: 6, captured: 2330, refunded: 8 }, Payment { id: 7, captured: 8904, refunded: 39 }, Payment { id: 8, captured: 7643, refunded: 22 }] } args (1,))

The property result <= captured is true in every real state and false in this drawn one, and the contract is blamed.

Refactoring

row Payment { id: id, captured: int(0, 9999), refunded: int(0, 9999) }
row Refund { id: id, payment: id, amount: int(1, 9999) }
unit Payments {
    state payments: Payment[8];
    fixture One = { payments: [Payment { id: 1, captured: 5000, refunded: 0 }] };
    contract capture(payment: id, amount: int(1, 9999)) {
        else => Captured: payments += [Payment { id: payment, captured: amount, refunded: 0 }];
        examples { {} (1, 5000) => Captured One; }
    }
    contract remaining(payment: id) -> int {
        ensures result <= payments[payment].captured;
        case !(payment in payments) => fail Unknown;
        else => Remaining(payments[payment].captured - payments[payment].refunded);
        examples { One (1) => Remaining(5000); One (9) => Unknown; }
    }
}
unit Refunds {
    state refunds: Refund[8];
    contract refund(refund: id, payment: id, amount: int(1, 9999)) {
        else => Refunded: refunds += [Refund { id: refund, payment, amount }];
    }
}
derived Payments.payments.refunded = for p => sum(where(r in Refunds.refunds: r.payment == p.id).amount);
invariant all(p in Payments.payments: p.refunded <= p.captured);
flow refund(refund: id, payment: id, amount: int(1, 9999)) atomic { let left = Payments.remaining(payment); Refunds.refund(refund, payment, amount); }
verify: 3 examples, 0 scenario steps and 4000 sampled checks passed

One change: refunded: int(0, 9999). The sampler now draws inside the domain, and the property holds on every drawn state.

Additional remarks

The relation between two fields (refunded <= captured) cannot go into the type, and it cannot go into the unit’s invariant either, because a unit invariant may not name a world-derived field; the checker refuses one that tries (the program is book/refusals/anti-derived-invariant.vish):

book/refusals/anti-derived-invariant.vish:5:15: error: this invariant of `Payments` mentions a world-derived field (refunded), which only takes its value when a flow commits; state it as a top-level `invariant` over `Payments.field`, checked after every flow
      invariant all(p in payments: p.refunded <= p.captured);
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It is a world invariant, as in the programs above, and the unit’s own check does not see it. So a contract’s property inside the unit must hold for every value the type allows, not only for the values the world produces; when the property needs the relation, state it on the flow’s ensures instead, where the world is in scope.

A money total in a fold

Problem

Until 30 September 2026 a total of a money column written as a fold that starts at a literal, acc = 0, or as a sum over the column, was refused in a view or a world invariant: the literal was typed as int when nothing around it said money, and sum took only integer columns. Writers worked around it with an int total, which lost the scale, or with a derived field whose declared type gave the fold its context.

Example

This is the program that was refused. The compiler now accepts it: a fold that starts at a literal takes its type from what its body combines it with, and sum keeps a money or duration column’s type (roadmap item 8). What it prints today:

row Payment { id: id, captured: money(2) }
unit Payments {
    state payments: Payment[8];
    contract capture(payment: id, amount: money(2)) {
        case amount <= 0 => fail BadAmount;
        else => Captured: payments += [Payment { id: payment, captured: amount }];
    }
}
view total() = fold(p in Payments.payments, acc = 0: acc + p.captured);
ok: 1 row(s), 0 event(s), 0 fn(s), 1 unit(s), 1 contract(s), 0 flow(s)

And the sum form (the program is book/programs/anti-money-sum.vish):

ok: 1 row(s), 0 event(s), 0 fn(s), 1 unit(s), 1 contract(s), 0 flow(s)

Refactoring

The shape that was the workaround is still the better shape, for a reason that has nothing to do with the old refusal:

row Payment { id: id, captured: money(2) }
unit Payments {
    state payments: Payment[8];
    derived total: money(2) = fold(p in payments, acc = 0: acc + p.captured);
    contract capture(payment: id, amount: money(2)) {
        case amount <= 0 => fail BadAmount;
        else => Captured: payments += [Payment { id: payment, captured: amount }];
    }
    contract captured_total() -> money(2) {
        else => Total(total);
        examples { {} () => Total(0); }
    }
}
view total() = Payments.total;
scenario two_payments { Payments.capture(1, 1000) => Captured; Payments.capture(2, 250) => Captured; Payments.captured_total() => Total(1250); total() => Value(1250); }
scenario two_payments
  Payments.capture(1, 1000) => Captured
  Payments.capture(2, 250) => Captured
  Payments.captured_total() => Total(1250)
  total() => Value(1250)
  Payments = Payments { payments: [Payment { id: 1, captured: 1000 }, Payment { id: 2, captured: 250 }], total: 1250 }
4 step(s), all as expected
verify: 1 examples, 4 scenario steps and 5000 sampled checks passed

A derived field inside the unit names the total’s type once, money(2), keeps the total in step with the table on every change without a contract doing the bookkeeping, and lets the view read a maintained value instead of folding the table on every call. The fold in the view is correct now; the derived field is what a reader of the interface expects to find.

Additional remarks

The anti-pattern that remains is the one the old refusal was protecting against: an int total for a money column, typed by hand, which loses the scale and lets the total be added to a count. Keep money as money(2) from the column to the view, and let the compiler carry the type.

Design anti-patterns

Mistakes in how units, flows and views are cut: a fact that lives in the wrong place, a rule kept by nobody, a name that belongs to someone else. Eight items, each with a program the guide’s check runs.

A verdict carried as a value

Problem

One unit answers a question with a boolean, the flow carries the boolean into another unit, and that unit fails when the flag is false. The rule now lives in three places: the unit that computed the flag, the flow that passed it, and the guard that reads it. Any flow can pass true. Inside the second unit’s own check, the sampler draws the flag freely, since it is only a bool, so the unit is verified against a fact it never held. A bool parameter that stands for another unit’s answer is a cross-unit read in disguise; so is comparing two ids that come from different units. The unit that knows decides and fails; the flow calls it first; the next contract takes no flag.

Example

row Room { id: id, seats: int(1, 200) }
row Booking { id: id, room: id, people: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) {
        else => Added: rooms += [Room { id: room, seats }];
    }
    contract fits(room: id, people: int(1, 200)) -> bool {
        case !(room in rooms) => fail Unknown;
        else => Fits(rooms[room].seats >= people);
    }
}
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, people: int(1, 200), fits: bool) {
        case !fits => fail TooSmall;
        else => Booked: bookings += [Booking { id: booking, room, people }];
        examples { {} (1, 1, 3, true) => Booked { bookings: [Booking { id: 1, room: 1, people: 3 }] }; {} (1, 1, 3, false) => TooSmall; }
    }
}
invariant all(b in Bookings.bookings: any(r in Rooms.rooms: r.id == b.room && b.people <= r.seats));
flow book(booking: id, room: id, people: int(1, 200)) atomic { let ok = Rooms.fits(room, people); Bookings.book(booking, room, people, ok); }
scenario a_day { Rooms.add(1, 4) => Added; book(1, 1, 3) => Booked; book(2, 1, 5) => TooSmall; }
verify: 2 examples, 3 scenario steps and 5000 sampled checks passed

Nothing catches it: the one flow passes the flag correctly, the world invariant holds, and the verifier has no way to know that fits was meant to be Rooms’ answer rather than a caller’s opinion. The cost arrives with the second flow that calls book, or with the writer who reads fits: bool and guesses what it means.

Refactoring

row Room { id: id, seats: int(1, 200) }
row Booking { id: id, room: id, people: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) {
        else => Added: rooms += [Room { id: room, seats }];
    }
    contract fits(room: id, people: int(1, 200)) {
        case !(room in rooms) => fail Unknown;
        case rooms[room].seats < people => fail TooSmall;
        else => Fits;
        examples { { rooms: [Room { id: 1, seats: 4 }] } (1, 4) => Fits; { rooms: [Room { id: 1, seats: 4 }] } (1, 5) => TooSmall; {} (9, 1) => Unknown; }
    }
}
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, people: int(1, 200)) {
        else => Booked: bookings += [Booking { id: booking, room, people }];
        examples { {} (1, 1, 3) => Booked { bookings: [Booking { id: 1, room: 1, people: 3 }] }; }
    }
}
invariant all(b in Bookings.bookings: any(r in Rooms.rooms: r.id == b.room && b.people <= r.seats));
// the room decides whether the booking fits, before it is made.
flow book(booking: id, room: id, people: int(1, 200)) atomic { Rooms.fits(room, people); Bookings.book(booking, room, people); }
scenario a_day { Rooms.add(1, 4) => Added; book(1, 1, 3) => Booked; book(2, 1, 5) => TooSmall; }
verify: 4 examples, 3 scenario steps and 5000 sampled checks passed

fits fails with TooSmall itself, the flow calls it before book, and book takes no flag. A failure in the first call aborts the transaction, so the booking is never made, and there is no second place for the rule to be wrong.

Additional remarks

A carried value that is a fact, not a verdict (a room’s seat count, an order’s location), is the right thing to carry, and the next item’s refactoring does exactly that. The test is whether the value is something the second unit needs to know or something it has been told to conclude.

A query flow where a view belongs

Problem

A read is written as a flow that calls a contract that computes a value. That costs a contract body for a writer to write, a transaction at the door for every read, and a place for the rule of who sees what to hide inside a body instead of standing in the declaration. A read is a view: an expression over the world, typed like a rule, served without a transaction, and checked by the scenarios that assert on it.

Example

row Room { id: id, closed: bool }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id) {
        else => Added: rooms += [Room { id: room, closed: false }];
    }
    contract close(room: id) {
        else => Closed: rooms[room].closed := true;
    }
    contract open_count() -> int {
        else => Count(count(r in rooms: !r.closed));
        examples { {} () => Count(0); }
    }
}
flow open_rooms() atomic { Rooms.open_count(); }
scenario counting { Rooms.add(1) => Added; Rooms.add(2) => Added; Rooms.close(2) => Closed; open_rooms() => Count(1); }
verify: 1 examples, 4 scenario steps and 6000 sampled checks passed

Nothing catches it; a flow that only reads is a legal flow. The cost is in the shape: open_count is a contract, so it waits for a writer, and its answer is a carried value, which the earlier page showed is checked only where an example states it.

Refactoring

row Room { id: id, closed: bool }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id) {
        else => Added: rooms += [Room { id: room, closed: false }];
    }
    contract close(room: id) {
        else => Closed: rooms[room].closed := true;
    }
}
view open_rooms() = count(r in Rooms.rooms: !r.closed);
scenario counting { Rooms.add(1) => Added; Rooms.add(2) => Added; Rooms.close(2) => Closed; open_rooms() => Value(1); }
scenario counting
  Rooms.add(1) => Added
  Rooms.add(2) => Added
  Rooms.close(2) => Closed
  open_rooms() => Value(1)
  Rooms = Rooms { rooms: [Room { id: 1, closed: false }, Room { id: 2, closed: true }] }
4 step(s), all as expected
verify: 0 examples, 4 scenario steps and 5000 sampled checks passed

The count is a view, one line, with no body to write. The scenario asserts on it with Value(1). A view that needs the caller says uses caller and puts the visibility rule in its expression, where the checker holds it.

Additional remarks

A read that must run inside a transaction with a write, such as a carried value the next contract needs, is a contract call in a flow, and that is the flows chapter’s subject. A view never writes and a route to it is always GET.

An internal counter named in an interface

Problem

The interface names a piece of state that is nobody’s business but the writer’s: a next-id counter, an index, a helper table. Every writer of every contract in the unit must now maintain it exactly as described, the examples must show it, and a different way of doing the same job is refused. In the experiments that shaped the language, the authoring review flagged two units of an accepted interface for this. The interface says what a unit answers and what its rows hold; how ids are minted is either the caller’s (an id parameter) or the flow’s (uses ids and fresh()).

Example

row Line { id: id, order: id, sku: id, qty: int(1, 99) }
unit Lines {
    caps storage;
    state lines: Line[16];
    // next_line is the id the next line gets; it goes up by one per add.
    state next_line: int = 1;
    contract add(order: id, sku: id, qty: int(1, 99)) -> id {
        // Inserts Line { id: as_id(next_line), order, sku, qty } and bumps next_line.
        outcomes Added(line);
        examples { {} (1, 7, 2) => Added(1) { lines: [Line { id: 1, order: 1, sku: 7, qty: 2 }], next_line: 2 }; }
    }
}
flow add_line(order: id, sku: id, qty: int(1, 99)) atomic { Lines.add(order, sku, qty); }
ok: 1 row(s), 0 event(s), 0 fn(s), 1 unit(s), 1 contract(s), 1 flow(s)

Nothing catches it; the interface check accepts any state a unit declares. The counter is a decision the interface author made for the writer, and the example pins it: the after-state must show next_line: 2.

Refactoring

row Line { id: id, order: id, sku: id, qty: int(1, 99) }
unit Lines {
    caps storage;
    state lines: Line[16];
    contract add(line: id, order: id, sku: id, qty: int(1, 99)) {
        // Inserts Line { id: line, order, sku, qty }; an existing line id is Exists.
        outcomes Added;
        examples { {} (1, 1, 7, 2) => Added { lines: [Line { id: 1, order: 1, sku: 7, qty: 2 }] }; }
    }
}
flow add_line(order: id, sku: id, qty: int(1, 99)) uses ids atomic { let line = fresh(); Lines.add(line, order, sku, qty); }
ok: 1 row(s), 0 event(s), 0 fn(s), 1 unit(s), 1 contract(s), 1 flow(s)

The line’s id comes from the flow, fresh, and the unit’s state is its rows and nothing else. The carried value is gone with the counter; the caller already knows the id it asked for.

Additional remarks

A number that is part of the product, such as a sequence customers see on their invoices, is a row’s field with a rule about it, not an internal counter, and belongs in the interface. The item is about state that exists only to implement something.

A unit every flow touches

Problem

One unit, often an audit log or an activity table, is called by every flow. Then every flow’s closure includes it, the whole program is one component, and every flow’s reachable walk draws from every other flow: verification cannot be partitioned, and nothing about the program’s structure is visible in its graph. Both real applications in the repository are one component for this reason.

Example

row Room { id: id, closed: bool }
row Note { id: id, body: string(64) }
row Entry { id: id, kind: int(1, 4) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id) { else => Added: rooms += [Room { id: room, closed: false }]; }
    contract close(room: id) { else => Closed: rooms[room].closed := true; }
}
unit Notes {
    state notes: Note[8];
    contract add(note: id, body: string(64)) { else => Added: notes += [Note { id: note, body }]; }
}
unit Activity {
    state entries: Entry[16];
    contract record(entry: id, kind: int(1, 4)) { else => Recorded: entries += [Entry { id: entry, kind }]; }
}
flow add_room(room: id, entry: id) atomic { Rooms.add(room); Activity.record(entry, 1); }
flow close_room(room: id, entry: id) atomic { Rooms.close(room); Activity.record(entry, 2); }
flow add_note(note: id, body: string(64), entry: id) atomic { Notes.add(note, body); Activity.record(entry, 3); }
scenario a_day { add_room(1, 1) => Recorded; add_note(1, "hi", 2) => Recorded; close_room(1, 3) => Recorded; }

vishy graph on it (the output is book/programs/anti-hub.graph.txt):

3 unit(s), 3 flow(s), 0 world invariant(s), 0 world-derived field(s): 1 component(s)
component 1: units Rooms, Notes, Activity | flows add_room, close_room, add_note | 0 invariant(s)
note: one component spans the whole world; every flow's walk draws from every flow. Look for a unit that every flow touches.

The verifier passes the program; the graph says what it costs.

Refactoring

row Room { id: id, closed: bool }
row Note { id: id, body: string(64) }
events { RoomAdded(room: id), RoomClosed(room: id), NoteAdded(note: id) }
unit Rooms {
    caps outbox;
    state rooms: Room[8];
    contract add(room: id) { else => Added: rooms += [Room { id: room, closed: false }], emit RoomAdded(room); }
    contract close(room: id) { else => Closed: rooms[room].closed := true, emit RoomClosed(room); }
}
unit Notes {
    caps outbox;
    state notes: Note[8];
    contract add(note: id, body: string(64)) { else => Added: notes += [Note { id: note, body }], emit NoteAdded(note); }
}
flow add_room(room: id) atomic { Rooms.add(room); }
flow close_room(room: id) atomic { Rooms.close(room); }
flow add_note(note: id, body: string(64)) atomic { Notes.add(note, body); }
scenario a_day { add_room(1) => Added; add_note(1, "hi") => Added; close_room(1) => Closed; }
2 unit(s), 3 flow(s), 0 world invariant(s), 0 world-derived field(s): 2 component(s)
component 1: units Rooms | flows add_room, close_room | 0 invariant(s)
component 2: units Notes | flows add_note | 0 invariant(s)
flow add_room: touches Rooms | re-checks 0 of 0 invariant(s), recomputes 0 of 0 derived | walks 2 of 3 flow(s)
flow close_room: touches Rooms | re-checks 0 of 0 invariant(s), recomputes 0 of 0 derived | walks 2 of 3 flow(s)
flow add_note: touches Notes | re-checks 0 of 0 invariant(s), recomputes 0 of 0 derived | walks 1 of 3 flow(s)
note: the largest component has 1 of 2 units; each component verifies independently.
verify: 0 examples, 3 scenario steps and 6000 sampled checks passed

The record of what happened is an event, emitted by the unit that did it, in the same transaction; a deliver declaration says where the events go, and the outbox is written with the state change, so nothing is lost. Rooms and notes are now two components, and each verifies on its own.

Additional remarks

A unit every flow touches because the product’s rules need it (a members table that every rule reads) is a fact about the product, not an anti-pattern, and the graph’s note is then a description. The item is about units that exist to be written to and are never read by a rule.

A coupling rule kept by no flow and not explained

Problem

A world invariant couples two units: a booking’s room is open. Every flow that changes either unit either keeps the rule, by calling the units in the order that keeps it, or cannot break it, and says why in a comment. A flow that does neither is where the verifier finds the world that breaks the rule, and a flow that keeps the rule without saying so is rewritten by the next writer into one that does not.

Example

row Room { id: id, closed: bool }
row Booking { id: id, room: id }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id) { else => Added: rooms += [Room { id: room, closed: false }]; }
    contract close(room: id) { else => Closed: rooms[room].closed := true; }
}
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id) { else => Booked: bookings += [Booking { id: booking, room }]; }
    contract clear_room(room: id) { else => Cleared: bookings -= where(b in bookings: b.room == room).id; }
}
// rule 1: a booking's room exists and is open.
invariant all(b in Bookings.bookings: any(r in Rooms.rooms: r.id == b.room && !r.closed));
flow add_room(room: id) atomic { Rooms.add(room); }
flow book(booking: id, room: id) atomic { Bookings.book(booking, room); }
flow close_room(room: id) atomic { Rooms.close(room); }
scenario a_day { add_room(1) => Added; book(1, 1) => Booked; close_room(1) => Closed; }

book never asks whether the room is open; close_room never clears the bookings. The verifier finds both:

verify FAILED: 5 failure(s):
[1] scenario a_day step 3 (close_room): flow close_room: world invariant 1 violated: after World { rooms: Rooms { rooms: [Room { id: 1, closed: true }] }, bookings: Bookings { bookings: [Booking { id: 1, room: 1 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (1,) out Closed
  world World { rooms: Rooms { rooms: [Room { id: 1, closed: true }] }, bookings: Bookings { bookings: [Booking { id: 1, room: 1 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 }
[2] reachable walk, flow book: flow book: world invariant 1 violated: after World { rooms: Rooms { rooms: [] }, bookings: Bookings { bookings: [Booking { id: 12, room: 4 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (12, 4) out Booked
[3] reachable walk, flow close_room: flow close_room: world invariant 1 violated: after World { rooms: Rooms { rooms: [Room { id: 0, closed: true }] }, bookings: Bookings { bookings: [Booking { id: 0, room: 0 }, Booking { id: 7, room: 0 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (0,) out Closed
[4] flow book: flow book: world invariant 1 violated: after World { rooms: Rooms { rooms: [Room { id: 0, closed: true }] }, bookings: Bookings { bookings: [Booking { id: 0, room: 0 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (0, 0) out Booked
  (world World { rooms: Rooms { rooms: [Room { id: 0, closed: true }] }, bookings: Bookings { bookings: [] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (0, 0))
[5] flow close_room: flow close_room: world invariant 1 violated: after World { rooms: Rooms { rooms: [Room { id: 10, closed: true }] }, bookings: Bookings { bookings: [Booking { id: 10, room: 10 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (10,) out Closed
  (world World { rooms: Rooms { rooms: [Room { id: 10, closed: false }] }, bookings: Bookings { bookings: [Booking { id: 10, room: 10 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (10,))

The first failure is the scenario’s own third step: closing a room with a booking in it. The others are the walks and the sampled flows, each with the world before and after.

Refactoring

row Room { id: id, closed: bool }
row Booking { id: id, room: id }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id) { else => Added: rooms += [Room { id: room, closed: false }]; }
    contract close(room: id) { else => Closed: rooms[room].closed := true; }
    contract is_open(room: id) {
        case !(room in rooms) => fail Unknown;
        case rooms[room].closed => fail RoomClosed;
        else => Open;
        examples { { rooms: [Room { id: 1, closed: true }] } (1) => RoomClosed; { rooms: [Room { id: 1, closed: false }] } (1) => Open; {} (9) => Unknown; }
    }
}
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id) { else => Booked: bookings += [Booking { id: booking, room }]; }
    contract clear_room(room: id) { else => Cleared: bookings -= where(b in bookings: b.room == room).id; }
}
// rule 1: a booking's room exists and is open.
invariant all(b in Bookings.bookings: any(r in Rooms.rooms: r.id == b.room && !r.closed));
// rule 1: unchanged because the flow adds an open room and touches no booking.
flow add_room(room: id) atomic { Rooms.add(room); }
// rule 1 is kept: the room says it is open before the booking is made.
flow book(booking: id, room: id) atomic { Rooms.is_open(room); Bookings.book(booking, room); }
// rule 1 is kept: the bookings go before the room closes.
flow close_room(room: id) atomic { Bookings.clear_room(room); Rooms.close(room); }
scenario a_day { add_room(1) => Added; book(1, 1) => Booked; book(2, 9) => Unknown; close_room(1) => Closed; book(3, 1) => RoomClosed; }
scenario a_day
  add_room(1) => Added
  book(1, 1) => Booked
  book(2, 9) => Unknown
  close_room(1) => Closed
  book(3, 1) => RoomClosed
  Rooms = Rooms { rooms: [Room { id: 1, closed: true }] }
  Bookings = Bookings { bookings: [] }
5 step(s), all as expected
verify: 3 examples, 5 scenario steps and 8000 sampled checks passed

Three comments, one per flow: the two that keep the rule say in which order, and the one that cannot break it says why. The verifier held the rule; the comments are for the writer who edits the flow next.

Additional remarks

A rule kept by a bound or a machine rather than by an order of calls needs no flow comment; the item is about rules that an order of calls keeps.

One outcome name with two shapes

Problem

An outcome name means one thing across the whole program: success or failure, with or without a value, of one type. A name used as a success in one unit and a failure in another is refused by the checker, and a brief that assigns the same name two meanings produces an interface that cannot be checked until the brief is amended. In the experiments that shaped the language a new flow’s success reused a name that an earlier account had used as a failure, and another flow was asked to fail with the name of a third flow’s success.

Example

row Room { id: id, seats: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) {
        else => Added: rooms += [Room { id: room, seats }];
    }
}
unit Waitlist {
    state waiting: id[8];
    contract add(room: id) {
        case room in waiting => fail Added;
        else => Waiting: waiting += [room];
    }
}
book/refusals/anti-two-shapes.vish:11:38: error: outcome `Added` is declared differently elsewhere (fail / carried value / type must agree across the program)
          case room in waiting => fail Added;
                                       ^^^^^

Refactoring

row Room { id: id, seats: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) {
        else => Added: rooms += [Room { id: room, seats }];
    }
}
unit Waitlist {
    state waiting: id[8];
    contract add(room: id) {
        case room in waiting => fail AlreadyWaiting;
        else => Waiting: waiting += [room];
        examples { {} (1) => Waiting { waiting: [1] }; { waiting: [1] } (1) => AlreadyWaiting; }
    }
}
verify: 2 examples, 0 scenario steps and 2000 sampled checks passed

The waitlist’s failure gets its own name. The rule reaches the brief: name the outcomes in the brief once, and keep a list.

Additional remarks

The same name in two units with the same shape is fine and often right (Added in both units above is a success without a value). The rule is about shape, not about reuse.

An unreachable outcome without its note

Problem

A rule makes a failure impossible: a ledger kept balanced by its invariant is never Unbalanced. A contract that declares the failure anyway, with a guard, has a case the sampler never reaches, because the sampler draws only states that satisfy the invariant; the verifier refuses the program for the unreached outcome. An example that produces it would have to start from a state that cannot exist. The failure is not declared; the flow says in a comment that it is unreachable and under which rule.

Example

row Entry { id: id, debit: int(0, 9999), credit: int(0, 9999) }
unit Ledger {
    state entries: Entry[8];
    invariant sum(entries.debit) == sum(entries.credit);
    contract post(entry: id, amount: int(1, 9999)) {
        else => Posted: entries += [Entry { id: entry, debit: amount, credit: amount }];
    }
    contract balance_check() {
        case sum(entries.debit) != sum(entries.credit) => fail Unbalanced;
        else => Balanced;
        examples { {} () => Balanced; }
    }
}
scenario posting { Ledger.post(1, 100) => Posted; Ledger.balance_check() => Balanced; }
verify FAILED: 1 failure(s):
[1] Ledger.balance_check: outcome `Unbalanced` is never produced by an example or a sampled call (1000 sampled calls): an unreachable case, or a state the sampler does not draw; add an example that produces it

Refactoring

row Entry { id: id, debit: int(0, 9999), credit: int(0, 9999) }
unit Ledger {
    state entries: Entry[8];
    invariant sum(entries.debit) == sum(entries.credit);
    contract post(entry: id, amount: int(1, 9999)) {
        else => Posted: entries += [Entry { id: entry, debit: amount, credit: amount }];
    }
    contract balance_check() {
        else => Balanced;
        examples { {} () => Balanced; }
    }
}
// Outcome: Unbalanced is unreachable under rule: balanced (the invariant); no contract declares it.
flow check_books() atomic { Ledger.balance_check(); }
scenario posting { Ledger.post(1, 100) => Posted; check_books() => Balanced; }
verify: 1 examples, 2 scenario steps and 4000 sampled checks passed

The case is gone, and the comment above the flow records that the brief’s failure was considered and why it has no outcome, so the next reader does not put it back.

Additional remarks

An outcome that is reachable but that the sampler does not draw at a thousand iterations gets an example that produces it, not a note; the verifier’s message names both possibilities. The note is for outcomes a rule forbids.

A contract that needs another unit’s fact

Problem

A contract’s guard needs something another unit knows: the room’s seat count, the customer’s tier. A unit cannot read another unit, and the checker refuses the attempt. The mistake that follows the refusal is worse than the refusal: moving the row into the wrong unit, copying the fact into a second table that goes stale, or changing the brief so the rule disappears. The fact is carried in by the flow, as a value, into the unit that owns the data.

Example

row Room { id: id, seats: int(1, 200) }
row Booking { id: id, room: id, people: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) { else => Added: rooms += [Room { id: room, seats }]; }
}
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, people: int(1, 200)) {
        case Rooms.rooms[room].seats < people => fail TooSmall;
        else => Booked: bookings += [Booking { id: booking, room, people }];
    }
}
flow book(booking: id, room: id, people: int(1, 200)) atomic { Bookings.book(booking, room, people); }
book/refusals/anti-reach.vish:10:14: error: unknown name `Rooms`
          case Rooms.rooms[room].seats < people => fail TooSmall;
               ^^^^^

Inside Bookings, Rooms is not a name.

Refactoring

row Room { id: id, seats: int(1, 200) }
row Booking { id: id, room: id, people: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) { else => Added: rooms += [Room { id: room, seats }]; }
    contract seats_of(room: id) -> int {
        case !(room in rooms) => fail Unknown;
        else => Seats(rooms[room].seats);
        examples { { rooms: [Room { id: 1, seats: 4 }] } (1) => Seats(4); {} (9) => Unknown; }
    }
}
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, people: int(1, 200), seats: int(1, 200)) {
        case seats < people => fail TooSmall;
        else => Booked: bookings += [Booking { id: booking, room, people }];
        examples { {} (1, 1, 3, 4) => Booked { bookings: [Booking { id: 1, room: 1, people: 3 }] }; {} (1, 1, 5, 4) => TooSmall; }
    }
}
// the flow carries the room's seats into the unit that owns the bookings.
flow book(booking: id, room: id, people: int(1, 200)) atomic { let seats = Rooms.seats_of(room); Bookings.book(booking, room, people, seats); }
scenario a_day { Rooms.add(1, 4) => Added; book(1, 1, 3) => Booked; book(2, 1, 5) => TooSmall; book(3, 9, 1) => Unknown; }
verify: 4 examples, 4 scenario steps and 5000 sampled checks passed

Rooms answers the seat count as a carried value, the flow binds it and passes it on, and book decides with a fact it was given. The absent room fails in Rooms, before Bookings is reached, and the scenario shows all three outcomes through the flow.

Additional remarks

When the fact is a verdict rather than a value (fits or not), the first item on this page applies instead: let the unit that knows fail. When the fact is needed by a rule that must hold at every moment, it is a world invariant or a world-derived column, which no contract reads and every flow is checked against.

Boundary anti-patterns

Mistakes at the edge of the core, where sealed functions and effects take on work the verifier could have seen, or a program’s shape changes without telling the store. Four items, each with a program the guide’s check runs.

The sealed layer or an effect where the core suffices

Problem

A sum, a count, a lookup, a selection by rank: the core has a quantifier for each, and a quantifier is sampled in every state the verifier draws. The same computation written as a sealed function with a while, or as an effect with a Rust body, is tested by its examples and nothing else: a sealed function is never sampled on its own, and an effect’s body is never run by the verifier at all. What was one checked expression becomes a boundary the verifier stops at, with a step budget and a declared type on the far side. The core is closed so that everything in it is checkable; leaving it for what it already does gives that up for nothing.

Example

row Line { id: id, qty: int(1, 99) }
sealed fn total_qty(xs: int[]) -> int {
    var i = 0; var t = 0;
    while i < len(xs) { t := t + xs[i]; i := i + 1; }
    t
}
examples total_qty { ([]) => 0; ([2, 3]) => 5; }
unit Cart {
    state lines: Line[8];
    contract add(line: id, qty: int(1, 99)) { else => Added: lines += [Line { id: line, qty }]; }
    contract total() -> int {
        else => Total(total_qty(lines.qty));
        examples { {} () => Total(0); }
    }
}
scenario two { Cart.add(1, 2) => Added; Cart.add(2, 3) => Added; Cart.total() => Total(5); }
verify: 3 examples, 3 scenario steps and 4000 sampled checks passed

Nothing catches it. The sealed function has three examples and they pass; the contract that calls it has one. What the verifier does not do is draw a thousand carts and check that the total is the sum of the lines, because no rule says so, and the loop that would compute it is behind the boundary.

Refactoring

row Line { id: id, qty: int(1, 99) }
unit Cart {
    state lines: Line[8];
    contract add(line: id, qty: int(1, 99)) { else => Added: lines += [Line { id: line, qty }]; }
    contract total() -> int {
        ensures result == sum(lines.qty);
        else => Total(sum(lines.qty));
        examples { {} () => Total(0); }
    }
}
scenario two { Cart.add(1, 2) => Added; Cart.add(2, 3) => Added; Cart.total() => Total(5); }
verify: 1 examples, 3 scenario steps and 4000 sampled checks passed

sum over the column is the whole computation, and the property on the contract says it is the answer, so every sampled state checks it. The sealed function, its examples and its loop are gone.

Additional remarks

The sealed layer is for what the core cannot say: recursion, a tree, unbounded text, a parser. The test is whether the computation can be written as a quantifier over a table; when it can, it belongs in the core. The chapter on functions draws the line in detail, and the three-kinds-of-code table on What Vishy is not says what each side is trusted with.

Problem

An effect that exists to write a line somewhere, a log, a console, a notification, is called from a flow and returns a value nobody uses. It is not a fact the program needs from the world; it is a side effect the program wants the world to have. Vishy’s form for that is an event: emitted by the unit that did the thing, in the same transaction, checked by scenarios and by ensures emitted, and carried out of the program by a deliver declaration. A print-like effect is checked by nothing, appears in no outcome, has already happened when a later call fails and the transaction rolls back, and, placed last, leaves the flow with no outcome to name.

Example

row Room { id: id, closed: bool }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id) { else => Added: rooms += [Room { id: room, closed: false }]; }
}
effect log_line(msg: string(200)) -> bool
  impl { eprintln!("{}", msg); true }
  fixtures { ("room added") => true; }
flow add_room(room: id) atomic { log_line("room added"); Rooms.add(room); }
scenario one { add_room(1) => Added; }
scenario one
  add_room(1) => Added
  Rooms = Rooms { rooms: [Room { id: 1, closed: false }] }
1 step(s), all as expected
verify: 0 examples, 1 scenario steps and 2000 sampled checks passed

Nothing catches it. In verification the effect answers from its fixture; in a service it runs the Rust, before the state change commits and whether or not it does. The line is written; the program does not know it was.

Refactoring

row Room { id: id, closed: bool }
events { RoomAdded(room: id) }
unit Rooms {
    caps outbox;
    state rooms: Room[8];
    contract add(room: id) {
        else => Added: rooms += [Room { id: room, closed: false }], emit RoomAdded(room) ensures emitted([RoomAdded(room)]);
        examples { {} (1) => Added { rooms: [Room { id: 1, closed: false }] }; }
    }
}
flow add_room(room: id) atomic { Rooms.add(room); }
scenario one { add_room(1) => Added; }
verify: 1 examples, 1 scenario steps and 2000 sampled checks passed

The unit emits RoomAdded as part of the change, the case’s ensures emitted holds it, and the example runs it. A deliver RoomAdded to … line, which this program leaves out, is where the world learns of it: one outbox row per event, written in the flow’s transaction, never lost and never recorded without its cause.

Additional remarks

An effect is right when the program needs an answer from outside (a rate, a lookup, a hash) or when the outside must act before the flow can continue. A record of what the program did is an event.

Logic inside an effect the verifier cannot see

Problem

A rule of the product is written in an effect’s Rust body: whether a booking fits, whether a discount applies. The verifier never runs Rust. In verification an effect answers with a fixture’s value when the arguments match one, and otherwise with a sampled value, so the rule is invisible to every check: a scenario that states the product’s real behaviour fails or passes by what the sampler drew, and no invariant can be held to the rule because the verifier cannot see it. The cost is a program verified against a rule that is not there, and a service that behaves differently from what was verified.

Example

row Room { id: id, seats: int(1, 200) }
row Booking { id: id, room: id, people: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) { else => Added: rooms += [Room { id: room, seats }]; }
    contract seats_of(room: id) -> int {
        case !(room in rooms) => fail Unknown;
        else => Seats(rooms[room].seats);
    }
}
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, people: int(1, 200), fits: bool) {
        case !fits => fail TooSmall;
        else => Booked: bookings += [Booking { id: booking, room, people }];
    }
}
effect fits(seats: int(1, 200), people: int(1, 200)) -> bool
  impl { seats >= people }
  fixtures { (4, 3) => true; (4, 5) => false; }
flow book(booking: id, room: id, people: int(1, 200)) atomic { let seats = Rooms.seats_of(room); let ok = fits(seats, people); Bookings.book(booking, room, people, ok); }
scenario a_day { Rooms.add(1, 4) => Added; book(1, 1, 3) => Booked; book(2, 1, 5) => TooSmall; book(3, 1, 1) => Booked; }

The last step books one person into a room of four, which the Rust says fits. The verifier did not run the Rust:

verify FAILED: 1 failure(s):
[1] scenario a_day step 4 (book): expected Booked, got TooSmall from Bookings.book
  world World { rooms: Rooms { rooms: [Room { id: 1, seats: 4 }] }, bookings: Bookings { bookings: [Booking { id: 1, room: 1, people: 3 }] },  __now: 0, __ids: 0, __seed: 0, __caller: 0 }

No fixture covers a room of four and one person, so the effect answered with a drawn value, false, and the step that states the product’s behaviour failed. The same program with a different scenario would pass, for the same reason.

Refactoring

row Room { id: id, seats: int(1, 200) }
row Booking { id: id, room: id, people: int(1, 200) }
unit Rooms {
    state rooms: Room[8];
    contract add(room: id, seats: int(1, 200)) { else => Added: rooms += [Room { id: room, seats }]; }
    contract fits(room: id, people: int(1, 200)) {
        case !(room in rooms) => fail Unknown;
        case rooms[room].seats < people => fail TooSmall;
        else => Fits;
        examples { { rooms: [Room { id: 1, seats: 4 }] } (1, 4) => Fits; { rooms: [Room { id: 1, seats: 4 }] } (1, 5) => TooSmall; {} (9, 1) => Unknown; }
    }
}
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, people: int(1, 200)) {
        else => Booked: bookings += [Booking { id: booking, room, people }];
    }
}
flow book(booking: id, room: id, people: int(1, 200)) atomic { Rooms.fits(room, people); Bookings.book(booking, room, people); }
scenario a_day { Rooms.add(1, 4) => Added; book(1, 1, 3) => Booked; book(2, 1, 5) => TooSmall; book(3, 1, 1) => Booked; }
scenario a_day
  Rooms.add(1, 4) => Added
  book(1, 1, 3) => Booked
  book(2, 1, 5) => TooSmall
  book(3, 1, 1) => Booked
  Rooms = Rooms { rooms: [Room { id: 1, seats: 4 }] }
  Bookings = Bookings { bookings: [Booking { id: 1, room: 1, people: 3 }, Booking { id: 3, room: 1, people: 1 }] }
4 step(s), all as expected
verify: 3 examples, 4 scenario steps and 5000 sampled checks passed

The rule is a guard in the unit that owns the seats, with examples for each outcome; the effect is gone. Every sampled state checks the guard, and the scenario passes because the rule is now in the program.

Additional remarks

The rule in the example was a comparison the core writes in one guard. A computation that genuinely needs Rust (a cryptographic check, a call to a service) stays an effect, and the rule about its answer is written in the core around it: the contract that consumes the answer states what the answer must satisfy, with an outcome for when it does not.

A row change deployed without a version and a migration

Problem

A row’s field is renamed or its meaning changes, the program is rebuilt, and the new service meets a store written by the old one. Without a version the compiler has nothing to compare; without a migrate it has nothing to run. The language derives what it can (an added field with a default, a widened bound, a new table) and refuses what would lose data, naming the declaration to write. The anti-pattern is to skip the comparison: to build the new program alone, where it checks, and deploy it onto a store it was never checked against.

Example

version 2;
row Customer { id: id, full_name: string(64) }
unit Customers {
    caps storage;
    state customers: Customer[8];
    contract add(customer: id, full_name: string(64)) { else => Added: customers += [Customer { id: customer, full_name }]; }
}

Checked alone, this program is accepted. Checked against the program that wrote the store (vishy check … --from= the version 1 program, book/refusals/migrate/anti-migration.from.vish), it is refused:

book/refusals/migrate/anti-migration.vish:1:1: error: `Customer.name` is gone and `Customer.full_name` is new with the same type: if that is a rename write `migrate Customer from 1 { full_name = old.name; }`; if the data is to be dropped write `migrate Customer from 1 { drop name; }`
  version 2;
  ^

The message names the field that is gone, the field that is new, and the two declarations that would say which of the two things happened.

Refactoring

version 2;
row Customer { id: id, full_name: string(64) }
migrate Customer from 1 { full_name = old.name; }
    examples { { id: 1, name: "Ada" } => { id: 1, full_name: "Ada" }; }
unit Customers {
    caps storage;
    state customers: Customer[8];
    contract add(customer: id, full_name: string(64)) { else => Added: customers += [Customer { id: customer, full_name }]; }
}

Checked against version 1:

migrate: 1 step(s) from version 1 to 2; 1 example(s) passed
ok: 1 row(s), 0 event(s), 0 fn(s), 1 unit(s), 1 contract(s), 0 flow(s)

The migration says the rename, its example shows one row carried forward, and a service built with --from carries the store forward exactly once and records the version.

Additional remarks

A program without caps storage has no store and no versions. A change the per-row form cannot say (data from another table, a backfill in batches) is written by hand as an effect or a script, and the chapter on versions and migrations lists what is and is not covered.

Process anti-patterns

Mistakes in how a team or a writer stage uses the tools: a verdict sent to the wrong owner, a change made in one place, a pattern left for every writer to rediscover, a pass read as more than it says. Four items, each with a program the guide’s check runs.

Repairing the writer when the verdict names a missing outcome

Problem

A verdict names a contract, so it goes back to the contract’s writer. But some verdicts that name a contract are the interface’s fault: a rule the invariant states has no outcome in the contract’s signature, so no body can keep the rule and be accepted. The writer adds the guard the rule needs and is refused for producing an outcome the interface did not declare; leaves it out and is failed by the invariant. In the experiments that shaped the language, three signatures lacked such an outcome, and every writer invented a name for it until the outcome gate refused them. Sending the verdict to the writer again is a repair round that cannot succeed; the fix is one line in the interface, and it is the interface owner’s.

Example

unit Account {
    state balance: money(2) = 0;
    invariant balance >= 0;
    contract withdraw(amount: money(2)) {
        // Withdrawn: the balance goes down by amount.
        outcomes Withdrawn, fail BadAmount;
        examples { { balance: 500 } (200) => Withdrawn { balance: 300 }; { balance: 500 } (0) => BadAmount; }
    }
}
flow withdraw(amount: money(2)) atomic { Account.withdraw(amount); }
unit Account {
    contract withdraw(amount: money(2)) {
        case amount <= 0 => fail BadAmount;
        else => Withdrawn: balance -= amount;
    }
}

The interface (the first block) promises a balance that never goes negative and declares no failure for an amount the balance cannot cover. The part (the second block) is the honest body; the verifier fails it:

verify FAILED: 2 failure(s):
[1] sampled check failed: Account.withdraw: Account.withdraw: invariant violated after case 2: state Account { balance: -13 } args (26,)
  (state Account { balance: 13 } args (26,))
[2] flow withdraw: Account.withdraw: invariant violated after case 2: state Account { balance: -51 } args (51,)
  (world World { account: Account { balance: 0 },  __now: 0, __ids: 0, __seed: 0, __caller: 0 } args (51,))

The writer who reads that verdict and adds the guard is refused (the program is book/refusals/anti-repair-guess.vish):

book/refusals/anti-repair-guess.vish:14:39: error: `Account.withdraw` produces `Insufficient`, which the interface does not declare (declared: Withdrawn, BadAmount; implicit: Unknown, Exists, Full, WrongStatus)
          case amount > balance => fail Insufficient;
                                        ^^^^^^^^^^^^

Both verdicts name Account.withdraw. Neither is the writer’s to fix.

Refactoring

unit Account {
    state balance: money(2) = 0;
    invariant balance >= 0;
    contract withdraw(amount: money(2)) {
        // Withdrawn: the balance goes down by amount; Insufficient when amount exceeds it.
        outcomes Withdrawn, fail BadAmount, fail Insufficient;
        examples { { balance: 500 } (200) => Withdrawn { balance: 300 }; { balance: 500 } (0) => BadAmount; { balance: 100 } (200) => Insufficient; }
    }
}
flow withdraw(amount: money(2)) atomic { Account.withdraw(amount); }
unit Account {
    contract withdraw(amount: money(2)) {
        case amount <= 0 => fail BadAmount;
        case amount > balance => fail Insufficient;
        else => Withdrawn: balance -= amount;
    }
}
verify: 3 examples, 0 scenario steps and 2000 sampled checks passed

The interface declares Insufficient with the example that produces it, and the same part passes. The rule for routing a verdict: an invariant broken by a contract that has no outcome for the rule goes to the interface owner first; a failed example or a refused outcome on a contract that does declare the outcome goes to the writer.

Additional remarks

A verdict that names a contract whose interface already declares the right outcome, with its example, is the writer’s, and the writer stage’s repair rounds handle it. The item is about the verdicts that look like the writer’s and are not.

A changed flow without its flows and scenarios restated

Problem

A requirement arrives in installments, or an interface is edited after scenarios were written. A flow gains a parameter or changes its order of calls; the scenarios and the other flows that call it are kept as they were. The checker refuses the kept scenario, which is the cheap case; the expensive case is a kept scenario that still types and now states behaviour the changed flow no longer has, or a kept flow that no longer keeps a rule the change added. In the experiments that shaped the language, twenty-five of thirty-one assembly failures in one run were one account’s new rule that no kept flow exercised. A change to a flow restates, in the same place, every flow and scenario that exercises it.

Example

row Booking { id: id, room: id, day: int(1, 366), slot: int(1, 8) }
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, day: int(1, 366), slot: int(1, 8)) {
        else => Booked: bookings += [Booking { id: booking, room, day, slot }];
    }
}
flow book(booking: id, room: id, day: int(1, 366), slot: int(1, 8)) atomic { Bookings.book(booking, room, day, slot); }
scenario kept_from_before { book(1, 1, 10) => Booked; }
book/refusals/anti-stale-scenario.vish:9:29: error: scenario `kept_from_before` step 1: `book` takes 4 argument(s), got 3
  scenario kept_from_before { book(1, 1, 10) => Booked; }
                              ^^^^^^^^^^^^^^^^^^^^^^^^^

Refactoring

row Booking { id: id, room: id, day: int(1, 366), slot: int(1, 8) }
unit Bookings {
    state bookings: Booking[8];
    contract book(booking: id, room: id, day: int(1, 366), slot: int(1, 8)) {
        else => Booked: bookings += [Booking { id: booking, room, day, slot }];
    }
}
flow book(booking: id, room: id, day: int(1, 366), slot: int(1, 8)) atomic { Bookings.book(booking, room, day, slot); }
scenario restated { book(1, 1, 10, 2) => Booked; }
verify: 0 examples, 1 scenario steps and 2000 sampled checks passed

The scenario is restated with the slot the flow now takes. When the change is to a rule rather than a signature, nothing refuses the stale scenario, and restating it is the only check: read every scenario that calls the flow, and every flow that commands a unit the rule couples, and say again what each does under the new rule.

Additional remarks

A scenario that the change does not reach (it calls other flows, on other units) is kept as it is. The item is about the ones that exercise what changed.

Idioms left unnamed for writers

Problem

A rule in an interface needs a pattern to write: a quota spread over rows in order, a selection by rank with ties broken, a removal by a computed list. A writer who is not told the pattern’s name and shape invents one, and the verifier refuses the inventions one by one. The quota-over-rows pattern cost twelve writer rounds before the writer reference stated it; the next writer passed in one. The interface owner names the idiom in the rule’s comment, and the pattern is in the reference the writers receive.

Example

row Batch { id: id, sku: id, qty: int(0, 99) }
unit Stock {
    state batches: Batch[8];
    fixture Three = { batches: [Batch { id: 1, sku: 7, qty: 5 }, Batch { id: 2, sku: 7, qty: 3 }, Batch { id: 3, sku: 8, qty: 4 }] };
    invariant all(b in batches: b.qty >= 0);
    contract take(sku: id, qty: int(1, 99)) {
        // Short when the sku's batches hold fewer than qty in total; else qty is taken from the batches, the oldest first, each giving what it has until qty is met.
        outcomes Taken, fail Short;
        examples {
            Three (7, 6) => Taken { batches: [Batch { id: 1, sku: 7, qty: 0 }, Batch { id: 2, sku: 7, qty: 2 }, Batch { id: 3, sku: 8, qty: 4 }] };
            Three (7, 9) => Short;
        }
    }
}
flow take_stock(sku: id, qty: int(1, 99)) atomic { Stock.take(sku, qty); }
ok: 1 row(s), 0 event(s), 0 fn(s), 1 unit(s), 1 contract(s), 1 flow(s)

Nothing catches it; the interface is complete and its examples pin the answer. The sentence “the oldest first, each giving what it has until qty is met” is a specification of a delta that the language writes in one particular way, and the writer has to find that way.

Refactoring

row Batch { id: id, sku: id, qty: int(0, 99) }
unit Stock {
    state batches: Batch[8];
    fixture Three = { batches: [Batch { id: 1, sku: 7, qty: 5 }, Batch { id: 2, sku: 7, qty: 3 }, Batch { id: 3, sku: 8, qty: 4 }] };
    invariant all(b in batches: b.qty >= 0);
    contract take(sku: id, qty: int(1, 99)) {
        // Short when the sku's batches hold fewer than qty in total; else the quota-over-rows pattern, oldest first
        // (writer reference): each batch gives up what is left of qty after the batches before it.
        case sum(where(b in batches: b.sku == sku).qty) < qty => fail Short;
        else => Taken: batches := for b where b.sku == sku => with(b, qty, b.qty - clamp(qty - sum(where(c in batches: c.sku == sku && as_int(c.id) < as_int(b.id)).qty), 0, b.qty));
        examples {
            Three (7, 6) => Taken { batches: [Batch { id: 1, sku: 7, qty: 0 }, Batch { id: 2, sku: 7, qty: 2 }, Batch { id: 3, sku: 8, qty: 4 }] };
            Three (7, 9) => Short;
            Three (8, 4) => Taken { batches: [Batch { id: 1, sku: 7, qty: 5 }, Batch { id: 2, sku: 7, qty: 3 }, Batch { id: 3, sku: 8, qty: 0 }] };
        }
    }
}
flow take_stock(sku: id, qty: int(1, 99)) atomic { Stock.take(sku, qty); }
verify: 3 examples, 0 scenario steps and 2000 sampled checks passed

The comment names the pattern and where it is written down, and the body is the pattern as the reference states it: each row gives up what is left of the quota after the rows before it, so one set-valued delta does the whole thing. The third example, on the other sku, is the one a writer most often gets wrong (the untouched batches must stay untouched).

Additional remarks

The item is for the interface owner and for whoever keeps the writer reference: a pattern that took a writer more than one round is a paragraph the reference is missing. A pattern the reference already names is the writer’s to look up.

A pass on the exampled states read as a pass on all states

Problem

verify ends with one line: so many examples, so many scenario steps, so many sampled checks passed. It is read as “the program is correct”. It says three narrower things: the examples agree with the bodies; the scenarios ran as stated; in the sampled states, no rule the program states was broken. A carried value with no property, a rule left in prose, a fact hidden in an effect, all pass, because there was nothing for the sample to break. The cost is confidence in the wrong place: the program is trusted for what nobody asked it to keep.

Example

row Room { id: id, closed: bool }
unit Rooms {
    state rooms: Room[8];
    fixture Two = { rooms: [Room { id: 1, closed: false }, Room { id: 2, closed: false }] };
    contract close(room: id) {
        else => Closed: rooms[room].closed := true;
        examples { Two (1) => Closed { rooms: [Room { id: 1, closed: true }, Room { id: 2, closed: false }] }; }
    }
    contract open_count() -> int {
        else => Count(len(rooms));
        examples { {} () => Count(0); Two () => Count(2); }
    }
}
verify: 3 examples, 0 scenario steps and 2000 sampled checks passed

open_count counts closed rooms as open. Three examples and two thousand sampled checks passed. The sampled checks held the unit to its invariant, and it has none; the examples were on states with no closed room. The line is true and the program is wrong.

Refactoring

row Room { id: id, closed: bool }
unit Rooms {
    state rooms: Room[8];
    fixture Two = { rooms: [Room { id: 1, closed: false }, Room { id: 2, closed: false }] };
    contract close(room: id) {
        else => Closed: rooms[room].closed := true;
        examples { Two (1) => Closed { rooms: [Room { id: 1, closed: true }, Room { id: 2, closed: false }] }; }
    }
    contract open_count() -> int {
        ensures result == count(r in rooms: !r.closed);
        else => Count(count(r in rooms: !r.closed));
        examples { {} () => Count(0); Two () => Count(2); }
    }
}
verify: 3 examples, 0 scenario steps and 2000 sampled checks passed

The same line, one more example, and now it means something: the property on open_count is a rule the sample could have broken and did not. The Contract page showed the wrong body failing under that property on a state with four closed rooms. What the pass covers is decided by what the program states, not by the number at the end of the line.

Additional remarks

Read the line as a report of what was tried. Then read the program for what it does not state: a carried value without a property, a rule in a comment, an effect with a rule inside, a table bound smaller than the state the bug needs. The chapter “What the verifier cannot see” lists the four holes, two of which now have a check; the other two are the reader’s. Before serving, run the verifier on three seeds and at three thousand iterations, as the handbook says: a program has passed at one thousand and failed at three.

The reference

Carried from docs/reference.md on 30 Sept 2026; the file is the source.

Reference

One section per feature. Each example is small enough to paste into the playground; the longer ones are in docs/examples/ and tests/*_ok.vish. The compiler knows a small core; the rest is a prelude written in the language.

1. A program

A program is declarations at the top level: rows, enums, events, functions, units, flows, invariants, derived fields, scenarios, routes, effects, and rust dependencies. It may span files and namespaces. The one thing every program has is at least one unit; the smallest useful one is a unit and a scenario.

2. Types

typemeaning
bool, int, int(lo, hi)booleans; unbounded ints; ints in a range the sampler respects
idan opaque key; an int literal may be written where an id is expected; ids have no arithmetic (as_int, as_id convert)
string(n)UTF-8 text of at most n bytes, written "…" with escapes \", \n, \t, \\
enum Name { a, b }a variant type; values are Name.a; variants order by declaration; read with match v { Name.a => x, Name.b => y } (every variant once, all of them or an else arm)
money(s)an amount in minor units with s decimals; adds with money of the same scale, scales by int, never mixes
instant, durationmilliseconds; instant ± duration is an instant, instant − instant a duration, duration × int a duration
option<T>some(e) or none; read with match o { some x => a, none => b }
Rowa declared row as a value
Row[n]a keyed table of rows by id, at most n for verification
T[n], Row[n] ordereda positional list of at most n elements
map<K, V>an ordered map; keys are int, id, string or enum

Literals of money, instant and duration are plain ints. [] and map() are empty literals typed by their context.

3. Rows

row Task { id: id, title: string(64), status: Status, created: instant, closed: option<instant>, tags: id[4], home: option<Address> }

A field may be a scalar, a string, an enum, an option of one, a list of scalars, another row, or an optional row. Rows nest but never recurse. A row kept in a keyed table needs an id: id field. Row literals name every non-derived field: Task { id: 1, title: "a", … }; a field whose value is a variable of the same name may be written once, Shipment { id: shipment, order }.

4. Units and state

unit Todo {
    caps storage, outbox;
    state tasks: Task[8];
    state next: int = 1;
    state prices: map<id, money(2)>;
    fn open(t: Task) -> bool = t.status == Status.open;
    invariant all(t in tasks: as_int(t.id) < next);
    contract …
}

A unit owns its state and nothing else may touch it: only the unit’s contracts change it, which is the language’s visibility rule. state fields are tables, lists or scalars with optional initial values; tables start empty. A unit may hold functions that read its state, one invariant (combine with &&) that must hold after every contract call, fixtures, machines, derived fields and a commutes declaration. caps names capabilities: outbox allows emit, storage makes the state persist.

5. Contracts

contract withdraw(amount: money(2)) {
    case amount <= 0 => fail BadAmount;
    case amount > balance => fail Insufficient;
    else => Ok: balance -= amount;
    examples { { balance: 500 } (200) => Ok { balance: 300 }; { balance: 100 } (200) => Insufficient; }
}
contract bump() => Ok: n += 1;                     // one guardless case: no block
contract level(item: id) -> int { outcomes Level(n), fail Unknown; }   // a stub: outcomes declared, no cases
contract read() -> int => Value(n);

Cases are tried in order; the first guard that holds decides; else is the catch-all. A case names an outcome, or fail Outcome, which changes nothing and aborts an atomic flow, or stop Outcome, a success that ends the flow at this call (its writes and the earlier steps’ commit; the later steps do not run; no value). A stub declares them the same way: outcomes Added, stop AlreadyDone, fail Exists;. An outcome’s kind is one for the whole program: a name is a success, a failure or a stop everywhere it appears, as fail has always been. A contract of a unit that declares caps ids may say uses ids; and call fresh() in its cases: each call is a new id above every id the caller knows (the whole world inside a flow, which must say uses ids; the unit alone in an example or a sampled check, where the ids are 1 + the largest present, in order), so a contract can create one row per element of a list (fold(w in winners, acc = []: append(acc, Sale { id: fresh(), … }))) and answer their ids, and the door passes no ids for rows it does not choose. A carried value is written inline, Found(expr), with the return type after -> (default int); the value is computed on the pre-state. Outcome names are yours; the same name means the same thing everywhere.

Deltas after : say what changes and nothing else changes. Right-hand sides read the pre-state.

deltameaning
x := e, x += e, x -= e, x max= e, x min= escalars (int, money, duration for +=/-=)
t += [row, …], t -= [k, …]insert rows / remove keys (keyed); append / remove first occurrences (positional)
t[k] := row, t[k].f := e, t[k].f += e, remove t[k]one row of a keyed table
t.f := e, t.f := for x where p => eevery row’s field, or only the rows where p holds
t := for x where p => rowexprreplace matching rows (ids unchanged)
emit Event(args)append to the unit’s outbox (needs caps outbox)

A keyed table is never assigned whole (t := [...] is refused): rows enter with +=, leave with -= or remove, and change with t[k] := row, t[k].f := e, t.f := e or t := for x where p => row.

Implicit outcomes, never guarded by hand: Unknown for a missing key, Exists for an inserted key that is present, Full for a table past its bound, WrongStatus for a move a machine forbids. They may appear in examples and scenarios.

Case bindings compute a selection once: case any(n in numbers: ok(n)) => let n = best(x in numbers: ok(x) by (x.added)); Picked(n.id): numbers[n.id].used := true;.

Examples are {before} (args) => Outcome[(value)] [{after}]. A state literal lists the fields that differ from the initial values, so {} is the initial state; a fixture name may stand for a state (Two (1, 5) => Ok Two with { … }). An example with no after-state means nothing changed, and the verifier enforces it. Failures never have after-states.

ensures adds a check on the post-state: items := heap_pop(items) ensures perm(append(items, result), old(items)); with old(f), result, unchanged(), emitted([…]).

Contract-level ensures. ensures expr; as a line of the contract (a stub’s or a body’s) is checked after every successful case, with result the carried value (the property then applies to the cases that carry one), bare fields the post-state, old(f) the pre-state, unchanged() the whole unit. Several lines conjoin. An interface’s contract-level ensures survive the merge with a part, so the part’s cases are held to them whatever the writer wrote; a part may add its own. See docs/blind-spots.md.

Deltas apply in order on the old state’s values; a column delta (t.f := e, t.f += e) skips a row that an earlier delta of the same case removed.

Stubs. A contract with outcomes A, fail B, C(value); and no cases is an interface stub: it declares the outcome shapes so examples, scenarios and flows type-check, and a call reports “not implemented (interface stub)” until a part supplies the cases. A contract has either cases or outcomes, never both. vishy check --interface accepts a program only if every contract is a stub.

Parts. A unit may be declared twice across the inputs: the interface, with state, fixtures, invariant and stub contracts, and a part, unit Name { contract … { cases; examples { … } } }, holding only contract bodies. The compiler merges them: the part’s header must equal the stub’s (names, bounds, return type, uses); the merged contract has the part’s cases, the stub’s declared outcomes and the examples of both, and the checker requires the cases to produce every declared outcome with its fail and value shape and nothing undeclared (the implicit outcomes excepted). A part’s examples may name the interface’s fixtures. A part with state, a body for a contract the interface does not declare, or a second body for the same contract is refused. See docs/toolchain.md, “Parts and the writer stage”.

6. Expressions

Arithmetic + - * / %, comparisons, && || !, if c then a else b, let x = e in e, match (on an option: some x => a, none => b; on an enum: one arm per variant, or an else), row fields r.f, with(r, f, e), table access t[k] (guard with k in t), len, lookup(t, k), append, range(n), columns t.f with elementwise lifting (jobs.priority > 3 is a bool column), reductions count, all, any, sum, largest, smallest, min, max, abs, clamp, percent, round_to.

Quantifiers bind a variable: all(x in t: p), any, count, first, where(x in t: p) (the matching elements; where(…).id for their ids), best(x in t: p by (k1, k2)) (the match with the smallest key tuple; bools sort false first; ties by id), fold(x in t, acc = e0: body), repeat(n, s = x0: body). There are no list comprehensions and no loops in expressions.

Strings: concat, starts_with, ends_with, contains, substr(s, start, count) (in bytes, like len; a count that ends inside a character includes that character), byte_at(s, i) (the byte at position i as an int, -1 past the end; the one string read that allocates nothing), to_upper, to_lower, trim, find, split, join, replace, int_to_string, string_to_int. Maps: map(k => v, …), get (an option), get_or, put, remove_key, has_key, keys, values, len. Time: seconds, minutes, hours, days, bucket(t, d), as_duration, as_instant.

7. Functions and the computation layer

fn fee(amount: money(2), tier: Tier) -> money(2) = if tier == Tier.gold then percent(amount, 1) else percent(amount, 3);
examples fee { (10000, Tier.basic) => 300; (10000, Tier.gold) => 100; }

fn heap_push(xs: any[], v: any) -> any[] {
    var h = append(xs, v);
    var i = len(xs);
    repeat len(xs) + 1 {
        let p = (i - 1) / 2;
        if i > 0 && h[p] < h[i] { h := swap_at(h, p, i); i := p; } else { break; }
    }
    h
}

A function is an expression, or a block of statements followed by its result: let and var locals (var toks: Token[64] ordered = []; names the type when the value alone does not, as an empty list or none), x := e on a var, statement if with else if chains, repeat n { … } with break. x := append(x, e) and x := concat(x, e) grow x in place in the compiled code; a loop that builds a list or a string is linear. Every loop is bounded, there is no while and no recursion, so every function is total. Top-level functions may carry examples, run by verify. Blocks are for functions only; contracts stay declarative. fn f(…) -> t impl { rust } is the escape hatch: trusted, not verified; its list, row, string, option and map parameters arrive by reference (&Vec<T>, &String), scalars by value. The prelude (stdlib/prelude.vish) provides perm, submultiset, sorted, is_heap, index_of, take, drop, set_at, swap_at, remove_first, insert_sorted, heap_push, heap_pop and the time helpers, all written in the language.

Sealed functions: the general-computation layer

sealed row Node { v: int, kids: Node[] }
sealed fn total(n: Node) -> int = n.v + fold(k in n.kids, acc = 0: acc + total(k));
sealed fn count_to(n: int(0, 100)) -> int(0, 100) { var i = 0; while i < n { i := i + 1; } i }
sealed fn words(s: string) -> int = len(split(s, " "));
examples total { (Node { v: 1, kids: [Node { v: 2, kids: [] }] }) => 3; }

A core function is total: no recursion (direct or through other functions), loops bounded by repeat, every string and list bounded, no return (a block ends with its result expression). That is what makes contracts and rules checkable and lets the compiler know everything. General computation, a parser, a tree walk, anything that needs recursion, lives behind a declared boundary instead: a sealed fn may call itself and other functions, loop with while, leave early with return e;, take and return string and T[] without a bound, and use sealed rows, which may contain themselves (inside a sealed function or row, Node[] is a list of rows, not a keyed table). Sealed rows exist only inside sealed functions: no unit state, contract, flow, event, view or core row may hold one, and a sealed function called from the core must return a core type.

What the seal promises and what it does not: a sealed function is pure (no unit state, no events, no effects) and its result is trusted only to its declared type, so the boundary checks it (an int(0, 9) result of 10 is a failure naming the function). Totality is not promised: verify runs a sealed function’s examples, and every call to it from a contract or flow during a sampled check, under a step budget of VISHY_FUEL steps (default 10 million, spent per call and per loop iteration, the same on both verifier paths); a function that exhausts it, or recurses deeper than 100,000 calls, fails the example or the check it was reached from. Sealed functions are never sampled on their own: their examples are their test. The service runs them as written, budget included.

8. Flows

flow place(qty: int) uses clock, ids atomic {
    let order = fresh();
    let total = Carts.total(order);
    Orders.place(now, order, qty, total);
    ensures Orders.count == old(Orders.count) + 1;
}

A flow is a sequence of contract calls and effect calls, and a transaction: atomic (the default) restores the whole world if any call fails; serial does not. A step written keep Unit.c(…); in an atomic flow commits what the flow has written so far: a later failure restores the world to right after that step, not to the start (the served door then answers the failure with the kept writes stored), so a refused attempt can be recorded before the refusal. A call that answers a stop outcome ends the flow there as a success; ensures is checked only when the flow runs to its end. Units never call each other; only flows compose them. A carried value can be bound with let and passed on. uses clock binds now; uses ids allows let x = fresh();, an id absent from every table. A flow’s outcome is its last call’s. ensures is checked after the flow commits, with Unit.field and old(Unit.field). A scenario or a route may call a contract directly, Counter.bump(), which is an implicit one-call atomic flow.

uses caller binds caller: id, the verified identity of the request: the served door takes it from the bearer token (401 without one) and never from the body, the command line from VISHY_CALLER, a scenario step from as N (as 7 remove_note(1) => NotOwner;), and the verifier samples it like any argument. A flow that does not use caller cannot name it; a body that passes an identity in is what uses caller replaces.

9. Views

A view is a read over the world, declared in the language so that who sees what is a rule the compiler holds:

view my_issues(project: id) uses caller
    = where(i in Issues.issues: i.project == project && any(m in Members.members: m.user == caller && m.project == project));
view issue_count(project: id) = count(i in Issues.issues: i.project == project);
route GET "/projects/{project}/issues" => my_issues(project);

view name(params) [uses caller] = expr;. The expression is typed like a world invariant, with Unit.field in scope, the parameters, and caller when the view uses it; its type is inferred and may be a list of rows, a row, an option or a scalar (not a unit). A view never writes: there are no deltas, and a route to it must be GET. Scenarios assert on views with name(args) => Value(v), as N before a step setting the caller. The verifier evaluates every view on reachable worlds with sampled arguments and fails on a panic (an empty first, an absent key), counted among the sampled checks; it cannot judge whether the value is right beyond the scenarios and the examples. The door serves a view route from the stored world without a transaction, as {"ok": true, "value": v}, and pages a list with limit and offset query parameters ("total" and "offset" alongside); a view served by a route cannot name a parameter limit or offset. The clients get one method per view route with the value’s type. A view is interface-owned: writers never write one.

10. World invariants and derived state

invariant all(l in Stock.levels: l.reserved <= l.on_hand);
derived Ledger.cash = sum(Payments.payments.captured) - sum(Payments.payments.refunded);
derived Stock.levels.reserved = for l => sum(where(x in CartLines.lines: x.sku == l.sku).qty);

A top-level invariant names units’ state and must hold after every flow; the verifier samples only worlds that satisfy it. A derived field is defined by an equation and maintained by the compiler after every call or flow, never assigned; inside a unit, derived total: int = sum(accts.bal);. A unit’s invariant may not mention a world-derived field of that unit; state such a rule at the top level.

11. Machines

machine orders.status { start Status.placed; Status.placed -> Status.paid; Status.paid -> Status.shipped; Status.placed..Status.paid -> Status.cancelled; }

Declares the legal moves of a row field; a delta that moves it any other way fails with WrongStatus. Staying in place is a move too and needs its edge. One contract advance(k, to) replaces one per transition.

12. Commutativity and fixtures

commutes { started, ended }; makes the verifier apply sampled pairs of those contracts in both orders from a sampled state and report when the orders disagree. fixture Two = { accts: [ … ] }; names a state for examples.

13. Scenarios

scenario checkout {
    at 1000 submit(7, 42, Kind.check) => Queued(s);
    at 1500 started(s) => Started;
    status(s) => Status("running\n");
    Cache.lookup(42) => Hit(s);
}

A sequence of flow or contract calls from the initial world with the outcome each must produce; at N sets the clock; as N sets the caller for a flow that uses caller; a bare name as the expected value binds the carried value for later steps; Outcome(_) accepts any value. Run prints the trace; verify stops a scenario at its first wrong step and shows the world.

14. Namespaces and files

A program may be many files. A file’s namespace is namespace x; at its top, else the directory it was given in, else the root. Inside its namespace a declaration is bare; elsewhere it is money::Fee, imported with use money::Fee;, use money::Fee as F; or use money::*;. Rows, enums, events, functions, units, flows and effects are what a namespace exports; state and contracts belong to their unit, so there is no pub. The prelude is visible everywhere. Names may not contain __. A unit may be split across files as an interface and a part (section 5).

15. Packages

A directory with package.vishy (name, version, optional namespace, dep x = path "…" or dep x = git "…" tag "…") is a package; dependencies resolve recursively, one version per name. vishy interface prints the public surface; vishy diff old new classifies a change as patch, minor or major and refuses a version that does not cover it.

16. Storage and capacity

caps storage; on a unit makes its state persist and changes nothing else. vishy schema prints the SQLite schema; vishy service writes a crate that runs each flow as a transaction over SQLite or Turso. The bound in Row[8] is for verification; a rule about capacity says cap(rows), which is the bound when verifying and unbounded in a service.

17. Migrations

A program with stored units declares version N;. When a row or a stored scalar changes between versions, the compiler carries the store forward, deriving what it can and requiring the rest to be written:

version 2;
row Customer { id: id, full_name: string(64), tier: Tier, greeted: int }
migrate Customer from 1 { full_name = old.name; }
    examples { { id: 1, name: "Ada" } => { id: 1, full_name: "Ada", tier: Tier.basic, greeted: 0 }; }

Derived without a declaration: a field added with a default (an enum’s first variant, 0, the empty string, none), a widened bound, a new table, a new scalar state. Never derived: losing data. A removed field needs drop field; inside the migrate block, or an assignment that reads it (a rename), and the compiler says which when a field of the same type appeared alongside. Written as migrate Row from N { field = expr; … }: a rename, a recomputed field, a narrowed type; old is the row as version N declared it, typed against that declaration, so old.name is an error when version N had no name. migrate drop Unit; allows a vanished unit’s tables to be dropped. Examples give an old row and the new row it must become; they run at check time. The verifier does not sample migrations.

vishy check new.vish --from=old.vish types the migrations and runs the examples; vishy service new.vish out/ --from=old.vish emits a service whose migrate carries a store at version N forward exactly once and records the version (schema_versions); a service built without --from refuses a store at another version and says which program to build it from. Not covered, by design: tables Vishy did not create, migrations that need data from other tables or outside, batched zero-downtime backfills, and anything a per-row expression cannot say; those are written by hand as an effect or a script. A rewrite (a rename) rebuilds the whole table; on a very large table run it in a window.

18. Routes and effects

route POST "/submit" => submit(client, hash, kind);
route GET "/status/{submission}" => status(submission);

rust serde_json = "1";
effect json_len(s: string(200)) -> int
  impl { serde_json::from_str::<serde_json::Value>(&s).map(|v| v.as_array().map_or(0, |a| a.len() as i64)).unwrap_or(-1) }
  fixtures { ("[1,2,3]") => 3; ("x") => -1; }
flow count(s: string(200)) atomic { let k = json_len(s); Counter.set(k); }

A route invokes exactly one flow; path parameters and JSON fields bind its parameters by name; the response is the outcome, 200 or 409; GET routes may only invoke query flows. An effect is a Rust-backed function with effects, callable from flows and never from contracts. In verification it returns a fixture’s value or a sampled one; in a service build it runs the Rust with the declared crate. The language has no I/O of its own: effects are facts a flow emits and a runtime performs.

Deliveries. deliver Event to webhook "https://…";, deliver Event to webhook env "NAME";, deliver Event to mail "ops@example.com";, deliver Event to effect name; say where an emitted event goes. A program declares the destinations; the service does the delivery: one outbox row per (event, delivery) written in the same store transaction as the flow’s state change, so an event is never lost and never recorded without its cause; a drainer posts each row as JSON with an Idempotency-Key (<tenant>-<row id>, stable across retries so the receiver can deduplicate), marks it delivered on a 2xx, and otherwise retries with exponential backoff (1 s, 2 s, 4 s, … up to 5 minutes, at most 20 attempts). Delivery is at least once. A webhook target is a URL or the name of an environment variable holding one; a mail target is an address, delivered by posting {to, subject, event} to the endpoint in VISHY_MAIL_HOOK; an effect target is a declared effect (body: string(n)) -> bool, called by the drainer with the event as JSON, delivered when it returns true, retried when it returns false or panics. The effect is how any crate becomes a destination: a broker client, a queue, a mail library, with the Rust inside the effect’s impl and the choice of destination one line of Vishy:

rust lapin = "2";
effect publish(body: string(4000)) -> bool impl { /* the AMQP client */ } fixtures { ("") => false; }
deliver OrderPlaced to effect publish;

The verifier does not deliver anything; scenarios and ensures emitted([…]) are what check that the right events are emitted.

19. Verification

verify runs every example, every scenario, sampled checks of every contract against its unit’s invariant and ensures from random and reachable states, reachable walks of the flows checking atomicity, flow ensures, world invariants and capacities, and commutativity pairs. It reports each failure with the state and arguments that caused it. After the checks it requires reachability: every outcome a contract’s cases can produce, the implicit four excepted, must have been produced by an example or a sampled call, else the case is unreachable or unverified and verify fails. It is bounded random testing, not proof; see Limits and measurements and, for what it cannot see, docs/blind-spots.md.

Verification is partitioned by what a flow can change. A flow’s closure is the units it calls plus every unit a world-derived column carries a change into. After the flow, the verifier re-checks only the world invariants that mention a unit in that closure and recomputes only the derived columns that write one; the others cannot have changed, so their values stand. A flow’s reachable walk draws only from the flows of its component of the unit graph (vishy graph prints the components and, per flow, what its verification touches). Both verifier paths partition the same way and report the same failure, including the invariant’s number in source order.

20. Reserved names

old, out, result, self, staged, outbox, sealed, while, anything starting with __, Rust keywords, and the emitted runtime’s type names.

Building with a team

On a real project today, a dialer platform, one agent has to do QA on the softphone while another integrates something into the frontend. Two agents, two features, one codebase. Agents struggle badly with that level of parallelism in any ordinary language: not because they are weak at either task, but because of what the codebase does not say. This appendix is about that, and about the part of it Vishy changes. It ends with the team handbook’s substance: the roles, the sequence on the room-booking program, and what to do when a step fails.

Why it is structural

The trouble is not a prompting problem. It comes from four facts about an ordinary codebase, and no instruction to either agent removes them.

  • Nothing says who owns what. Two agents on different features touch the same tables, the same handlers, the same helpers, because nothing in the code marks a table as one feature’s. Each edit is legal; the pair is not.
  • The only arbiter is the whole test suite. It is slow, it is shared, and its answer is “something broke”. It does not say whose change broke it, so both agents read the failure, both suspect the other, and one of them re-runs everything to find out.
  • The contract between frontend and backend lives in prose and in heads. The frontend agent builds against an integration that is still moving, guesses at the shape of an answer, and finds out at the end whether the guess held.
  • Coordination is done by reading each other’s diffs. That is the thing agents do worst: a diff is a list of edits without the intent that made them, and reconciling two of them is the work the whole arrangement was meant to avoid.

What the language changes

On the part of the system the core covers, the state and its rules, the language takes each of the four facts away.

  • State has one owner, and the language refuses a second writer. A unit’s state is changed only by that unit’s contracts; another unit cannot name it, and the checker says so (What Vishy is not shows the refusal). Two agents on two units cannot touch the same table, because there is no way to write it from outside.
  • The interface is fixed first, with examples, and a writer gets a local verdict in under a second with nobody else’s work present. The interface owner writes every row, every rule, every contract’s outcomes and examples, before any body exists. A unit’s writer then receives only the shared declarations and their own unit, writes the bodies, and checks them alone: the examples, the interface’s examples, and sampled calls against the unit’s own invariant. The verdict names the contract, the state and the arguments. Nothing another writer is doing is in that check.
  • Assembly is the one shared step, and the verifier decides, not a reviewer. When every part passes, the whole program is checked and verified: every example, every scenario, the walks over the flows, the world rules. A part is accepted when its local check passes; a program is accepted when the verifier passes. No review replaces either, and no review is asked to.
  • A verdict names its contract, and the routing is written down. A failure that names a contract goes to that unit’s writer; a failure on a world rule or a scenario goes to the interface owner first, who decides whether the rule, the flow or a body is wrong. The Process page of the anti-patterns part says which verdicts that name a contract are the interface’s fault, and how to tell.
  • The wire contract is generated from the interface, so a frontend agent builds against a door that does not move. The routes, the request bodies with their bounds, the outcomes a client switches on and the typed clients all come from the interface file, before any body is written (the wire contract is that page). The frontend agent’s guess is replaced by a generated file, and an interface change is a new version of that file, diffed, not a surprise.

What it does not change

Said plainly: the softphone QA is audio, WebRTC and screens, and the frontend integration is host-side code. Both sit outside the core, in the world the three-kinds-of-code table puts under “effects” and beyond, and the parallelism problem stays hard there for everyone, this language included. What the language gives those two agents is narrower than a solution and still real: a fixed contract to build against, and a backend whose rules are already verified. The QA agent tests the phone, instead of rediscovering rule bugs through it.

The roles

One application has one interface owner and any number of unit owners.

  • The interface owner writes the brief and the interface: rows, enums, events, every unit’s state, fixtures, invariants and stub contracts with examples, the flows, views, routes, world rules, scenarios, deliveries, migrations. The interface is the only shared file. Nobody else edits it; a unit owner who needs a change asks for it.
  • A unit owner writes the contract bodies of one unit, as a part, and nothing else. The writer stage, vishy write, can be that owner: a cheap model behind the same checks. A person or a stronger agent takes a unit the stage cannot finish.
  • The verifier decides. A part is accepted when its local check passes; a program is accepted when vishy verify passes at three thousand iterations.

The split works because a unit cannot be reached into. Nothing outside a unit reads its state, so a unit’s body can be written and checked alone, and the interface is the whole of what its writer needs.

The sequence

The example is the guide’s room-booking program: two units, six contracts, four flows, two views, six routes, one scenario. Every line the tools print below is regenerated by the guide’s check; the timings are the team handbook’s, from its run on a laptop, and are named as such.

1. The brief

A page of prose: what exists, the rules, who may do what, the screens. For the rooms it is six lines: a room has a name and seats and can be closed, and closing it cancels its bookings; a booking is one room, one day, one slot, for some people, held by whoever made it; rule 1, a booking’s room exists, is open and seats at least the booking’s people; rule 2, a room has at most one booking per day and slot; only the holder cancels; two screens, my bookings and a room’s day. Number the rules; the interface cites them. The appendix on authoring an interface says how to get from any requirement to this page.

2. The interface

The owner writes it against the checker until it passes:

row Room { id: id, name: string(64), seats: int(1, 200), closed: bool }
row Booking { id: id, room: id, holder: id, day: int(1, 366), slot: int(1, 8), people: int(1, 200) }

// rule 1: a booking's room exists, is open, and seats at least the booking's people.
invariant all(b in Bookings.bookings: any(r in Rooms.rooms: r.id == b.room && !r.closed && b.people <= r.seats));

unit Rooms {
    caps storage;
    state rooms: Room[32];
    fixture Two = { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: false }, Room { id: 2, name: "Beta", seats: 10, closed: true }] };
    contract add(room: id, name: string(64), seats: int(1, 200)) {
        // Exists when the room id is present; else insert Room { id: room, name, seats, closed: false }.
        outcomes Added, fail Exists;
        examples {
            {} (1, "Alpha", 4) => Added { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: false }] };
            Two (1, "Again", 3) => Exists;
        }
    }
    contract fits(room: id, people: int(1, 200)) {
        // Fits when the room exists, is open and has at least `people` seats; RoomClosed when it is closed; TooSmall when open with fewer seats; an absent room is Unknown. Changes nothing.
        outcomes Fits, fail RoomClosed, fail TooSmall;
        examples {
            Two (1, 4) => Fits;
            Two (1, 5) => TooSmall;
            Two (2, 1) => RoomClosed;
            Two (9, 1) => Unknown;
        }
    }
    contract close(room: id) {
        // Closed: the room's closed becomes true (already closed is fine); an absent room is Unknown.
        outcomes Closed;
        examples {
            Two (1) => Closed { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: true }, Room { id: 2, name: "Beta", seats: 10, closed: true }] };
            Two (2) => Closed;
            Two (9) => Unknown;
        }
    }
}

unit Bookings {
    caps storage;
    state bookings: Booking[64];
    fixture One = { bookings: [Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }] };
    // rule 2: one booking per room, day and slot.
    invariant all(a in bookings: all(b in bookings: a.id == b.id || a.room != b.room || a.day != b.day || a.slot != b.slot));
    contract book(booking: id, room: id, holder: id, day: int(1, 366), slot: int(1, 8), people: int(1, 200)) {
        // Exists when the booking id is present; Taken when the room already has a booking on that day and slot; else insert Booking { id: booking, room, holder, day, slot, people }.
        outcomes Booked, fail Taken, fail Exists;
        examples {
            {} (1, 1, 100, 10, 2, 3) => Booked { bookings: [Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }] };
            One (1, 2, 100, 11, 1, 1) => Exists;
            One (2, 1, 101, 10, 2, 1) => Taken;
            One (2, 1, 101, 10, 3, 1) => Booked { bookings: [Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }, Booking { id: 2, room: 1, holder: 101, day: 10, slot: 3, people: 1 }] };
        }
    }
    contract cancel(booking: id, holder: id) {
        // Cancelled: removes the booking when its holder is `holder`; NotHolder when it is someone else's; an absent booking is Unknown.
        outcomes Cancelled, fail NotHolder;
        examples {
            One (1, 100) => Cancelled { bookings: [] };
            One (1, 101) => NotHolder;
            One (9, 100) => Unknown;
        }
    }
    contract clear_room(room: id) {
        // Cleared: removes every booking of the room (none is fine).
        outcomes Cleared;
        examples {
            One (1) => Cleared { bookings: [] };
            One (2) => Cleared;
        }
    }
}

flow add_room(room: id, name: string(64), seats: int(1, 200)) atomic { Rooms.add(room, name, seats); }
// rule 1 is kept: the bookings go before the room closes.
flow close_room(room: id) atomic { Bookings.clear_room(room); Rooms.close(room); }
// rule 1 is kept: the room decides whether the booking fits before it is made.
flow book(booking: id, room: id, day: int(1, 366), slot: int(1, 8), people: int(1, 200)) uses caller atomic { Rooms.fits(room, people); Bookings.book(booking, room, caller, day, slot, people); }
flow cancel(booking: id) uses caller atomic { Bookings.cancel(booking, caller); }

view my_bookings() uses caller = where(b in Bookings.bookings: b.holder == caller);
view room_day(room: id, day: int(1, 366)) = where(b in Bookings.bookings: b.room == room && b.day == day);

route POST "/rooms" => add_room(room, name, seats);
route POST "/rooms/{room}/close" => close_room(room);
route POST "/bookings" => book(booking, room, day, slot, people);
route POST "/bookings/{booking}/cancel" => cancel(booking);
route GET "/me/bookings" => my_bookings();
route GET "/rooms/{room}/days/{day}" => room_day(room, day);

scenario a_day_in_alpha {
    add_room(1, "Alpha", 4) => Added; add_room(1, "Again", 2) => Exists; add_room(2, "Beta", 10) => Added;
    as 100 book(1, 1, 10, 2, 3) => Booked;
    as 101 book(2, 1, 10, 2, 1) => Taken;
    as 101 book(2, 1, 10, 3, 5) => TooSmall;
    as 101 book(2, 9, 10, 3, 1) => Unknown;
    as 101 cancel(1) => NotHolder;
    as 100 my_bookings() => Value([Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }]);
    room_day(1, 10) => Value([Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }]);
    close_room(1) => Closed;
    as 100 my_bookings() => Value([]);
    as 102 book(3, 1, 11, 1, 1) => RoomClosed;
    as 100 cancel(1) => Unknown;
}
vishy check --interface team-rooms.vish
ok: 2 row(s), 0 event(s), 0 fn(s), 2 unit(s), 6 contract(s), 4 flow(s)

Every contract is a stub: outcomes declared, examples given, no cases. The check refuses a contract with a body, a declared failure without an example, a flow passing the wrong type to a stub, an outcome name used with two shapes, a scenario step that cannot type. The Contract page of the anti-patterns part shows each refusal. The four rules that cost the most when skipped: bound every quantity; every failure a rule implies is an outcome of some contract, with an example that produces it; a rule that couples two units is kept by the flow that commands them, in the order that keeps it, with a comment saying so (close_room clears the bookings before it closes the room); a fact one unit needs from another is carried by the flow as a value (book asks Rooms.fits first), never read across.

3. The parts

vishy write team-rooms.vish --out parts

One request per unit, all at once; each part is checked as it lands; the contracts a check names are retried; then assembly and verification. The team handbook’s run on this example: both units answered in 2.9 seconds, 6.2 seconds in all with verification; a second run kept every part that still passed and wrote nothing, in 0.6 seconds. After an interface change, only the contracts the change reaches are rewritten.

A unit the stage cannot finish goes to a person or a stronger agent, who asks for the brief and writes the part by hand:

vishy brief team-rooms.vish Rooms
vishy part team-rooms.vish Rooms parts/rooms.vish

The brief prints the writer’s reference, the shared declarations with their comments, the world rules that name the unit, and the unit’s block as the owner wrote it; nothing about the other units. The part check is the same local check the stage runs. The guide’s check runs it on this example’s rooms part against the interface:

verify: 21 examples, 0 scenario steps and 1500 sampled checks passed

It leaves out flows, scenarios, world rules and views, which belong to the whole program.

4. Assembly and verification

The interface and the two parts, assembled into one program:

row Room { id: id, name: string(64), seats: int(1, 200), closed: bool }
row Booking { id: id, room: id, holder: id, day: int(1, 366), slot: int(1, 8), people: int(1, 200) }

// rule 1: a booking's room exists, is open, and seats at least the booking's people.
invariant all(b in Bookings.bookings: any(r in Rooms.rooms: r.id == b.room && !r.closed && b.people <= r.seats));

unit Rooms {
    caps storage;
    state rooms: Room[32];
    fixture Two = { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: false }, Room { id: 2, name: "Beta", seats: 10, closed: true }] };
    contract add(room: id, name: string(64), seats: int(1, 200)) {
        // Exists when the room id is present; else insert Room { id: room, name, seats, closed: false }.
        outcomes Added, fail Exists;
        examples {
            {} (1, "Alpha", 4) => Added { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: false }] };
            Two (1, "Again", 3) => Exists;
        }
    }
    contract fits(room: id, people: int(1, 200)) {
        // Fits when the room exists, is open and has at least `people` seats; RoomClosed when it is closed; TooSmall when open with fewer seats; an absent room is Unknown. Changes nothing.
        outcomes Fits, fail RoomClosed, fail TooSmall;
        examples {
            Two (1, 4) => Fits;
            Two (1, 5) => TooSmall;
            Two (2, 1) => RoomClosed;
            Two (9, 1) => Unknown;
        }
    }
    contract close(room: id) {
        // Closed: the room's closed becomes true (already closed is fine); an absent room is Unknown.
        outcomes Closed;
        examples {
            Two (1) => Closed { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: true }, Room { id: 2, name: "Beta", seats: 10, closed: true }] };
            Two (2) => Closed;
            Two (9) => Unknown;
        }
    }
}

unit Bookings {
    caps storage;
    state bookings: Booking[64];
    fixture One = { bookings: [Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }] };
    // rule 2: one booking per room, day and slot.
    invariant all(a in bookings: all(b in bookings: a.id == b.id || a.room != b.room || a.day != b.day || a.slot != b.slot));
    contract book(booking: id, room: id, holder: id, day: int(1, 366), slot: int(1, 8), people: int(1, 200)) {
        // Exists when the booking id is present; Taken when the room already has a booking on that day and slot; else insert Booking { id: booking, room, holder, day, slot, people }.
        outcomes Booked, fail Taken, fail Exists;
        examples {
            {} (1, 1, 100, 10, 2, 3) => Booked { bookings: [Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }] };
            One (1, 2, 100, 11, 1, 1) => Exists;
            One (2, 1, 101, 10, 2, 1) => Taken;
            One (2, 1, 101, 10, 3, 1) => Booked { bookings: [Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }, Booking { id: 2, room: 1, holder: 101, day: 10, slot: 3, people: 1 }] };
        }
    }
    contract cancel(booking: id, holder: id) {
        // Cancelled: removes the booking when its holder is `holder`; NotHolder when it is someone else's; an absent booking is Unknown.
        outcomes Cancelled, fail NotHolder;
        examples {
            One (1, 100) => Cancelled { bookings: [] };
            One (1, 101) => NotHolder;
            One (9, 100) => Unknown;
        }
    }
    contract clear_room(room: id) {
        // Cleared: removes every booking of the room (none is fine).
        outcomes Cleared;
        examples {
            One (1) => Cleared { bookings: [] };
            One (2) => Cleared;
        }
    }
}

flow add_room(room: id, name: string(64), seats: int(1, 200)) atomic { Rooms.add(room, name, seats); }
// rule 1 is kept: the bookings go before the room closes.
flow close_room(room: id) atomic { Bookings.clear_room(room); Rooms.close(room); }
// rule 1 is kept: the room decides whether the booking fits before it is made.
flow book(booking: id, room: id, day: int(1, 366), slot: int(1, 8), people: int(1, 200)) uses caller atomic { Rooms.fits(room, people); Bookings.book(booking, room, caller, day, slot, people); }
flow cancel(booking: id) uses caller atomic { Bookings.cancel(booking, caller); }

view my_bookings() uses caller = where(b in Bookings.bookings: b.holder == caller);
view room_day(room: id, day: int(1, 366)) = where(b in Bookings.bookings: b.room == room && b.day == day);

route POST "/rooms" => add_room(room, name, seats);
route POST "/rooms/{room}/close" => close_room(room);
route POST "/bookings" => book(booking, room, day, slot, people);
route POST "/bookings/{booking}/cancel" => cancel(booking);
route GET "/me/bookings" => my_bookings();
route GET "/rooms/{room}/days/{day}" => room_day(room, day);

scenario a_day_in_alpha {
    add_room(1, "Alpha", 4) => Added; add_room(1, "Again", 2) => Exists; add_room(2, "Beta", 10) => Added;
    as 100 book(1, 1, 10, 2, 3) => Booked;
    as 101 book(2, 1, 10, 2, 1) => Taken;
    as 101 book(2, 1, 10, 3, 5) => TooSmall;
    as 101 book(2, 9, 10, 3, 1) => Unknown;
    as 101 cancel(1) => NotHolder;
    as 100 my_bookings() => Value([Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }]);
    room_day(1, 10) => Value([Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }]);
    close_room(1) => Closed;
    as 100 my_bookings() => Value([]);
    as 102 book(3, 1, 11, 1, 1) => RoomClosed;
    as 100 cancel(1) => Unknown;
}

unit Rooms {
contract add(room: id, name: string(64), seats: int(1, 200)) {
    case room in rooms => fail Exists;
    else => Added: rooms += [Room { id: room, name: name, seats: seats, closed: false }];
    examples {
        {} (1, "Alpha", 4) => Added { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: false }] };
        Two (1, "Again", 3) => Exists;
        Two (3, "Gamma", 6) => Added { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: false }, Room { id: 2, name: "Beta", seats: 10, closed: true }, Room { id: 3, name: "Gamma", seats: 6, closed: false }] };
    }
}
contract fits(room: id, people: int(1, 200)) {
    case !(room in rooms) => fail Unknown;
    case rooms[room].closed => fail RoomClosed;
    case people > rooms[room].seats => fail TooSmall;
    else => Fits;
    examples {
        Two (1, 4) => Fits;
        Two (1, 5) => TooSmall;
        Two (2, 1) => RoomClosed;
        Two (9, 1) => Unknown;
        {} (1, 1) => Unknown;
    }
}
contract close(room: id) {
    case !(room in rooms) => fail Unknown;
    else => Closed: rooms[room].closed := true;
    examples {
        Two (1) => Closed { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: true }, Room { id: 2, name: "Beta", seats: 10, closed: true }] };
        Two (2) => Closed;
        Two (9) => Unknown;
        {} (1) => Unknown;
    }
}
}

unit Bookings {
contract book(booking: id, room: id, holder: id, day: int(1, 366), slot: int(1, 8), people: int(1, 200)) {
    case any(b in bookings: b.id == booking) => fail Exists;
    case any(b in bookings: b.room == room && b.day == day && b.slot == slot) => fail Taken;
    else => Booked: bookings += [Booking { id: booking, room: room, holder: holder, day: day, slot: slot, people: people }];
    examples {
        {} (1, 1, 100, 10, 2, 3) => Booked { bookings: [Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }] };
        One (1, 2, 100, 11, 1, 1) => Exists;
        One (2, 1, 101, 10, 2, 1) => Taken;
        One (2, 1, 101, 10, 3, 1) => Booked { bookings: [Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }, Booking { id: 2, room: 1, holder: 101, day: 10, slot: 3, people: 1 }] };
    }
}
contract cancel(booking: id, holder: id) {
    case any(b in bookings: b.id == booking && b.holder == holder) => Cancelled: bookings -= [booking];
    case any(b in bookings: b.id == booking) => fail NotHolder;
    else => fail Unknown;
    examples {
        One (1, 100) => Cancelled { bookings: [] };
        One (1, 101) => NotHolder;
        One (9, 100) => Unknown;
    }
}
contract clear_room(room: id) {
    else => Cleared: bookings -= where(b in bookings: b.room == room).id;
    examples {
        One (1) => Cleared { bookings: [] };
        One (2) => Cleared;
    }
}
}

vishy run team-rooms.vish
vishy verify team-rooms.vish 7 1000
scenario a_day_in_alpha
  add_room(1, "Alpha", 4) => Added
  add_room(1, "Again", 2) => Exists
  add_room(2, "Beta", 10) => Added
  book(1, 1, 10, 2, 3) => Booked
  book(2, 1, 10, 2, 1) => Taken
  book(2, 1, 10, 3, 5) => TooSmall
  book(2, 9, 10, 3, 1) => Unknown
  cancel(1) => NotHolder
  my_bookings() => Value([Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }])
  room_day(1, 10) => Value([Booking { id: 1, room: 1, holder: 100, day: 10, slot: 2, people: 3 }])
  close_room(1) => Closed
  my_bookings() => Value([])
  book(3, 1, 11, 1, 1) => RoomClosed
  cancel(1) => Unknown
  Rooms = Rooms { rooms: [Room { id: 1, name: "Alpha", seats: 4, closed: true }, Room { id: 2, name: "Beta", seats: 10, closed: false }] }
  Bookings = Bookings { bookings: [] }
14 step(s), all as expected
verify: 39 examples, 14 scenario steps and 12000 sampled checks passed

The scenario is the brief’s day in room Alpha, every failure reached through a flow. Run it on three seeds and at three thousand iterations before serving anything; the guide’s check runs one seed at one thousand, which is the line above. A failure names the flow or the contract, the world and the arguments.

5. The service and the clients

vishy service team-rooms.vish out/service sqlite
vishy client  team-rooms.vish out/client

The first writes a Rust crate that runs each flow as a transaction over a store; the second writes a TypeScript client, an OpenAPI description and a Rust client. The guide’s check builds the service and calls its first route with an empty body; the door answers with the flow and the parameter it is missing:

POST /rooms
{"error":"missing `room`","flow":"add_room"}

That answer is the frontend agent’s contract: every parameter, every bound, every outcome, before a body exists.

When a step fails

what you seewho actswhat to do
the interface check refusesinterface ownerfix the interface; the message names the declaration
a part fails its local check and the failure names a contractthat unit’s ownerrewrite that contract; the part check says whether it passes
a part fails and no contract is namedthat unit’s ownerthe part check prints every refused contract checked alone and every declared contract the part lacks
the verifier fails on a scenario stepinterface owner firsta scenario states the product’s behaviour; decide whether the step or a contract is wrong, then route to the unit owner
the verifier fails on a world rule during a walkinterface ownerthe trace shows the flow; either a flow calls units in an order that breaks the rule, or a contract lacks a guard the rule needs: add the outcome and the example, then the unit owner rewrites
the verifier reports an unreached outcomeinterface owneradd an example that produces it, or remove the outcome
the service refuses a request with 400the callerthe message names the flow, the parameter and the bound
the service answers 500the unit owner of the flow named, or the runtimea panic inside the flow, in an effect’s Rust or the runtime; the message is in the response and in the log, and nothing was written
the service answers 409nobodya failed outcome is the program working; the client switches on the outcome

Changing a running application

An interface change is a version of the program. The owner edits the interface, runs the interface check, then the writer stage again: parts that still pass are kept, the contracts the change reaches are rewritten. When a row changes, the program gets a version number and, for a rename or a recomputed field, a migration with an example; the chapter on versions and migrations says what the compiler derives and what it refuses. The interface diff says whether clients written against the old interface still fit.

What is not here, by decision: nothing generates a user interface. A screen is the application’s work, built on the generated clients, in whatever the team likes.

The team that built this

This guide was written by a team of agents, one chat per seat, each with a charter: the files it owns and nobody else writes, its inputs, a local check it runs before it reports, and a report in a fixed shape that someone else reproduces by running the same commands. A seat that needed a change in another seat’s file proposed the text and waited. The checks were the arbiter, and a verdict named the seat.

That is the language’s discipline done by hand: one owner per piece of state, a fixed contract between owners, a local verdict before the shared step, and a check that decides instead of a reader. It worked for the agents for the same reason it works in the language. The parallelism was never the hard part; the ownership was.

Authoring an interface

The guide for the interface owner, included from docs/authoring.md, which is the source; only its third and fifth lines are carried by hand, so that their links work in the book.

Authoring: from a requirement to a checked interface

For a frontier model acting as the interface owner of one application. You receive a business requirement in whatever form it comes and produce three things: a brief, a numbered list of assumptions, and an interface that vishy check --interface accepts. Writers, human or vishy write, take it from there (Building with a team, steps 3 to 5). You do not write contract bodies and you do not design screens.

The guide is in two steps. Step one turns any input into the brief, five lists. Step two turns the brief into the interface. Between them sits the one review checkpoint, used in one of the two modes below. Every rule from docs/agents.md, “Rules for interface authors”, is placed at the step where it applies, with the run that taught it. Every command quoted here was run on 27 Sept 2026 on the handbook’s example, docs/examples/handbook/, and on small refused cases whose messages are quoted verbatim; vishy means target/debug/vishy in the repository (it is not on PATH).

What you receive

Any mix of these:

  • Prose. A page or an email: what the product is, what it must do, what it must never do.
  • A PRD with user stories. “As a member, I want to cancel my booking, so that the slot frees up”, with acceptance criteria.
  • A conversation transcript. People deciding things, changing their minds, leaving points open.
  • An ontology module, optionally: entities with attributes, relations between them, shapes (which attributes are required, their types and ranges).

None of these is the brief. The brief is what you write in step one, and it is the only thing step two reads.

The rule that lets you finish: never block

The input will not answer every question. You decide, and you record. A decision is recorded when the verifier will enforce it, because then a wrong guess becomes a failing check for someone who did not make the guess. Three kinds are recorded, as a numbered assumption with the default you took:

  1. A number that becomes a bound or a cap. “A room has seats” gives no maximum; seats: int(1, 200) needs one. The input’s own numbers (“slots 1 to 8”) are not assumptions.
  2. A rule with two readings that give different scenarios. “Only the holder can cancel”: is there an administrator who can cancel for others? Write both scenarios, keep one, record which and why.
  3. A failure the rules require that nothing names. Rule 1 makes a booking of five people in a four-seat room fail; the input never says what that failure is called. Name it (TooSmall), mark it invented. The name is part of the API (a client switches on it), so it is recorded.

Everything else is decided silently: the split into units, field names, example values, fixture names, table sizes used for verification, and every mistake the checker catches, because the checker’s refusal is the record. A silent decision still gets a comment where the reader needs one (a flow’s order of calls that keeps a rule), but no number.

An assumption is written once, in ASSUMPTIONS.md, and cited in the interface as a comment on the declaration it produced:

// assumption 1: seats is int(1, 200); the requirement gives no maximum.
row Room { id: id, name: string(64), seats: int(1, 200), closed: bool }

The comment goes on its own line, before the declaration’s other comments, in the form // assumption N: …, so that one command lists every assumption the interface carries:

grep -n "assumption" interface.vish

Comments in that form parse in every position that takes one: before a row, before a world invariant, inside a unit before a state or a contract, before a flow (run on a copy of the handbook’s interface with five such comments; check --interface still answers ok).

When the input contradicts itself, the later and the more specific statement wins, and the choice is an assumption of kind 2. When the input asks for something the language cannot say, do not invent a construct: write the nearest thing the language has, record the gap in your report, and keep going.

The two modes

  • Alone. You run steps one and two without stopping. The assumption list is the audit trail: a reviewer who reads it afterwards sees every decision the input did not make, in the order you made them, each with the declaration it changed.
  • With a human reviewing the brief. You stop after step one, hand over BRIEF.md and ASSUMPTIONS.md, and the assumption list is the review agenda: the reviewer answers by number. Then you run step two. A reviewed assumption stays in the list with the answer added (decided by review: an administrator may cancel; see flow admin_cancel); an unanswered one keeps its default. The interface may be reviewed a second time before writers start; agents.md asks for it (“have the interface adjudicated before writers start”), and the checker’s ok is the entry condition for that review, not a replacement for it.

The files are the same in both modes. The mode changes only whether you wait at the checkpoint marked below.

Step one: the brief

The brief is five lists, in BRIEF.md; the third list, what must always hold, is the rules, and they are numbered because the interface will cite the numbers. Every item cites the input it came from (a paragraph, a story, a line of the transcript, an entity) so the reviewer can check it.

  1. What exists. The things the product keeps, each with its attributes and, for every quantity, its range. This list becomes the rows.
  2. What may be done to it, and with which outcomes. Every action, its inputs, its success, and every way it can fail. This list becomes the contracts and the flows.
  3. What must always hold. Every rule that is true of the whole state at every moment, whichever action ran. This list becomes the bounds, the machines and the invariants.
  4. What is read. Every screen, report or query, with what it shows and to whom. This list becomes the views.
  5. Who may do what. Every action’s caller and the condition on the caller. This list becomes the uses caller flows and the visibility rules inside views.

The handbook’s six-line brief, docs/examples/handbook/BRIEF.md, in this form:

listitems
existsa room: name, seats, open or closed. A booking: one room, one day (1 to 366), one slot (1 to 8), a number of people, a holder.
may be doneadd a room; close a room (cancels its bookings); book; cancel a booking.
must holdrule 1: a booking’s room exists, is open, and seats at least the booking’s people. Rule 2: one booking per room, day and slot.
is readmy bookings (the caller’s); a room’s bookings for a day.
whoanyone adds, closes and books; only the holder cancels.

Two assumptions this brief leaves to the author: the maximum number of seats (kind 1; the interface takes 200) and the names of the failures rule 1 and rule 2 require (kind 3; TooSmall, RoomClosed, Taken, all invented). Everything else the six lines decide.

Reading each kind of input

  • Prose. Every noun that the product keeps goes to list 1; every verb to list 2; every “always”, “never”, “at most”, “only” to list 3 or 5; every “shows”, “sees”, “lists” to list 4. A sentence that mentions a number puts the number in list 1 (a range) or list 3 (a cap).
  • User stories. The “I want” is an item of list 2; the “as a” is its caller in list 5; the “so that” often names a rule for list 3 or a read for list 4. Each acceptance criterion is one scenario step: a call and the outcome it must produce. Keep the story’s id on the item; the scenario will carry it.
  • A transcript. Read it to the end before writing anything. For each point, the last decision stands; a point raised and never settled is an assumption of kind 2; a number someone proposed and nobody rejected is a number the input gave, not an assumption.
  • An ontology module. Each entity is an item of list 1 with its attributes. A relation becomes an id field on the row that owns the relation (the booking holds room: id, not the room a list of bookings) and a rule in list 3 that the referenced thing exists (the handbook’s rule 1 is that rule with two conditions added). A shape’s required attributes become non-optional fields; its optional ones become option<T>; its ranges become bounds; a cardinality (“at most 4 labels per issue”) becomes a cap in list 3.

What the brief never contains: how anything is computed, which unit holds what, or the name of a counter, an index or a helper. Those are step two’s and the writer’s.

Review checkpoint (mode two stops here)

Hand over BRIEF.md and ASSUMPTIONS.md. Resume when the answers arrive, or at once in mode one.

Step two: the interface

The interface is one file, interface.vish, written against the checker until it answers ok. The order below is the order that produces the fewest rewrites; each part places the rules from agents.md that apply to it, with the run that taught the rule in brackets.

Declare version 1; at the top before anything else. A program without a version is served at version 0 and runs, but the first change to a stored row or a new stored unit is then refused at reload (“the new program declares no version N;”) until the version exists; declared on day one, that first change is a version bump and a derived migration [the privacy ladder, 30 Sept 2026].

2a. Units that own their data

Split list 1 into units so that every row has one owner and no unit needs to read another’s state to decide anything. The handbook’s example has Rooms and Bookings; the tracker (apps/tracker/interface.vish) has nine units for ten rows.

  • A fact that two units need is carried by the flow as a value or a bounded list, into the unit that owns the data: let skus = Wishlists.wished(customer); Stock.count_available(skus, location);. A contract never reads another unit, a flow never computes, and a world-derived column takes no parameter. [Experiment ten: the author and the supervisor both missed this and changed the brief instead.]
  • A verdict is not a value. A boolean carried from one unit into another, or a bool parameter that stands for another unit’s answer, hides a cross-unit read. The unit that knows decides and fails; the flow calls it first; the next contract takes no flag. Comparing a warehouse id with an order’s location is the same mistake: carry the warehouse’s location in. [Experiment ten.]
  • Never name an internal counter, index or helper in the interface (next_line, a sku index): those are the writer’s to choose. [The authoring gate flagged two units of experiment nine’s accepted interface for this.]

vishy graph interface.vish prints the components and, per flow, the units it touches. One component is normal for a small application; the note it prints when one component spans the whole world is worth reading once.

2b. Rows, with bounds

Write every row from list 1. Give every quantity a bound, and mark the storage units with caps storage.

  • Bound every quantity by default. qty: int(1, 999), not int. A bound makes a bad value unwritable in examples, unsampleable by the verifier and unarguable at a call; a plain int leaves the guard to every writer and the discovery to luck. [Experiment nine: the one bug found late was a negative quantity reaching a stock level through an unbounded field.] The checker accepts a plain int (run: an interface with seats: int answers ok), so the bound is yours to remember; each bound the input did not give is an assumption of kind 1.
  • A derived amount’s domain lives in the row’s type (refunded: int(0, …)): the sampler draws a world-derived column freely inside a unit check and respects only what the type says, and a unit invariant cannot name a world-derived field. [Found by a 3,000-iteration sample on a program that had passed at 1,000.]
  • A table’s size (Room[32]) is a verification bound, chosen silently. A rule about capacity in list 3 says cap(rows) and does not repeat the number; then the number is a product cap and an assumption of kind 1 if the input did not give it.
  • A row whose field moves through fixed states (open, in_progress, blocked, done) gets an enum and a machine declaring the legal moves; a move the machine forbids is WrongStatus without a guard.

2c. Stubs, one per action, every failure exampled

Write every unit’s contracts as stubs: the header with bounded parameters, the rule as a one-sentence comment, outcomes …;, and examples { … }. No cases: the checker refuses a body in an interface.

b_body.vish:4:5: error: `Rooms.add` has cases; an interface declares outcomes, not bodies (`outcomes Ok, fail Full;`)
  • Every rule an invariant states needs an outcome in some contract’s signature. If rule 3 says amounts are positive, some contract must be able to answer BadAmount. [Experiment nine: three signatures lacked one; every writer invented a name for it until the outcome gate refused them.] Go through list 3 rule by rule and name the contract and outcome that keeps each one; a rule with no outcome is either kept by a bound (then say so in the rule’s comment) or is a gap.

  • Every declared failure outcome gets an acceptance example. A writer who omits the guard then fails locally and deterministically, instead of when a deep sample happens to reach it. [Experiment nine.] The checker enforces it:

    a_noexample.vish:5:30: error: `Rooms.add` declares `fail TooBig` without an acceptance example that produces it; write the example (it is the rule for when the failure occurs)
    
  • Never leave a rule only in prose. “Amount checks are another unit’s job” in a signature’s comment became an invented guard in two experiments. Say it as an outcome, a bound or an example.

  • A formula for a carried value is stated through examples, not prose. Prose formulas read as implementation and the authoring gate flags them. Three examples that pin the formula beat a sentence.

  • A carried value gets its property in the stub: ensures result == count(…); or the bounds that must hold. Without it the value is checked only on the exampled states; with it, on every sampled one, whatever the writer wrote (docs/blind-spots.md). A stub with a contract-level ensures passes check --interface (run).

  • One outcome name has one shape across the whole program, and the brief must obey it too. [Experiment ten’s brief named a new flow’s success Backordered while an earlier account used it as a failure, and asked one flow to fail Held while Held was another’s success; the brief was amended.] The checker refuses the clash:

    c_twoshapes.vish:9:27: error: outcome `Added` is declared differently elsewhere (fail / carried value / type must agree across the program)
    
  • Two examples with the same starting state and arguments must agree. If they expect different outcomes, the contract is deciding on a fact it does not hold. The interface check does not catch this (run: two such examples answer ok); the verifier catches it once a part exists, so catch it yourself first.

  • A failure the brief lists may be impossible under a rule (a balanced ledger is never Unbalanced; a held order is never paid, so never fulfilled). Then the flow says // Outcome: unreachable under rule:<name> and no contract declares it, instead of an example from a state that cannot exist.

  • The implicit outcomes are never declared and never guarded: an absent key is Unknown, an inserted existing key is Exists, a table past its bound is Full, an illegal machine move is WrongStatus. They may appear in examples (Two (9, 1) => Unknown;) and scenarios, and an example that shows one costs nothing.

  • Every example’s arguments lie inside the bounds; the checker refuses one outside:

    g_unreachable.vish:6:85: error: example argument for `seats` is outside its type
    

    A failure example has no after-state; a success example whose contract changes state shows what changed.

Fixtures (fixture Two = { rooms: [ … ] };) make the examples short; name a fixture for each state the examples keep returning to.

2d. Flows, carrying values between units

Write one flow per item of list 2, calling the units in the order that keeps the rules of list 3. Give the flow uses caller when list 5 puts a condition on the caller, and pass caller into the contract that decides.

  • A rule that couples several units is kept by the flow that commands them, in the order that keeps it, with a comment saying so. close_room clears the bookings before it closes the room; book asks Rooms.fits before Bookings.book. Every flow that commands one of the coupled units and never calls the others says why the rule cannot break: // rule: unchanged because …. The verifier finds the world where it breaks otherwise. The tracker’s interface carries such lines above most of its flows.
  • A flow never computes. It binds a carried value, let p = Issues.project_of(issue);, and passes it on. A condition that needs arithmetic belongs in the contract of the unit that owns the numbers.
  • A flow that only reads is not a flow; see 2e.
  • If the input arrives in installments and a later one changes a flow’s signature or precedence, or adds a rule, restate every flow and scenario that exercises it in that installment’s section, so that nothing kept from an earlier installment calls a flow that no longer exists in that form. [Experiment ten: twenty-five of thirty-one assembly failures were one account’s new rule that no kept flow called.]

A flow’s parameter types should equal the contract’s. The interface check refuses a different type:

j2_mismatch.vish:9:68: error: argument `seats` of `Rooms.add` expects int(1,200), got string(8)

It accepts a wider bound of the same type (run: int(1, 300) in the flow over int(1, 200) in the contract answers ok), so the bound is yours to keep equal; the verifier would find it later as an argument outside the contract’s type.

2e. Views, for everything that is read

Every item of list 4 is a view, with a GET route, and the rule of who sees what inside the expression:

view my_bookings() uses caller = where(b in Bookings.bookings: b.holder == caller);
route GET "/me/bookings" => my_bookings();
  • A read a screen needs is a view, not a flow. A flow that only reads still costs a transaction and a contract; a view costs neither, and the rule of who sees what is checked where it is written. A scenario step as 100 my_bookings() => Value([…]) asserts on it. (The tracker predates views and states its reads as query flows; do not copy that.)

2f. Scenarios, from the stories

Every user story, acceptance criterion and rule-with-two-readings becomes a scenario or a step of one: the call, as N for the caller, at N for the clock, and the outcome. A scenario is the product’s behaviour stated once; the verifier stops at the first wrong step and shows the world. The checker types every step against the flows:

d_badstep.vish:10:26: error: scenario `s` step 1 argument `seats` expects int(1,200), got string

Write at least one step that produces each declared failure through a flow, not only through the contract’s example, so the flow’s order of calls is exercised too. The handbook’s a_day_in_alpha reaches Exists, Taken, TooSmall, Unknown, NotHolder and RoomClosed in fourteen steps.

2g. Later changes

A row change on a deployed program is a version bump and, for a rename or a recomputed field, a migrate Row from N { field = expr; } with an example; the compiler derives added fields with defaults and removed fields and refuses what it cannot derive, naming the declaration to write (reference §17). A change to the interface after writers have started goes through the same check and then the handbook’s step 3 again; parts that still pass are kept.

Step two in parallel: the interface as a directory, authors as roles

Measured on 29 Sept 2026 on one application with two products (the fifteenth and sixteenth compiler reports): one author wrote a 111-contract interface serially in 42 minutes; five authors then wrote the second product’s 66 contracts at once in 18 minutes, with no name clash, and the whole program was verified 49 minutes after they started. What carried the parallel work was not the authors talking to each other; they never did. It was three things each author received before starting, and the checker enforcing the rest. Keep it that way as features grow.

The interface is a directory, and the memory. One program, many files, checked as one (vishy check --interface dir/ or the files listed). Each file has one owner role, never a person:

  • shared.vish: the enums and carried rows that cross components. The director owns it. An author who needs a new shared row asks; the director adds it, serially.
  • one file per component: its rows, units, stubs, fixtures, machines and examples; beside it <component>.assumptions.md, numbered, with the default taken for every number the input did not give and every open point. That file is the replacement author’s memory: a fresh agent given the component file, its assumptions file and the check command continues where the last one stopped.
  • flows.vish, views.vish, scenarios/: the director’s. Flows are the only place two components meet, so the joint check lives here.
  • ASSUMPTIONS.md at the top: the run’s decisions, the findings, and the reviewer’s answers.

A new feature is a new component file plus additions to the shared file and the flows. The parts already written stay; the write stage writes only new or changed units; a changed row is a version bump with a migrate block (reference §17). Split a single-file interface into this shape before the next feature, not after.

The shared step, written before any author starts. One page:

  1. the components, each with its scope in the input (sections, actions, rules, stories) and the contracts the director expects to call;
  2. the carried rows and shared enums, in shared.vish, already checking against the existing interface;
  3. the taken outcome names with their shapes, generated (vishy interface prints the public surface), never maintained by hand; a taken name may be reused only with the same shape and meaning; a new success that carries a value gets a name prefixed by the component’s word;
  4. an id block per component for examples, disjoint from the existing fixtures;
  5. the flow’s limits, stated as signature rules, because every author assumes the opposite: a flow cannot loop, so a contract fed by the flow for many things takes a bounded list; a flow cannot build a row, so a contract fed by the door takes scalars; a flow can pass one field of a row it carries (let f = Campaigns.facts(c); Offers.terms_of(f.offer), one level, no arithmetic), so a contract that needs one field of another unit’s answer takes that field, not the whole row [the run’s F6 said the opposite; wrong since 30 Sept 2026, tests/flow_field_ok.vish]; a flow still cannot branch, but a contract can end it early as a success with a stop outcome (a repeated confirmation answers AlreadyRecorded and the payout step does not run), and a step marked keep records before a later refusal (an attempt is noted, then the sale refuses) [item 11 c, 30 Sept 2026]; a flow still cannot loop, but a contract of a unit with caps ids that says uses ids; can mint one id per element of a list with fresh() in a fold, so a flow that creates several rows no longer takes their ids from the door [item 11 d];
  6. the boundaries (what is outside, and that its events arrive as flows the door calls), and the check command with a time box.

Without item 5 the run needed a second round: five sets of signature changes sent to the authors after the flows were written. With it, that round does not exist.

The joint round. The director writes the flows, views, routes and scenarios against the components, runs the joint check, and sends every change a component needs to its author as a message, never editing the author’s file (the file’s history must say who wrote what, and a director who edits mid-flight hides whether the author could have done it). Scenarios are written now, before the write stage: in the run, a scenario found a rule three flows had missed that no unit check could see.

What to avoid. Author-to-author collaboration: two agents editing one file, or negotiating a row between them, is what the shared step exists to prevent; the one time it is needed it is the director’s job. A single monolithic interface file once there are two products. Hand-maintained lists of taken names.

The director role. The shared step, the flows, the scenarios, the joint check, the calls on assumptions, and the messages to authors. It is serial, and it is where judgement sits. It is also replaceable: its state is the files and the log, nothing else.

Two more rules from the compiler in Vishy (30 Sept 2026), where two authors wrote the parser’s remaining declarations in twelve minutes: give each author its own scratch directory, because two authors sharing one overwrote each other’s build mid-run and lost minutes to a phantom discrepancy; and when the authors’ oracle is a check script, run it once yourself before the launch, because both authors lost their first minutes to the same harness bug (a script handing three files to a one-file tool).

Running the checker

From the repository root:

vishy check --interface docs/examples/handbook/interface.vish
ok: 2 row(s), 0 event(s), 0 fn(s), 2 unit(s), 6 contract(s), 4 flow(s)

On a refusal the message names the file, line, column and declaration; fix that declaration and run again. Then:

vishy graph docs/examples/handbook/interface.vish
2 unit(s), 4 flow(s), 1 world invariant(s), 0 world-derived field(s): 1 component(s)
component 1: units Rooms, Bookings | flows add_room, close_room, book, cancel | 1 invariant(s)
flow add_room: touches Rooms | re-checks 1 of 1 invariant(s), recomputes 0 of 0 derived | walks 4 of 4 flow(s)
flow close_room: touches Rooms, Bookings | re-checks 1 of 1 invariant(s), recomputes 0 of 0 derived | walks 4 of 4 flow(s)
flow book: touches Rooms, Bookings | re-checks 1 of 1 invariant(s), recomputes 0 of 0 derived | walks 4 of 4 flow(s)
flow cancel: touches Bookings | re-checks 1 of 1 invariant(s), recomputes 0 of 0 derived | walks 4 of 4 flow(s)
note: one component spans the whole world; every flow's walk draws from every flow. Look for a unit that every flow touches.

What the checker catches at interface time, each run on a small case on 27 Sept 2026: a contract with a body; a declared failure without an example; one outcome name with two shapes; a scenario step that does not type; an example argument outside its bound; a flow passing a value of the wrong type to a contract. What it does not catch, also run: a plain int where a bound belongs; two examples that disagree on the same state; a flow bound wider than the contract’s; a rule left only in prose. Those four are the list to walk before you call the interface done.

Before handing over, walk the brief once more against the interface: every rule of list 3 is an invariant, a bound or a machine, and has the outcome that keeps it; every failure in list 2 is declared and exampled or marked unreachable; every read of list 4 is a view; every caller condition of list 5 is inside a uses caller flow or a view; every assumption in ASSUMPTIONS.md has its comment in the interface.

What you hand over

Five files, in one directory:

filecontents
REQUIREMENT.mdthe input as it arrived, unedited
BRIEF.mdthe five lists and the numbered rules, each item citing its source
ASSUMPTIONS.mdthe numbered assumptions, each with its kind, the default taken, and after review the answer
interface.vishthe interface, with // assumption N: on each declaration an assumption produced
CHECK.txtthe transcript of vishy check --interface and vishy graph on the final interface

The worked example

This slot is open. The example will be a conventional requirement the author is supplying (a dialer), carried from its prose through the brief and the assumptions to vishy check --interface passing, in docs/examples/authoring/, in the five files above. Until it lands, the handbook’s example is the small case every command above was run on.

Origins

Carried from docs/origins.md on 30 Sept 2026; the file is the source.

Origins

Where Vishy came from, and where each step is written down. Everything below is in this repository; the dates are the days the work happened.

The idea

A thought experiment on 25 Sept 2026: a software factory in which many AI agents write one program at the same time. Every attempt at that stalls in the same places, because in ordinary languages the piece of work an agent gets is not a boundary. Agents reach into each other’s state, duplicate helpers, hold different assumptions, and every disagreement surfaces at integration, serially, in merge conflicts. The conclusion was that the fix is not in the tooling but in the language: give agents a unit of work that is an ownership boundary, make the compiler know every state, every writer of it and every rule, and let a machine decide instead of a reviewer. Rust would be the first target, the way assembly is a C compiler’s target, and nobody would write it by hand.

The documents, in order

whendocumentwhat it holds
25 Sept 2026experiments/history/parallel-first-demo/README.md and experiments/history/parallel-first-report.htmlThe first prototype, “Parallel-first: executable construction language, v0.2”: a source language inside a parallel! { … } macro, checked before bodies exist, bodies searched per unit, Rust emitted; the visual walkthrough of the first run.
25 Sept 2026experiments/history/experiment-1/README.mdExperiment one and its results: agents given only the reference wrote correct programs on the first compile; the missing primitives each run exposed.
25 Sept 2026experiments/history/pf/JEV-BRIEF.mdThe concept stated in full for a language designer: what the language is for, what three experiments showed, the vocabulary to replace, the proposal of a small fixed core with a prelude written in the language. The closest thing to the original specification.
25 Sept 2026experiments/history/pf/LANGUAGE.md, experiments/history/pf/README.mdThe v0.3 language and its Rust front end.
25 to 26 Sept 2026RESULTS.md, LANGUAGE.mdThe results of every experiment through the thirty-unit shop, and the single-file reference the compiler and the writers were developed against.
26 Sept 2026apps/COMPARISON.mdThe same applications built the conventional way and this way, timed.
27 Sept 2026docs/why.md, ROADMAP.mdWhat makes the language different, in two facts; and the roadmap as one list, with the language items in the order the self-hosted compiler needs.

What each step established

  • Experiment one: the agents were never the bottleneck; the language’s missing primitives were, and hard-coding them one by one is accretion, not a language. Hence the small fixed core with a prelude written in the language.
  • Experiment seven: a frozen invariant caught a bug the plain-Rust arm shipped; the first time verification beat review.
  • Experiments eight to ten: fifteen units in twenty seconds with zero edits, then thirty, with cheap writers; the cost moved to authoring the interface, which is where it still is.
  • The tracker and the shop (26 Sept 2026): the conventional arm won on wall clock at CRUD size and lost at thirty units, where integration is serial; a single interface change costs exactly the contracts it reaches.
  • 27 Sept 2026: the writer stage moved into the compiler, the language got views, deliveries, migrations, typed clients, partitioned verification, the sealed computation layer, and its own lexer and parser written in itself, held to the Rust ones on every file in this repository.

The name: the language was pf and then pf5 until 27 Sept 2026, when it became Vishy, after its author, with .vish files.

Limits and measurements

Carried from docs/limits.md on 30 Sept 2026; the file is the source.

Limits, as of 27 September 2026

What the language, the verifier, the generated services and the writer stage do not do, with the numbers behind each statement. Dated because items below are being worked on.

Verification is not proof

verify runs examples, scenarios and bounded random testing: sampled unit states, random walks of contracts, random walks of flows, sampled commutativity pairs. It finds states that break rules; it does not prove none exists. Four classes of bugs are outside it by construction (wrong or missing rules, a body that does nothing, a carried value with no stated property, trusted code and the runtime); two of them now have a check, outcome reachability and contract-level ensures. See docs/blind-spots.md. Measured on the number pool: authored checks in the language killed 9 of 12 planted bugs against 11 of 12 for hand-written Rust tests; the three survivors were rules nobody had written down. Depth matters: a program that passed at 1,000 iterations failed at 3,000 (a world-derived column drawn freely inside a unit check). Run deep_regression.sh before a release.

What is trusted

Anything inside impl { … }: functions with Rust bodies and effects. The verifier checks what the program does with their results, not the Rust.

Scale of evidence

programunitsflowsscenarioslinesdeepest verification
the shop (apps/shop, also experiments/exp8/x10n/shop.vish)3056442,505 interface + 1,103 parts3,000 iterations on seeds 7, 11, 13: 744 examples, 1,081 scenario steps, 456,000 sampled checks per seed
the tracker (apps/tracker)938141,123 interface + 684 parts3,000 iterations on seeds 7, 11, 13 (27 Sept 2026, after change round two): 437 examples, 345 scenario steps, 309,000 sampled checks per seed; served on SQLite with bearer tokens

Verification is partitioned (reference §19): after a flow, only the invariants and derived columns over units the flow can change are re-checked, and a flow’s walk draws from its component’s flows. On the shop and the tracker that is worth little, because every flow of both goes through one unit (Activity), so both are one component, and the invariant checks were never the cost: the shop at 3,000 iterations went from 19.4 s to 17.0 s in the interpreter and from 47 s to 45 s through the compiled verifier with -O; the tracker from 7.7 s to 7.0 s and 32 s to 32 s. The value model (ROADMAP item 2) then took the compiled shop from 45 s to 16 s and the tracker from 32 s to 17 s on the same day, each pair measured back to back: rows read in place, strings compared without copies, ids sampled without collecting, the walk snapshotting only the units a flow can change, single-call flows not staged and the before-state kept only for ensures. Later the same day a contract case stopped copying its unit, measured in a declared quiet window (the other seats idle, load recorded per round, medians of five interleaved rounds): compiled shop 15.5 s to 13.9 s and tracker 13.4 s to 10.6 s, reproduced by the director in its own window within a tenth of a second; the interpreter single-threaded at 1,000 iterations 26.8 s to 27.1 s on the shop and 11.8 s to 12.1 s on the tracker, within 2%, because its copies come from its callers’ snapshots (ROADMAP.md, item 2). With one job per core the interpreter’s times swing by half between rounds under load from outside the seats, so no multi-job size is recorded. A single unit’s local check takes 0.3 to 3 s. Nothing has been written in the language at ten thousand lines.

Performance of emitted code

A multi-call atomic flow stages the units it touches; a contract case copies nothing of its unit unless one case writes the same field twice (then that field) or its ensures uses unchanged() (then the unit); tables are vectors with linear lookup. A service loads only the units a flow or view touches, so the cost is the size of those units, not of the tenant. The number pool runs about 330,000 flows per second in a release build on a world of eight rows. Cost grows with world size, not with the change: tenant-scoped worlds of thousands of rows are fine, millions are not. What remains is in ROADMAP.md, item 2; none of it touches the language.

Generated Rust

Compiler output: a mechanical translation plus the verification harness. Product-only output is about twice the size of the source and roughly four times a hand-written crate, because of the clones and the generic table helpers. Correct, compiling, non-idiomatic.

Density

Measured on the number pool: contracts are 0.53 of the hand-written crate’s logic by Halstead volume; with scenarios and fixtures the whole artefact is 0.3 to 0.4 of it, near the floor, because guards and ranking keys are the decisions themselves. Algorithms in the computation layer are at parity with Rust (the prelude’s heap: 386 tokens against 371).

The language

  • No floats, no bytes; money and time are ints with a scale.
  • In the core, no while, no recursion, no mutation outside deltas and function blocks, no effects except through effect declarations; recursion, while, unbounded strings and lists and self-containing rows exist only behind sealed fn and sealed row (reference §7), pure, checked by examples, run under a step budget, never sampled. By design: every function is total.
  • Rows do not hold maps. Lists inside rows have no stored form.
  • Strings compare by bytes; substr and find count characters.
  • Effects return scalars, strings, options and lists, not rows or maps.
  • No user interface layer; routes expose JSON.

Parts

A part is a second declaration of a unit holding only contract bodies. It cannot add state, fixtures, invariants, derived fields or machines; its examples may name the interface’s fixtures. A part is checked with its interface, never alone. Inside a unit’s local check a world-derived column is drawn freely within its type, so a bound on the column belongs in the row (open_issues: int(0, 99)), not in a unit invariant.

The writer stage

vishy write writes one request per unit against a stub interface, checks each part locally, retries the contracts the check names with two candidates, assembles, verifies, and repairs the contracts the whole-program verdict names. Measured on the tracker, eight units and 56 contracts, DeepSeek V4.1 Flash and V4 Pro on Baseten: 18.4 s from interface to a verified program (all parts written in 9.3 s, one repair round). A change round on the same program (five cross-cutting changes, one new unit, nine new contracts, three row changes): 2 min 9 s of interface authoring, then 12.3 s to a verified program with the existing parts kept or repaired in place, twelve contracts rewritten and nothing else. The thirty-unit shop, 96 contracts: 26.4 s from a cold directory to a verified program (parts 19.8 s, six contracts retried once, no repair); the earlier Python stage on the same design: 23.4 s with the interface amended by hand during the run. Limits: units of six or more contracts are written in two halves, and nothing larger is split further; a reply cut by the token limit costs one more request; scenario failures name a flow, not a contract, so they are reported and not repaired; a failing contract after the retry rounds is reported and the program is not assembled; a checker failure names every refused contract by checking each alone, at one parse and check per contract. The interface author is a person or a strong model; the stage does not design.

Storage and the door

SQLite and Turso: whole-table rewrite on save. Events are handed to effects::handle in-process and, when the program declares deliver, written to a persisted outbox and delivered at least once. Migrations carry a store across a row change one version at a time (a tenant several versions behind runs each version’s migrate in order); a rewrite rebuilds the whole table; only the tables Vishy created, only per-row expressions, no batched or zero-downtime procedures. Deliveries are at least once: webhooks, mail through an HTTP mail endpoint (VISHY_MAIL_HOOK, not SMTP), or a declared effect wrapping any crate; a delivery that fails 20 times stays in the outbox for an operator; there is no ordering guarantee across events. Typed clients and views exist. A view is sampled for panics, not for the right value: only its scenario steps say what it must return. A flow or a view loads whole units, not the rows it needs: a view over Issues loads every issue of the tenant and filters in memory; pushing the filter into the store’s query is the next step. Route arguments are scalars, options, enums and lists of those; a row cannot be passed as an argument.

Namespaces and packages

No registry; dependencies are paths or git tags. One version per package name, conflicts refused, no lockfile. Names may not contain __.

The toolchain

Two verifier paths with the same verdict: the compiled verifier (emit, rustc, run) and the interpreter (vishy verify), which walks the syntax tree with the same sampler and random sequence; tests/verify_differential.sh holds them to the same first failure. The interpreter is the faster path for per-part checks; for whole programs at 3,000 iterations with one job per core it was faster on the tracker and slower on the shop in the one clean round measured (tracker 8.0 s against 10.6 s compiled, shop 17.9 s against 13.9 s), a size not yet held to the quiet-window rule. The playground’s checker and the writer stage use the interpreter only.

The numbers, measured when the book is checked

The four numbers below are not typed into this page. A script under book/numbers/ prints each one from the repository, and tests/check_book.sh runs the scripts again and fails when this page shows a number the repository no longer has.

The compiler and its command line are 8331 lines of Rust, the files in compiler/src and cli/src, and they are the code that the next number’s checks are written against.

The compiler’s hand-written checks, the scripts under tests/ and the small programs beside them, are 3331 lines, to be read against the compiler’s lines above: that is how much of the compiler’s specification lives in a suite, one chosen point at a time.

The tracker’s interface, apps/tracker/interface.vish, is 1123 lines, compared with the 1,123 that the scale-of-evidence table above gives for it on 27 September 2026; a difference would mean the tracker changed after that table was written.

Of those lines, 357 are inside examples blocks and scenario blocks, compared with the whole interface above: they state behaviour by example, the part a test suite would hold, and the rest of the interface (the rows, the contract headers with their outcomes, the flows) is the part a suite has no place for.