// SPDX-License-Identifier: CC-BY-SA-4.0
= AffineScript Frontier Guide: The Unveiling
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
v1.2, 2026-06-04
:toc:
:icons: font
:source-highlighter: rouge
:revnumber: 1.2
:revdate: 2026-06-04

[NOTE]
====
Status: **canonical, living document.** This guide is the design reference for AffineScript itself and for any migration into AffineScript. Edits are welcome; record them in the <<revision-history>> appendix so the timeline is visible to future readers and AI agents.

For systematic translation patterns from , TypeScript, or another `-script` family language, read this guide first, then the link:migration-playbook.adoc[Migration Playbook].
====

The Frontier Guide takes you from "I write JavaScript / Python" to
"I understand why AffineScript's design choices produce better programs."
It is not a reference manual — it is an unveiling.

Each section begins by naming a problem you already know, then shows exactly
how AffineScript solves it, then gives you something to try.

== The Six Problems of X-Scripts

Before AffineScript, there were many "improved" JS variants.  Most of them
fixed one or two problems and ignored the rest.  AffineScript's thesis is that
all six of these problems have the same root cause: **the runtime does not know
the program's intent** — so neither does the programmer.

[cols="1,2,2"]
|===
| Problem | Symptom | AffineScript answer

| Null is a lie
| Two nothings: `null` and `undefined`
| `Option[T]` — exactly one nothing

| Mutation is invisible
| Every variable is secretly mutable
| `own`/`ref`/`mut` — mutation is in the type

| Errors throw sideways
| Untyped `throw` / `catch` anywhere
| `Result[T, E]` — errors in the return type

| Async is bolted on
| Callbacks → Promises → async/await → signals
| Algebraic effects — one composable model

| Types become unreadable
| TypeScript `infer`, conditional types, mapped types
| Row polymorphism — structural, principled, readable

| Runtime is still JS
| Type erasure — guarantees vanish at run time
| Typed WASM (WasmGC) — types survive into execution
|===

Let us unveil each one.

== Chapter 1: No Null

JavaScript gives you two ways to say "nothing": `null` and `undefined`.
They behave differently.  TypeScript adds `T | null | undefined` which
proliferates at every call site.

AffineScript has **one** nothing.

[source,affinescript]
----
// Option[T] — present or absent, not three states:
let name: Option[String] = Some("Alice")
let missing: Option[String] = None

// Pattern matching forces you to handle both cases:
fn greet(name: Option[String]) -> String {
  match name {
    Some(n) => "Hello, " ++ n ++ "!"
    None    => "Hello, stranger!"
  }
}
----

The compiler rejects any code that uses a `Some` without matching on `None`.
There is no way to accidentally treat `None` as a value.

=== Try it

[source,affinescript]
----
// Type-check this file and read the error:
fn greet_broken(name: Option[String]) -> String {
  // Deliberate mistake: treating None as Some
  name ++ "!"   // ERROR: Option[String] is not String
}
----

Run: `affinescript check` — the error tells you exactly what went wrong and
on which line.

== Chapter 2: Ownership (Mutation You Can See)

In JavaScript, every object is a shared mutable reference by default.  You
pass a value to a function; the function might mutate it; you have no idea.
You copy defensively.  You introduce bugs through aliasing.

AffineScript's type system makes mutation **visible in the type**:

[source,affinescript]
----
type Buffer = own { bytes: Array[Int], length: Int }

// ref: read-only borrow — the caller keeps the buffer
fn size(buf: ref Buffer) -> Int {
  buf.length
}

// mut: mutable borrow — modifies in place, caller sees the change
fn append(buf: mut Buffer, byte: Int) -> () {
  buf.bytes = buf.bytes ++ [byte];
  buf.length = buf.length + 1;
}

// own: transfers ownership — caller cannot use buf after this call
fn into_bytes(buf: own Buffer) -> Array[Int] {
  buf.bytes
}
----

The compiler tracks usage automatically.  If you pass `buf` to `into_bytes`,
any later use of `buf` is a compile-time error.  No runtime check needed.

