← Polyglot PureScript

Writing a PureScript backend

PureScript's compiler emits CoreFn — a small, untyped, functional intermediate representation — and hands it off. Everything downstream is a backend: a program that reads CoreFn (as JSON, or via the compiler's own plumbing) and produces code for some other runtime. This page collects everything we've gathered building columns for the differential matrix — the contract, the rules that cost us time, and the prior-art shelf.

Counterexamples are first-class content. The thesis of this whole project is that divergences between backends are enumerable. Finding one is a result, not an embarrassment.

The three lineages

Backends sort into three families, and the lineage predicts what a backend can cheaply do:

Whole-program monomorphisation lives naturally on the optimizer-IR path, so a performance-oriented backend tends to sit there. The Wasm GC backend is an outlier — it consumes CoreFn and externs.cbor to reconstruct foreign signatures for boundary marshalling.

The contract

"Adding a backend" to the differential suite means, concretely:

  1. Build path — compile the suite's Test/*.purs modules to runnable artifacts on your backend. The suite is deliberately FFI-free beyond the core libraries, so the only foreign code you need is your backend's own core-library shims.
  2. Runner — a pure subprocess run_<backend>(module) -> (stdout, error), plus the module-name mapping. No shared state.
  3. Divergence curation — every non-identical line is either a fix (your shim is wrong) or a KNOWN_DIVERGENCES entry with a prefix naming the cause (INT64-, ASTRAL-, BIGNUM-, FLOATFMT-, …) and a sentence in your README. An uncurated divergence is a failure.
  4. Results as data — one JSONL record per (program, backend, test). The site renders from this; don't emit prose summaries only.
  5. Regression — the existing columns must still pass.

Ground rules (learned the hard way)

The differential suite

The harness diffs every backend against the reference (JS) and pairwise where both diverge the same way — bignum backends should agree with each other, which is itself a checkable claim. The registry of backends is the clean extension point:

BACKENDS = {
    "js":     Backend(build=build_js,     run=run_js, ref=True),
    "julia":  Backend(build=build_julia,  run=run_julia),
    "erl":    Backend(build=build_erl,    run=run_erl),
    # default: all available — probe toolchains, skip missing with a
    # SKIPPED record, never a silent drop.
}

KNOWN_DIVERGENCES is keyed (module, test) -> {backend_or_class: expected}, where divergence classes (bignum, utf8) beat per-backend entries wherever they apply.

Prior-art shelf

Definition of done