Wheeler

WIP-0021: Uniform call and assertion syntax

FieldValue
StatusImplementing
OwnersWheeler language, compiler, testing, formatter, and documentation maintainers
Created2026-07-18
Updated2026-07-18
AreaSource syntax, assertions, test doubles, diagnostics, migration
Depends onWIP-0005, WIP-0006, WIP-0016, WIP-0018
SupersedesNone
Superseded byNone

Summary

Wheeler should use call-shaped syntax for operations that take arguments. A direct assertion therefore uses assert(condition);.

Shared syntax does not erase semantic differences. Ordinary, inverse, coherent, unitary, measurement, and effectful calls keep distinct IR descriptors and verifier rules. Wheeler should not add parallel forms such as assertTrue, assertEquals, expectEqual, matcher chains, or bare assert condition; syntax.

Test doubles should be explicit typed values or fixtures passed through normal ownership and capability boundaries. Wheeler shouldn't add Mockito-style interception, when(...).thenReturn(...), ambient replacement registries, or another meaning of verify.

This proposal sets the syntax rule and migration gate before the corpus changes. It prevents a broad text replacement from becoming the language definition by accident.

Motivation

Before this migration, the bootstrap profile wrote equality checks like this:

assert value == 2;

The form is easy to parse, but it does not match argument-taking intrinsics such as drop(value);, prepare(q, 0);, or normal calls. It also encourages a second family of test-only names and matcher APIs.

Extra syntax has a real bootstrap cost. The stage-0 parser, Wheeler parser, Tree-sitter grammar, formatter, examples, lessons, malformed corpus, and self-host differential fixtures must all agree. Two spellings for the same operation double the maintenance surface without doubling clarity.

The migration needs a plan because Wheeler source appears in .w files, Java text blocks, Tree-sitter corpora, Markdown, package archives, and native compiler fixtures. Search can find likely matches. Parsers and tests must prove the final result.