[NOTE]
====
**Where the keyword goes.** Notice that `mut` / `ref` / `own` annotates the
*binder*, not the type. AffineScript writes `mut buf: Buffer`, never
`buf: mut Buffer`. The grammar rejects the latter. This is a real fork in
the road for translators coming from Rust (`&mut T`), Swift
(`var`/`let`), or TypeScript (`readonly T[]`):

* `mut buf: Buffer` — *the binding is mutable*; the buffer it points at
  may also be mutable, but ownership of "who is allowed to mutate" lives
  with the binder.
* `buf: mut Buffer` — would say *the type is mutable*, separable from the
  binding. AffineScript made the first choice; the compiler tracks
  mutability through the binder, so the `mut` lives there.

When porting Rust-shaped code, the `mut` moves left.
====

=== The quantity annotations

For fine-grained control, the underlying quantity annotations are available:

[source,affinescript]
----
// @linear: must be used exactly once
fn consume_once(@linear handle: Int) -> () { () }

// @erased: compile-time only (proof terms, type-class witnesses)
fn trust_me(@erased _proof: Bool) -> String { "I proved it" }

// @unrestricted: unlimited use (the default for all values)
fn read_many(@unrestricted n: Int) -> Int { n + n }
----

=== Try it

[source,affinescript]
----
type File = own { fd: Int }

fn open() -> own File { File { fd: 1 } }
fn read(f: ref File) -> String { "contents" }
fn close(f: own File) -> () { () }

fn broken() -> String {
  let file = open();
  close(file);          // file is consumed here
  read(file)            // ERROR: file used after move
}
----

== Chapter 3: Errors in the Return Type

JavaScript `throw` can happen from anywhere.  TypeScript does not check it.
You write try/catch and hope.

AffineScript errors live in the **return type**:

[source,affinescript]
----
// The function signature tells you everything:
fn parse_int(s: ref String) -> Result[Int, String] {
  match s {
    "0" => Ok(0)
    "1" => Ok(1)
    _   => Err("not an integer: " ++ s)
  }
}

// You must handle both cases — the compiler enforces it:
fn double_parsed(s: ref String) -> Result[Int, String] {
  match parse_int(s) {
    Ok(n)  => Ok(n * 2)
    Err(e) => Err(e)
  }
}

// The `?` operator propagates errors up automatically:
fn double_parsed_short(s: ref String) -> Result[Int, String] {
  let n = parse_int(s)?;
  Ok(n * 2)
}
----

There is no hidden throw path.  Reading the signature tells you what can go wrong.

=== Try it

[source,affinescript]
----
fn safe_head[T](xs: ref Array[T]) -> Result[ref T, String] {
  // Complete this function:
  // Return Ok(first element) if the array is non-empty,
  // or Err("empty array") if it is empty.
  Err("TODO")
}
----

== Chapter 4: Effects (One Model for Everything Impure)

JavaScript's async history: callbacks (2009), Promises (2012), async/await
(2017), React Hooks (2019), signals (2023-).  Each layer partially fixes
the previous one.  They do not compose.

AffineScript uses **algebraic effects** — one model for all of this:

[source,affinescript]
----
// Declare what the effect offers:
effect IO {
  fn println(s: String);
  fn read_line() -> String;
}

effect Async {
  fn await[T](promise: Promise[T]) -> T;
  fn spawn[T](f: () -> T) -> Promise[T];
}

effect State[S] {
  fn get() -> S;
  fn put(s: S) -> ();
}

// Declare which effects a function uses — no surprise impurity:
fn echo() -> () / IO {
  let line = IO.read_line();
  IO.println("You said: " ++ line)
}

// Compose effects naturally — they are rows:
fn fetch_and_log(url: ref String) -> Result[String, String] / IO + Async {
  let response = Async.await(http_get(url));
  IO.println("fetched " ++ url);
  Ok(response)
}
----

Because effects are types, the compiler rejects code that uses IO in a
context where IO was not declared.  No accidental side effects.

== Chapter 5: Row Polymorphism (Duck Typing with Proofs)

