Skip to content

feat: write a proved machine as TypeScript - #89

Merged
Sequela02 merged 6 commits into
mainfrom
feat/stage-10-typescript
Nov 13, 2026
Merged

Sequela02 merged 6 commits into
mainfrom
feat/stage-10-typescript

Conversation

@Sequela02

Copy link
Copy Markdown
Contributor

Stage 10's second slice: accord build writes a pub machine as a TypeScript module, once every obligation is proved and every example passes. The ports (ProviderRetry, opossum) come next.

The module

The reference is conformance/typescript/machine/out/switch.ts. It was written by hand before the generator, and the generator matches it byte for byte.

  • export type M = { readonly field: T; … }.
  • checkM$(where$, state$) reads the state once. It checks the object, each field's type and range, every invariant, and every helper invariant, then returns that snapshot. Failures throw TypeError or RangeError, naming the field or the invariant.
  • An init takes its parameters and returns a state.
  • An event or observer takes (state$, params…), runs only on the checked snapshot, checks its parameters and requires, and returns the next state (for an event) without changing the one it was given.
  • Generated names end in $, which no Accord name can use. A module whose exported value or type names would collide is refused.
  • A machine with any unproved obligation, not_inductive included, isn't written (refused-machine).

Process

  • The expected module and a refused case were committed before the generator (5f74f30).
  • An independent review found 3 defects. The contract was corrected first (b383518), then the code (f3c626f):
    • The check and the event each read the state, so a getter could pass the check with one value and hand the event another. Reproduced: press returned presses: -4, breaking the invariant.
    • A field named state or where produced a module that wouldn't import.
    • An enum and a machine sharing a name produced a duplicate type.

Verification

  • cargo fmt --all --check and cargo clippy --workspace --all-targets --locked -- -D warnings are clean.
  • cargo test --workspace --locked with the pinned solvers: 97 passed, 1 ignored. That includes:
    • every TypeScript folder byte for byte, so function-only modules are unchanged;
    • the differential test, which now runs each init, event and observer against the interpreter, including states that break an invariant or helper and must be rejected;
    • Node tests for the getter attack and for fields named state and where.
  • The CI step for the E2 materials (score.py --self-test and materials.py) passes, run without a bytecode cache.
  • The reviewer's attack, rerun against the generated module: every result uses the checked value.
  • Sanity check on the differential test: removing the helper-invariant check made it fail. The author reports this; it wasn't rerun independently.

Not established: no reference covers i64 fields, event parameters that are text or lists, or a decision table nested in an if. tsc wasn't run; Node strips types, and the module is checked for typing only where the existing tests typecheck generated code.

Risk note

Two-way door: TypeScript output is "unstable until the first release" (docs/typescript.md), and nothing is published. Blast radius: accord build for programs with machines; function-only output is unchanged byte for byte. Evidence: the reference module, the differential test, and the review's attacks rerun.

Refs #82

The TypeScript conformance folders hold the modules accord build must
write, byte for byte, written before the generator. This one fixes how a
machine crosses the boundary: a readonly state type, one check of the
state's fields, invariants and helper invariants for every event and
observer, and events that return the next state. A machine whose
invariant isn't proved inductive must be refused.

Refs #82
A pub machine becomes a state type, a check of a state's fields and
every invariant and helper invariant, and an exported function for each
init, event and observer; the generator reuses the functions' parameter
and requires checks, expressions and decision tables. Build writes it
only when every obligation is proved, and refuses a module whose names
would collide. The differential test runs each member against the
interpreter, including states that break an invariant, which the module
must reject.

Refs #82
A review showed the state check and the event each read the state, so an
accessor could pass the check with one value and hand the event another.
The check now returns one snapshot of the fields, which is all the event
reads, and its generated names end in $ so no field or parameter can
collide with them.

Refs #82
The state check returns a snapshot built from its single read, and
events and observers run on it, so a getter can't pass the check and
then give the body another value. Generated names end in $, so fields
called state or where no longer collide. Enums and machines sharing an
exported type name are refused.

Refs #82
@Sequela02 Sequela02 added the full-ci Run Windows and macOS CI once before merging label Nov 13, 2026
@Sequela02
Sequela02 merged commit 5dd815d into main Nov 13, 2026
5 checks passed
@Sequela02
Sequela02 deleted the feat/stage-10-typescript branch November 13, 2026 04:58
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