Skip to content

feat: check machines with the solvers and an explorer - #86

Merged
Sequela02 merged 9 commits into
mainfrom
feat/stage-10-machines
Nov 13, 2026
Merged

Sequela02 merged 9 commits into
mainfrom
feat/stage-10-machines

Conversation

@Sequela02

Copy link
Copy Markdown
Contributor

Stage 10's first slice: Accord now checks machine declarations, as #82's decisions and docs/design/syntax.md ("Machines", decision 7) describe. TypeScript output for machines and the ports come next.

What it does

  • Parses and checks machines. That covers state, init, event, observers, invariant and helper invariant, and next { … } bodies through if or decide. Sequence examples use example M … gives.
    • It rejects: a missing or unknown field, a field set twice, a parameter named like a field, next used as a name, an event with ensures, an invariant that calls a function, a machine with no init, and two invariants with one name.
  • Obligations, through the existing machinery. Each init and event becomes a function with the fields as parameters, so types, SMT terms, overflow, decision tables and contracts are reused.
    • New: an init meets every invariant; an event preserves them; an event is allowed in some state an init reaches.
    • Invariants and helpers are proved assuming the arithmetic they evaluate stays in its type, which is an obligation of its own.
  • Explorer. It runs breadth-first from every init and enumerates only finite domains: Bool, enum, or integers pinned to literals or to a literal range of at most 1,024 values, clamped to their type. It caps at 100,000 states and 2,000,000 steps.
    • Every trace is replayed by the interpreter before it counts.
    • An invariant is refuted only by such a trace.
    • A solver refutation without one is the new outcome not_inductive, carrying the state the solvers found. It makes standing incomplete, not failed, and has code ACC0015, add_helper_invariant.
    • Exploration coverage is an annotation on the obligation, never an outcome.
  • Receipt version 4: not_inductive, trace refutations, exploration annotations, helper invariants as support, and typed inputs for every parameter.
  • accord build and accord diff refuse machines for now.

Process

  • The 21 machine* cases were committed before their code (cce0d9d and cbddd1b) and none of their expected lines changed. Two cases renamed a field from count to value, because count is a quantifier keyword.
  • An independent review found 4 defects plus 2 minor ones. Each got a case first, then a fix (20240a7):
    • an argument outside its type proved an event "allowed";
    • an overflowing step refuted an invariant it never broke;
    • diff reported a machine's lost invariants as kept;
    • the hash ignored member roles and helper flags;
    • duplicate invariant names were accepted;
    • stale reused flags.

Verification

  • cargo fmt --all --check and cargo clippy --workspace --all-targets --locked -- -D warnings are clean.
  • cargo test --workspace --locked with the pinned solvers: 93 passed, 1 ignored (the existing solver-timeout helper). That includes all 21 machine cases and 6 tests in tests/machines.rs.
  • No regression for functions: I built main (70a624d) and this branch, and ran all 120 non-machine cases through both. Definition hashes and every outcome are identical: 507 obligations and examples, and every standing. The only difference is the receipt version (3 to 4).

Not established:

  • the 100,000-state and 2,000,000-step caps, which no test reaches;
  • a machine in a multi-file folder;
  • the ProviderRetry and opossum ports, which are the next slices.

Risk note

One-way-ish for the receipt format: version 4 adds an outcome, and consumers that match outcomes exhaustively must handle not_inductive. Nothing is published yet, so this is still reversible. The function path is unchanged, as shown above. Blast radius: the checker and receipt for programs that declare machines. Evidence: the conformance cases, the review's reproductions now passing, and the base comparison.

Refs #82

AGENTS asks for a construct's conformance cases, faults included, before
the compiler code that produces them. These fix what a machine's
obligations are and in which order, what refutes an invariant (a trace
from an init) and what is only not inductive, and what the checker must
reject.

Refs #82
A machine is desugared into functions of its state, so its inits,
events and observers reuse the obligation machinery: the invariants
are assumed of the state an event or observer reads and claimed of the
state an init or event ends in. Z3 and cvc5 prove each obligation.

An invariant or arithmetic obligation the solvers refute from a state
is refuted only by a run from an init that a breadth-first explorer
finds and the interpreter runs again; otherwise it is not_inductive,
with the solvers' state, a standing of incomplete and ACC0015. An event
is allowed in a reachable state only by such a run. What exploration
covered is an annotation on the obligation, never an outcome.

The receipt becomes version 4. `accord build` refuses a machine. `count`
is a name again outside a quantifier, since machines have fields by it.

Refs #82
The receipt gains the not_inductive outcome, traces, the exploration
annotation and ACC0015, and the concepts name them. Machines are
checked now, though not yet written as TypeScript.

Refs #82
Making `count` an ordinary name changed the whole language for two
cases. Those two cases' field is now `value`, since `count` is a
quantifier keyword. No expected outcome changed.

Refs #82
A helper invariant is marked in the checked program and reported as
support beside the specification, in JSON and text; it is assumed and
proved as an invariant is. Each machine obligation also carries the
parameters its inputs name, so a not_inductive state or an event
witness types the event's and init's parameters too.

Refs #82
A review found an argument outside its type proving an event allowed,
an overflowing step refuting an invariant it never broke, and two
invariants sharing a name. These cases fix the intended outcomes before
the code changes: each failure is reported once, by its own obligation,
and no run uses a value its type can't hold.

Refs #82
Arguments the explorer enumerates, and that a run may carry, are now
within their parameter's type. An init's or event's invariants are
proved, and a run breaks them, only where its arithmetic stays within
its type; that failure belongs to the arithmetic obligation alone.
`accord diff` refuses a machine, a machine's hash covers each member's
role and whether an invariant is a helper, invariant names are unique,
and an outcome the explorer changed no longer says it was reused.

Refs #82
@Sequela02 Sequela02 added the full-ci Run Windows and macOS CI once before merging label Nov 13, 2026
The review materials check the receipt contract they read, and receipt 4
adds machine outcomes and a support list. For functions it changes only
the review hash, so the cards and stage 7 packets differ in that line
alone; regenerated with materials.py --write.

Refs #82
The wire test's known report still named receipt 3, which the contract
now rejects; a stale bytecode cache hid it locally.

Refs #82
@Sequela02 Sequela02 added full-ci Run Windows and macOS CI once before merging and removed full-ci Run Windows and macOS CI once before merging labels Nov 13, 2026
@Sequela02
Sequela02 merged commit 1514a3d into main Nov 13, 2026
5 checks passed
@Sequela02
Sequela02 deleted the feat/stage-10-machines branch November 13, 2026 04:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

full-ci Run Windows and macOS CI once before merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant