speckl/docs/blog-03-compiler-architecture.md
Scott Scoble 45ee6fc1bc
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
docs: sync Phase 2 content pipeline — all 5 blog posts + publishing toolkit
blog-01: Spec-Code Drift Is a Provenance Problem (~1,500 words)
blog-02: Designing SpeckDL (~1,800 words)
blog-03: How Speckl's Compiler Works (~1,900 words)
blog-04: Embedded Provenance (~1,200 words)
blog-05: Consensus Protocols in SpeckDL (~1,300 words)
publishing-toolkit: Dev.to/Medium/Reddit/HN formatting + objection prep (~3,500 words)

Total Phase 2 pipeline: ~11,200 words across 6 files.
Previously existed only in workspace copy (posts 1/3/toolkit) or
as untracked files (posts 2/4/5). All now committed to Forgejo.
2026-05-09 00:11:29 -07:00

8.3 KiB

How Speckl's Compiler Works: From Spec to Five Auditable Artifacts

Series: Building Speckl — Part 3 of 5
Published: [date]
Read time: 8 minutes
Tags: #compilers #specification #formal-methods #typescript #webassembly #compliance


You've seen why spec-code drift matters (Part 1) and what SpeckDL looks like (Part 2). Now let's look under the hood: how does a single .speck file become five independently auditable artifacts?

The Speckl compiler is a single-pass, multi-target compiler in TypeScript. It's deterministic, zero-dependency (beyond Node.js + wabt), and produces no network calls. Here's the pipeline:

.speck file → Parser → AST → [PROV-O | CycloneDX | SPDX | TypeScript | WASM]

Every generator walks the same AST independently. No generator ever talks to another. That shared AST is the single source of truth — if the spec says it, every artifact reflects it.

The Parser: ~850 Lines of Recursive Descent

The parser is hand-written using recursive descent with a tokenizer. No parser generators. No PEG grammar files. Just a straightforward TypeScript tokenizer that handles:

Keywords: speck, state, init, action, invariant, type, next, forall, let, emit, return, require, if, and, or, not, implies, in, notIn, union, subsetOf, emptySet, emptyMap, List

Operators: := (assignment/equality), ==, !=, <=, >=, +, -, *, /, \ (set difference), | (action composition)

Types: Bool, Nat, Int, String, Set<T>, Map<K,V>, List<T>, Record{...}, Optional<T>

The parser builds a typed AST with these node types:

  • SpeckNode — top-level spec with name and member list
  • StateNode — typed state variables
  • InitNode — initial state assertions
  • ActionNode — named actions with preconditions, body, and return type
  • InvariantNode — boolean invariants checked after every action
  • NextNode — action composition (A | B | C means "A or B or C may fire next")

Type aliases are fully resolved during parsing — generators never see them. A type AccountID = Nat becomes Nat everywhere it's referenced. This means generators operate on structurally complete types, never chasing references.

Five Generators, One AST

1. PROV-O Generator — The Audit Trail

Emits a W3C PROV-O provenance graph in Turtle (.ttl). This is the foundation of Speckl's auditability:

  • Entities: The .speck file, each generated output file
  • Activities: Compilation as a whole, each generator invocation
  • Agents: The compiler (version + commit hash), the human author, any AI contributor
  • Relations: wasGeneratedBy, wasDerivedFrom (code → spec), wasAttributedTo, used

The PROV-O graph answers "Where did this code come from?" with a machine-readable chain. Feed it to a PROV validator and you get cryptographic certainty about provenance.

2. CycloneDX Generator — The Industry-Standard SBOM

Outputs CycloneDX v1.4 JSON. Each artifact declares:

  • The generated module as a software component with SHA-256 hash
  • The compiler as a build tool
  • The source .speck file as a component dependency
  • License metadata

CycloneDX is consumed by Dependency-Track, OWASP scanners, and compliance platforms (SOC 2, FedRAMP). Your spec doesn't just produce code — it ships with an audit-ready SBOM out of the box.

3. SPDX Generator — The ISO Standard SBOM

Outputs SPDX v2.3 JSON (ISO/IEC 5962:2021). Provides package-level license declarations, relationship mapping to the source spec, and creator attribution. SPDX is required by many enterprise procurement processes — Speckl generates it automatically.

Two SBOM generators might seem redundant, but they serve different ecosystems: CycloneDX for OWASP/devsecops toolchains, SPDX for enterprise legal review. Both from the same spec with zero extra effort.

4. TypeScript Generator — Runnable State Machines

This is the powerhouse. The TypeScript generator emits a class implementing the entire state machine:

  • Type aliases become TypeScript type declarations
  • State becomes a typed State interface
  • Actions become public methods with guard clauses (preconditions throw PreconditionError)
  • Invariants compile to a _checkInvariants() method that runs after every action
  • Events (emit) route to an injected EventEmitter interface — decoupled from any logging/metrics/audit infrastructure
  • State is private — only readable via snapshots, never directly mutable from outside

Here's what a ToggleSwitch spec produces:

class ToggleSwitch {
  private _state = { on: false };

  TurnOn(): void {
    if (!this._state.on) throw new PreconditionError("TurnOn requires !on but on is true");
    this._state.on = true;
    this._checkInvariants();
  }

  TurnOff(): void {
    if (this._state.on) throw new PreconditionError("TurnOff requires on but on is false");
    this._state.on = false;
    this._checkInvariants();
  }

  private _checkInvariants(): void {
    // Runtime invariant checks
  }

  getState(): Readonly<State> { return { ...this._state }; }
}

It also handles forall loops over sets, if branches, let bindings, and compound types (Set<T> → Set<T>, Map<K,V> → Map<K,V>, List<T> → T[]).

5. WASM Generator — Specs in the Browser

Emits WebAssembly Text Format (.wat) that assembles to .wasm via wat2wasm:

  • One exported function per action
  • State stored in linear memory with type-specific encodings
  • Guard failures cause WASM unreachable traps
  • Invariants validated with trap-on-failure

This means Speckl specs run in browsers, edge functions, embedded systems — anywhere with a WASM runtime. And because the output is human-readable .wat before assembly, you can grep it, verify it, and feed it to formal verification tools.

The Test Suite: 41 Tests, 2 Seconds

The compiler runs 41 tests across parser correctness, generator output format compliance, and end-to-end WASM instantiation. All tests complete in under 2 seconds. The suite is designed to make the compiler safe to refactor — break spec semantics, and tests catch it instantly.

Key test categories:

  • Parser: Every language construct, nested generics, operator precedence, edge cases
  • Generators: PROV-O structure, CycloneDX schema compliance, SPDX completeness, TypeScript type checking (tsc --noEmit), WAT assembly validity
  • E2E: Full pipeline → validate TypeScript compiles → validate WAT assembles → instantiate WASM → run actions → verify state + guards

Design Decisions That Matter

Zero external services. The compiler never makes network calls. Reads from disk, writes to disk. Deterministic, reproducible, air-gappable.

Independent generators. Adding a new target language (Rust, Python, C, SQL schema) means writing one new 200-400 line module. The parser and other generators don't change.

Text-first WASM. We emit .wat (text) not binary .wasm. Human-readable. Grep-able. Assembly is deferred to wat2wasm, keeping dependency surface minimal.

DIST DESYNC guard. A check-dist.sh + Makefile guard prevents stale compiled JavaScript from masking real bugs. The compiler refuses to run if dist/ is out of date relative to src/. This eliminated an entire class of false-positive test passes.

CLI-first. The compiler is speckl compile file.speck. Internal API is stable but the primary interface is the command line — easy to drop into Makefiles, CI/CD pipelines, and build scripts.

What This Means

The Speckl compiler transforms a specification from prose to proof. One .speck file becomes: a provenance graph, two industry-standard SBOMs, a type-safe TypeScript class, and a deployable WASM module. All independently auditable. All from a shared AST with zero external dependencies.

In the next post, we'll look at Speckl's embedded provenance system — how the compiler proves that the code you're running is the code you specified.


Next: Part 4 — "Embedded Provenance: Proving What Ran Came From What You Wrote"

Read Part 1: Spec-Code Drift Is a Provenance Problem
Read Part 2: Designing SpeckDL: What a Spec Language Needs in 2026
Speckl repo: os.scoble.me/forgejo/sscoble/speckl