TypeScript's structural typing is powerful but the inference gets complex.
AffineScript's row polymorphism is principled and readable:

[source,affinescript]
----
// This function works on any record that has at least `name: String`.
// Whatever other fields it has (captured by `..r`) are passed through.
fn greet[..r](entity: {name: String, ..r}) -> String {
  "Hello, " ++ entity.name ++ "!"
}

type Person = {name: String, age: Int}
type Company = {name: String, employees: Int, revenue: Float}

// Both work — the row variable absorbs the extra fields:
greet(Person #{ name: "Alice", age: 30 })
greet(Company #{ name: "Acme", employees: 100, revenue: 9999.0 })
----

Row polymorphism is also how effects compose:

[source,affinescript]
----
// This function accepts any function that uses at least IO,
// plus any additional effects it may use (`..e`):
fn run_logged[..e](f: () -> () / IO + ..e) -> () / IO + ..e {
  IO.println("start");
  f();
  IO.println("end");
}
----

=== Arithmetic Without Typeclasses

Other languages let you forget which numeric type you're in. JavaScript's
`+` is overloaded across numbers and strings; Python's `+` polymorphises
across `int`, `float`, `Decimal`. AffineScript makes you commit — *but
only on the lhs* — and the rhs and result follow.

[source,affinescript]
----
// lhs is Float → rhs is checked as Float, result is Float:
fn scale(x: Float, k: Float) -> Float { x * k + 1.5 }

// lhs is Int → rhs is checked as Int, result is Int:
fn step(n: Int) -> Int { n * 2 + 1 }

// Mixed Int/Float — type error, no coercion:
fn bad(x: Float, n: Int) -> Float { x + n }     // ERROR: Float vs Int
----

The rule: arithmetic and comparison operators (`+`, `-`, `*`, `/`, `%`,
`<`, `<=`, `>`, `>=`) synthesise the lhs type first. If it's `Float`, the
rhs is checked against `Float`. Otherwise the operator is monomorphic at
`Int×Int → Int`. There is no implicit promotion and no typeclass
dispatch. If you want to mix, convert explicitly.

This earns its keep in two ways. First, error messages name a single
concrete type instead of a tyvar; you read the failure on the offending
line. Second, the lowering to every backend (WASM, JS, C, Faust, WGSL,
…) emits the right per-type instruction without needing dictionary
passing: `add` becomes `i64.add` or `fadd`, never both.

== Chapter 6: Typed WASM (Guarantees at Runtime)

TypeScript erases all its types when it compiles to JavaScript.  The runtime
has no idea what you proved.  You rely on defensive checks, branded types,
and faith.

AffineScript targets **Typed WebAssembly (WasmGC)**.  The type guarantees
survive into the binary.  The WASM runtime enforces them.

This means:
- Your struct layout is what you declared — no fields added at runtime.
- Your variant tags are what the compiler assigned — no stringly-typed dispatch.
- Linear memory is managed by the AffineScript runtime, not the JS GC.
- Your ownership invariants hold at the machine level, not just in the source.

[source,bash]
----
# Compile to standard WASM:
affinescript compile hello.affine --output hello.wasm

# Compile to WasmGC (struct.new, array.new_fixed, typed references):
affinescript compile-gc hello.affine --output hello.wasm

# Run in Deno (which supports WasmGC natively):
deno run --allow-read runner.js
----

== Chapter 7: Faces In, Backends Out

AffineScript reaches users through two orthogonal axes. Reading the
documentation of either alone leads to the wrong assumption that they
compose. They don't. The diagram is:

----
[ Python source ]   [ JS source ]   [ Lucid source ]   [ canonical .affine ]
        \                |                |                /
         ─────────► AffineScript AST ◄──────────────
                          |
       ┌──────┬──────┬────┴────┬──────┬──────┬─ ─ ─
     WASM    JS     C       WGSL   Faust   ONNX  ...
----

**Faces are input skins.** A face is a parser front-end: Python-shaped
syntax, JavaScript-shaped syntax, pseudocode, Pu-shaped Lucid,
CoffeeScript-shaped Cafe. The `--face` flag picks the parser; the AST
that comes out the other side is canonical AffineScript. Error messages
are translated back into the face's vocabulary, so a Python-face user
sees `def`, `True`, indentation, not `fn`, `true`, `{}`.

**Backends are output skins.** A backend is a code generator: WASM,
WASM-GC, Julia, JavaScript, C, Rust, OCaml, Lua, Bash, Gleam, ,
Nickel, WGSL (compute shaders), Faust (DSP), ONNX (tensor graphs),
LLVM IR, Verilog, … The output extension picks the backend
(`-o foo.js`, `-o foo.wgsl`, `-o foo.onnx`, …).

The two axes don't compose: there is no "JS face implies JS output" or
"Python face implies Python output." You can write Python-faced source
and compile it to WGSL; you can write canonical AffineScript and compile
it to . The face you read in is independent from the target you
write out.

[NOTE]
====
**Why this matters for design.** Once you internalise the orthogonality,
the whole `affinescript compile` command becomes legible: it's
*N faces × M backends* compositions, not a fixed pipeline. New surface
syntaxes don't require new backends; new backends don't require new
surfaces. This is why the language can keep adding both without
combinatorial cost.
====

== Chapter 8: What Every Backend Gets vs. What Some Get

The backend list is long, but not every backend accepts every program.
Backends are tiered by how much of the source language they take. The
tier is part of the *user contract*, not a limitation to be apologised
for — kernel sublanguages are restricted *on purpose*, because their
target runtimes are restricted.

[cols="1,2,2"]
|===
| Tier | Members | Acceptance

| **A — host-language emitters**
| WASM 1.0, WASM-GC, Julia, JS, C, Rust, OCaml, , Lua, Bash, Gleam, Nickel
| Full AffineScript surface. Anything you can write in canonical AS, these accept. Missing features in the codegen produce a loud error at compile time, never silent miscompilation.

| **B — IR / native**
| LLVM IR (Cranelift planned)
| Full surface, lower level of abstraction. Some passes (closures, traits) may need lowering that doesn't exist yet. From LLVM IR, `llc` retargets to x86-64, ARM64, RISC-V, AVR, PTX, AMDGPU.

| **C — kernel sublanguages**
| WGSL (compute shaders), SPIR-V (planned), CUDA / Metal / OpenCL (planned), Faust (DSP), ONNX (tensor graphs), MLIR/StableHLO (planned)
| Deliberately restricted subset. No `match`, no algebraic effects, no dynamic allocation, often no `String`. Anything outside the subset rejects explicitly. The acceptance criteria are documented per backend.
|===

A program that compiles to Tier A may not compile to Tier C. That is by
design: you wouldn't expect `match` over enum variants to work on a GPU
shader, and the WGSL backend's job is to tell you that explicitly rather
than emit shader code that misbehaves at runtime.

A practical implication for translators: identify your target's tier
*before* you start re-decomposing. If you're aiming at WGSL, you're
writing kernel-shaped code from the first line; sum types and effects
won't be available, and that should drive the design.

== Chapter 9: The Co-processor and the Integer

You already know the problem, even if you have not named it. You have a
working  or JavaScript program — a game, a simulator, a tool — and
you want the *logic* to run with AffineScript's guarantees, compiled to
wasm, without rewriting the rendering, the input handling, or the network.
The instinct is to port the whole thing. The better move is to split it.

**AffineScript is the brain; JS/Pixi are the senses.** The wasm core does
logic, physics, decisions, state machines; the host does rendering, input,
strings, and dispatch. The two halves communicate by passing **raw
primitives** — Ints, Floats, Arrays — across the wasm boundary. Nothing
stringly-typed crosses; nothing stateful crosses; only numbers.

This chapter unveils the mental model. The systematic, verified
re-decomposition patterns live in the
link:migration-playbook.adoc#coprocessor-integerization[Migration Playbook's
co-processor section] — read that when you are actually carrying a codebase
across.

=== The integer IS the X

The keystone idea: a value drawn from a small, named domain — a security
rank `Open/Weak/Medium/Strong`, a device-port name, a tile kind — is
**encoded to a canonical integer on the host side**, and the wasm core
computes on that integer. The host keeps the names; the core gets the code.

[source,affine]
----
// The encoding IS the rank.  0 = Open, 1 = Weak, 2 = Medium, 3 = Strong.
// The host owns "Open" <-> 0; this core owns the arithmetic.
pub fn rank_security_level(v: Int) -> Int {
  if v < 0 { 0 } else { if v > 3 { 3 } else { v } }
}

pub fn passes_min_security(actual: Int, required: Int) -> Bool {
  rank_security_level(actual) >= rank_security_level(required)
}
----

There is no `String` here, and no sum type crossing the boundary. The four
rank *names* never enter wasm; only their codes do. Compiled host-native,
this is a tiny module — in idaptik's case **454 bytes**, whose only WASI
import is `fd_write`. Every decision the host used to phrase in terms of
rank *names* is re-phrased as integer arithmetic on rank *codes*. That is
the whole pattern, and its slogan is _"the integer IS the X."_

=== Why a co-processor, and not a rewrite

Three things fall out of the split that a full rewrite would not give you:

. **The boundary is narrow and auditable.** Only primitives cross. You can
  enumerate exactly what passes between brain and senses, because it is a
  short list of Ints and Floats — not an open-ended graph of objects.
. **The core is small and verifiable.** A pure-integer kernel has no
  strings to allocate, no state to corrupt, no effects to handle. It
  compiles to a handful of kilobytes and its behaviour is a function of its
  inputs — which is exactly what makes <<chapter-9-parity,differential
  parity>> (below) a usable acceptance test.
. **The senses stay where they are good.** Rendering, input, and the DOM
  remain in the host, which already does them well. You are not reimplementing
  Pixi in AffineScript; you are giving Pixi a verified brain to call.

[NOTE]
====
This is the operational form of the WASM Co-processor Model described in the
link:frontier-programming-practices/Human_Programming_Guide.adoc[Frontier
Programming Practices] guide. That guide states the posture; this chapter and
the Playbook show you how to *port into it*.
====

[#chapter-9-parity]
=== How you know it worked: differential parity

A co-processor port has a crisp acceptance bar, and it is *not* "the compiler
accepted it." It is two conditions, both required:

* **Build-green** — the `.affine` compiles to wasm, host-native
  (`affinescript compile FILE -o OUT.wasm`).
* **Parity-green** — the wasm output matches an **independent oracle** — a
  separate implementation written from the *contract*, not from the
  `.affine` — across a sweep of inputs, including out-of-band ones.

idaptik's `SecurityRank` core cleared this bar at **78/78** against a JS
oracle of the documented `0..3` contract: every input, in-band and
out-of-band, agreed. Build-green alone would only have proved the compiler
ran; parity-green proves the kernel is *right*.

=== The honest edges

The co-processor model is sharp because its limits are sharp, and the limits
are teaching points, not apologies:

* **Strings have an ABI, so avoid crossing them.** When a string genuinely
  must cross, there is a real wire format (`[len: i32 little-endian][utf8]`)
  and an exported allocator to honour — see the Playbook. The cheapest way
  to handle a string at the boundary is to keep it host-side and send a code
  instead (the integerization above). Sometimes the data was *never* a
  string — idaptik's `Kernel_IO` does a sandbox prefix check on bytes that
  are already an `Int` array, so it compares integer arrays directly and
  never builds a `String` at all.
* **No code runs at module load.** AffineScript modules have no load-time
  side effect to hook.  that mutates a module-level cell, reads
  `Date.now()`, or calls `Console.log` ambiently has nowhere to put that
  after the port — those capabilities must be *threaded in as parameters*
  (or supplied by `extern` host functions) until effect-handling codegen
  lands. A kernel that relies on module-load effects will not compile to
  wasm today; that is the compiler telling you to make the dependency
  explicit.
* **The faithfulness is provable.** "The integer IS the X" is not folklore —
  it is an encoding-faithfulness theorem, machine-checked in
  link:../../proposals/echo-types/README.adoc[`EchoEncodingFaithfulness.agda`].
  An *injective* encoder is lossless; a *clamp* is provable, localised loss
  with no decoder. SecurityRank's clamp is exactly that certified hazard —
  and which *direction* it clamps (garbage up to `Strong` is fail-open;
  garbage down to `Open` is fail-closed) is a security decision the proof
  surfaces but does not make for you.

=== Try it

Sketch the boundary for a different domain before reading the Playbook.
Suppose the host has tile kinds `Empty | Wall | Door | Water`:

. Choose the integer encoding (`Empty = 0`, `Wall = 1`, …). Write it down —
  that table is the contract.
. Write a pure-integer AffineScript function `is_passable(tile: Int) -> Bool`
  that the host can call per tile. No `String`, no sum type crosses.
. Name one out-of-band input (a negative code, or `99`) and decide — *as a
  game-design question* — what `is_passable` should return for it, and
  whether that default is fail-open or fail-closed.

You have just done the three hardest parts of a co-processor port: the
encoding, the pure kernel, and the out-of-band policy. The Playbook turns
the sketch into a compiled, parity-checked module.

== Putting It Together: A Complete Program

[source,affinescript]
----
// complete_example.affine — a small resource manager

effect IO {
  fn println(s: String);
}

effect Exn[E] {
  fn throw(err: E) -> Never;
}

type Config = { max_retries: Int, timeout: Int }
type Request = own { url: String, config: ref Config }
type Response = { body: String, status: Int }
type FetchError = { message: String }

// Open a request (produces a linear resource):
fn make_request(url: String, config: ref Config) -> own Request {
  Request #{ url: url, config: config }
}

// Send the request (consumes it — can't send twice):
fn send(req: own Request) -> Result[Response, FetchError] / IO {
  IO.println("Sending request to " ++ req.url);
  // Real implementation would use the Async effect
  Ok(Response #{ body: "OK", status: 200 })
}

// The return type tells you: this can fail, and it does IO.
fn fetch_with_retry(
  url: String,
  config: ref Config
) -> Result[String, FetchError] / IO {
  let req = make_request(url, config);
  match send(req) {
    Ok(resp) =>
      if resp.status == 200 {
        Ok(resp.body)
      } else {
        Err({ message: "HTTP " ++ Int.to_string(resp.status) })
      }
    Err(e) => Err(e)
  }
}
----

== What to Read Next

- link:warmup/README.adoc[Warmup scripts] — hands-on exercises, 15 minutes each
- link:migration-playbook.adoc[Migration Playbook] — systematic re-decomposition rules for porting  / TypeScript / other `-script` codebases into AffineScript
- link:frontier-programming-practices/Human_Programming_Guide.adoc[Frontier Programming Practices] — the wider design philosophy (v2.0, 2026-04-10)
- link:../specs/SPEC.adoc[Language Specification] — the authoritative grammar and semantics
- link:../specs/SETTLED-DECISIONS.adoc[Settled Decisions] — architectural choices and why they were made
- link:../DESIGN-VISION.adoc[Design Vision] — the long view

== Faces: Meeting Learners Where They Are

If canonical AffineScript syntax feels unfamiliar, the `--face` flag lets
you write in a surface syntax closer to what you already know:

[source,bash]
----
# Python-style syntax — def, True/False/None, indentation:
affinescript --face python check my_program.py

# Preview what the preprocessor produces from a Python-face file:
affinescript preview-python-transform my_program.py
----

Regardless of which face you use, the type checker sees canonical
AffineScript.  Error messages are translated back to the face you chose, so
"Ownership error: single-use variable" appears instead of internal jargon.

Additional faces (JS-face, Pseudocode-face, and others) are on the roadmap.
See link:../specs/faces.adoc[faces.adoc] for the architecture.

[appendix]
== Appendix: Bringing in Target Intrinsics

Backends often need to call runtime ops the AffineScript resolver doesn't
know about — GPU shuffle intrinsics, ONNX operators, DSP primitives,
hardware-simulation tasks. You don't need to extend the language for
these. The pattern is **declare-then-recognise**:

. Declare the op as a stub function in your source file with the right
  signature.
. The resolver and typechecker accept the stub as a normal function.
. The backend codegen pattern-matches the stub's *name* (case-insensitive
  where appropriate) and emits the target's intrinsic.
. The stub's *body* is irrelevant — it's a placeholder so the AS frontend
  has something to bind. Use any value-shape that returns a sensible
  default.

[source,affinescript]
----
// Stubs for ONNX operators. Bodies are placeholders; the ONNX codegen
// recognises `relu` → ONNX op `Relu` and ignores what follows the `=`.
fn relu(x: Array[Float]) -> Array[Float] { x }
fn add(a: Array[Float], b: Array[Float]) -> Array[Float] { a }

fn graph(x: Array[Float], y: Array[Float]) -> Array[Float] {
  let s: Array[Float] = add(x, y);
  let r: Array[Float] = relu(s);
  r
}
----

`affinescript compile graph.affine -o graph.onnx` produces a real ONNX
`ModelProto` containing `Add` and `Relu` nodes. The codegen ignored the
stub bodies and used the names. The same pattern applies to every Tier-C
kernel-sublanguage backend: CUDA's `__shfl_xor`, Metal's `metal::float4`,
Faust's `tanh`, Verilog's `$display`, and so on.

Why this is preferable to extending the language: each backend stays
self-contained. The frontend remains target-agnostic. Stubs live in the
user's source where they're visible at the call site, rather than in a
hidden compiler-internal table that drifts as targets evolve. And when a
new target ships, its intrinsic vocabulary is just *whatever names its
codegen recognises* — a property the user can read off from the
backend's documentation and reproduce locally.

[appendix#revision-history]
== Revision History

This document is intended to evolve. When you change it — adding a chapter, sharpening an example, retiring an obsolete claim — record the change here so that downstream readers (especially AI agents loading the guide at session start) can see what has moved.

[cols="1,1,3"]
|===
| Revision | Date | Notes

| 1.0
| 2026-04-11
| Initial unveiling. The six X-Script problems and AffineScript's answers; chapters 1–6; complete-program example; faces.

| 1.1
| 2026-05-02
| Findings from the May-2026 backend buildout (16 new backends shipped: JS, C, WGSL, Faust, ONNX, OCaml, Lua, Bash, Nickel, , Rust, LLVM, Verilog, Gleam, plus protobuf and onnx_proto support modules) folded back into the guide:
  (1) Chapter 2 gains a "where the keyword goes" sidebar — `mut` annotates the binder, not the type.
  (2) Chapter 5 gains the *Arithmetic Without Typeclasses* sub-section — the typechecker now dispatches arithmetic on the lhs type (Float-aware as of 2026-05-02), with no implicit promotion.
  (3) New Chapter 7 — *Faces In, Backends Out* — makes the orthogonality of input skins and output backends explicit.
  (4) New Chapter 8 — *What Every Backend Gets vs. What Some Get* — introduces the A/B/C tier taxonomy so users know which backends accept full AS and which are kernel sublanguages by design.
  (5) New Appendix — *Bringing in Target Intrinsics* — documents the stub-as-intrinsic pattern that lets user code call CUDA / ONNX / Faust / Verilog runtime ops without extending the language.
  No removals; existing v1.0 content unchanged.

| 1.2
| 2026-06-04
| New Chapter 9 — *The Co-processor and the Integer* — unveils the  → AffineScript → wasm co-processor mental model ("AffineScript is the brain; JS/Pixi the senses; only primitives cross the boundary") and the "the integer IS the X" encoding idea, grounded in idaptik's verified 454-byte `SecurityRank` core (78/78 differential-parity). The chapter introduces the model and the honest edges (String ABI, no module-load effects, machine-checked faithfulness via `EchoEncodingFaithfulness.agda`); the systematic re-decomposition patterns live in the link:migration-playbook.adoc#coprocessor-integerization[Migration Playbook §Co-processor Integerization] (playbook v1.4). No removals; existing v1.1 content unchanged.

|===