Use cases

  1. A classical entry checks state with assert(count == 2);. Stage 0, the Wheeler compiler, Tree-sitter, the formatter, and documentation all recognize exactly that form.
  2. A parameterized test checks assert(roundTrip(value) == value);. It does not choose among assertTrue, assertEquals, and expectEqual according to the author's breakfast.
  3. A package test needs a failing storage provider. It receives an explicit FailingStore fixture implementing the required typed boundary. No ambient registry rewrites the production store, and ordinary ownership rules still apply.
  4. A reversible test records an event, runs an inverse, and asserts over the resulting typed state. The assertion is an irreversible observation. Method-like syntax does not make it part of the reversible operation.
  5. Old bare assertion syntax reaches the new parser. Compilation fails at the missing ( with one bounded diagnostic. No compatibility parser guesses which source generation produced it.

Goals

Non-goals

Terms and semantic model

An ordinary call evaluates explicit arguments left to right, checks the callee's static signature, and crosses the ownership and effect boundary declared by that signature.

An intrinsic call uses the same visible argument punctuation but the compiler resolves it to a language operation instead of a user declaration. Call-shaped syntax does not imply that an intrinsic is a first-class function.

A structural form controls parsing, binding, effects, inversion, or proof construction. Classes, methods, if, match, reverse, and theorem clauses are structural forms and need not mimic calls.

An assertion evaluates one Boolean expression exactly once. If it is true, execution advances without mutating program state. If it is false, execution traps before later state mutation or report publication. A test runner can record the assertion event, but the event is not a reversible program-state transition.

A test double is an explicit implementation of a typed boundary supplied by a test descriptor or fixture. A double has ordinary identity, ownership, limits, effects, and event output. It is not an instruction to rewrite another declaration.

The first grammar decision is:

assertion_statement := "assert" "(" boolean_expression ")" ";"

The following are deliberately not aliases:

assert ready;                    // rejected: missing call punctuation
assertTrue(ready);               // rejected: no duplicate intrinsic
assertEquals(expected, actual);  // rejected: equality belongs in the expression
expectEqual(actual, expected);   // rejected: another duplicate

The canonical form is:

assert(ready);
assert(actual == expected);

Ownership and boundaries

The language specification owns the classification of call, intrinsic, and structural forms. The compiler owns name resolution, Boolean typing, single evaluation, lowering, and source diagnostics. The bytecode verifier owns the validity of the lowered checked operation. The VM owns successful advance and trap-before-mutation behavior.

tree-sitter-wheeler owns a structurally equivalent editor grammar, not an alternative language. The formatter owns whitespace around the accepted token structure and must not translate between assertion APIs. The package test runner owns assertion events and test-double fixture construction. Host adapters may implement a typed fixture but may not install ambient interception.

Documentation and examples consume this contract. They are not a compatibility authority, even when an old snippet has been copied often enough to acquire seniority.

Design

Syntax rubric

A construct uses call-shaped punctuation when all of the following hold:

  1. it consumes zero or more value expressions.
  2. argument evaluation follows ordinary expression order.
  3. it does not introduce a lexical binding or a nested control region.
  4. it does not alter parse precedence outside its parentheses.

A construct remains structural when it introduces names, scopes, branches, loops, inversion regions, ownership transfer syntax, effect clauses, or proof terms whose meaning cannot be represented as ordinary eager arguments.

New syntax proposals must state which side of this line they occupy. A preference based on one example is not a semantic category.

Assertions

Wheeler exposes only assert(condition); for a direct Boolean assertion. Equality and truth are expression operations. Diagnostics may render evaluated expected and actual values when the expression carries typed comparison metadata, but richer rendering does not create another source spelling.

Expected traps, inverse restoration, rewind restoration, quantum-state comparison, sampled acceptance, and proof-kernel rejection are test-runner operations with their own typed evidence. They may use call syntax, but they must remain separate from direct Boolean assertions.

Reversible, quantum, and proof evidence

The framework does not add a separate assertion name for every Wheeler test dimension. Execution operations produce typed evidence, and assert(condition); then checks a proposition over that evidence.

DimensionEvidence producerValid assertion subjectInvalid shortcut
Forward executionbounded fresh VM attemptfinal typed state, output, steps, trap, eventshost exception text
Language inverseverified forward/inverse attempt pairexact declared-state restoration and inverse identityVM rewind result
VM rewindretained-history rewind attemptcomplete snapshot and history cursor restorationinvoking a generated inverse
Uncomputationcoherent workspace witnessclean ancillas, restored workspace, preserved outputmeasurement-only agreement
Exact quantumexact simulator evidenceamplitudes under a declared global-phase rule, qubit order, clean ancillassampled counts
Sampled quantumshot-bounded target evidenceexplicit acceptance result and retained target identityBoolean coercion of inconclusive evidence
Hybrid workflowdurable run and event evidencereplay, retry, commit, cancellation, compensation, and job identitiesrerunning a provider call during replay
Proofkernel result tied to a proposition and certificateexact accepted/rejected rule result and proposition identitycompiler search success
Malformed artifactverifier attemptstable rejection code and pre-execution statea Java decoder exception class

A test may take snapshots and invoke these producers through explicit runner APIs. The syntax must keep operations distinct even when the final line is an ordinary assertion:

MachineSnapshot before = machine.snapshot();
InverseEvidence inverse = runner.invokeInverse(machine, operation);
assert(inverse.operationIdentity == operation.identity);
assert(machine.snapshot() == before);

RewindEvidence rewind = runner.rewind(machine, inverse.forwardSteps);
assert(rewind.restoredHistoryCursor);

The example is design pseudocode, not current reference syntax. In particular, invokeInverse and rewind return different nominal types. A generic matcher that accepts either would erase the distinction the framework is supposed to test.

Exact quantum assertions occur outside unitary regions over simulator evidence. An assertion inside coherent evolution would itself be an observation and is rejected. Sampled evidence reduces to Accepted, Rejected, or Inconclusive. Only an explicit comparison with one of those variants yields a Boolean. Inconclusive never becomes truthy because the dashboard looked anxious.

Proof assertions consume kernel output. assert(result.accepted); establishes that the kernel accepted the exact certificate and proposition identity. It says nothing about exhaustive compiler search or ideal hardware behavior. It also cannot treat a differently encoded proposition as equivalent.

Expected traps likewise become values in runner-owned attempt evidence instead of host exceptions:

Attempt evidence = runner.observe {
  boundedOperation();
};
assert(evidence.outcome == Outcome.Trap(TrapCode.Bounds));
assert(evidence.successfulTransitions == 0);

observe { ... } is structural pseudocode because it delimits execution, ownership, and trap capture. It is not proposed as an ordinary eager function. WIP-0018 owns its final spelling and descriptor semantics.

Test doubles

A test selects a double by declaring a fixture or explicit parameter whose static type satisfies the production boundary. Construction is deterministic and bounded. Calls produce typed events owned by that double. Tests assert over returned values, state, or event sequences using ordinary Wheeler expressions.

There is no method interception, invocation-count matcher, hidden global replacement table, or source-level verify(double).called(...) dialect. If call accounting matters, the boundary exposes a typed bounded event log. This is slightly more typing and considerably less séance.

Naming

Assertion-related operations use the assert stem only when they terminate a case on failure. Predicates remain predicates: isEmpty, contains, verified, or a domain name. Verification operations retain domain-qualified meaning and return typed results or diagnostics. They are not assertion aliases.

The manual must not teach assertTrue, assertFalse, assertEquals, expectEqual, bare assert, Mockito matcher names, or fluent stubbing as Wheeler syntax.

Reversibility and history

An assertion is a checked observation. A successful assertion changes no Wheeler program state and emits no reversible inverse operation. A failed assertion traps before state mutation and therefore adds no successful VM transition or retained-history record.

Runner-level assertion evidence is append-only test evidence. Rewinding program execution does not erase the runner's knowledge that an assertion was attempted. Language inversion, VM rewind, and test-report reduction remain separate operations.

A test double follows the reversibility contract of its typed boundary. Pure deterministic doubles need no history. Stateful reversible doubles provide explicit inverse behavior. Effectful doubles record bounded fixture-owned events. Calling those events mock history does not make them reversible.

Concurrency and determinism

Assertion expression evaluation follows the language's ordinary deterministic evaluation order. The runner assigns assertion events stable case-local sequence numbers. Parallel case completion cannot alter semantic report order.

Test doubles are invocation-owned unless a descriptor explicitly declares shared state. Shared doubles require a deterministic scheduler or append-only canonical event order. Ambient singleton doubles and process-wide call counters are rejected.

Quantum and proof implications

assert(condition); checks a classical Boolean. It cannot inspect unmeasured amplitudes, establish a theorem, or turn sampled evidence into certainty.

Quantum state, sampled distributions, generated adjoints, clean ancillas, and proof certificates require typed runner or kernel operations defined by WIP-0011 and WIP-0018. Their results may feed a classical assertion only when the relevant contract explicitly produces a Boolean with adequate evidence.

The proof kernel's verify operation retains its semantic name. Mockito-style verify(mock) syntax is excluded so proof review does not require guessing which universe the verb belongs to.

Bytecode, persistence, and compatibility

The punctuation migration does not require a new .wbc opcode. The accepted assertion lowers to the existing checked operation where its expression fits the current profile. Broader Boolean expressions may require separately reviewed lowering but not a duplicate assertion family.

Source compatibility is intentionally broken. The parser rejects bare assert condition; after migration. Canonical package archives containing source must be rebuilt and relocked. Existing canonical .wbc artifacts remain valid because their semantics do not depend on source punctuation.

Artifacts and manifests contain no legacy assertion syntax switch. Source profiles do not nest old syntax modes.

Safety, limits, and failures

Assertion expressions obey ordinary expression depth, local, step, and arithmetic limits. Evaluation traps before the assertion outcome if the expression itself traps. A false result produces one stable assertion diagnostic. Diagnostic payloads and rendered values remain bounded.

Test-double event logs, queued responses, failures, and fixture bytes have descriptor limits. Exhaustion fails the case instead of dropping events or returning an undeclared default. Unknown fixture kinds, incompatible boundary types, duplicate fixture identities, and ambient replacement requests fail during discovery.

Near-miss syntax is rejected deterministically:

Migration and deletion

  1. Inventory assertion and test-double spellings in Wheeler files, embedded source fixtures, Tree-sitter corpora, Markdown, package archives, and generated locks. Classify matches. Do not replace English prose based only on a text search.
  2. Add parser and Tree-sitter acceptance for assert(condition);, plus malformed tests for the old and near-miss forms.
  3. Update the Wheeler-native parser and differential compiler fixtures. Require stage-0/native byte-identical .wbc for equivalent accepted sources.
  4. Update the formatter and documentation validator tests for token preservation, comments, idempotence, and call punctuation.
  5. Migrate canonical Wheeler packages and examples, rebuild exact .wpk archives and locks, then run every package and workspace test.
  6. Migrate Java text blocks, Tree-sitter corpora, manuals, WIPs, and future sketches. Speculative documents must still use valid syntax for the profile they claim.
  7. Delete bare assertion parsing in the same feature. Add no compatibility switch, warning period, or second AST node.
  8. Audit assertion-name and mock-style vocabulary. Replace duplicate proposed APIs with assert(expression) or explicit typed fixture/event operations.

Progress

Testing and acceptance

Alternatives

Keep bare assert condition;

Rejected. It saves two punctuation characters while creating a special argument-taking statement and a predictable bikeshed over every future assertion name.

Provide assertTrue, assertFalse, and assertEquals

Rejected. Boolean truth and equality already belong to expressions. Duplicate assertion names split teaching, diagnostics, generic equality, and future self-hosting work without adding semantics.

Copy JUnit and Mockito

Rejected. JUnit remains useful stage-0 scaffolding, but its Java overloads, reflection, exceptions, and extension model are not Wheeler semantics. Mockito's dynamic interception and fluent matcher state conflict with explicit ownership, deterministic discovery, and self-hosting.

Treat every structural form as a function

Rejected. Control flow, binding, inversion, and proofs have region semantics that eager ordinary calls do not express. Uniform syntax is not useful when it hides different evaluation rules.

Open questions

References