|
| 1 | +// SPDX-License-Identifier: CC-BY-SA-4.0 |
| 2 | +// SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath) |
| 3 | += AffineScript Face Contract — the normative binding between faces and the canonical |
| 4 | +Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> |
| 5 | +:toc: macro |
| 6 | +:toclevels: 2 |
| 7 | + |
| 8 | +Status: DRAFT (2026-07-08) — Milestone A keystone of the face-sealing plan. |
| 9 | + |
| 10 | +This document is the *single normative source* for how a language _face_ binds to |
| 11 | +the canonical AffineScript compiler. Where `docs/specs/faces.adoc` describes the |
| 12 | +_design_ of the face architecture (ADR-010), this document fixes the _contract_: |
| 13 | +the roster, the file/pragma model, the CLI names, the shim protocol, the canonical |
| 14 | +pin, and the conformance + proof obligations every face must meet. Brand-surface |
| 15 | +repos link here rather than restating any of it. |
| 16 | + |
| 17 | +toc::[] |
| 18 | + |
| 19 | +== The face roster (normative) |
| 20 | + |
| 21 | +A _face_ earns a place on this roster only if it serves a *distinct, living surface |
| 22 | +community* whose idioms map meaningfully onto AffineScript's affine/linear + |
| 23 | +effect + typed-wasm semantics. Near-duplicate surfaces and dead-runtime dialects |
| 24 | +do not qualify as standalone faces (see <<actionscript>>). |
| 25 | + |
| 26 | +[cols="1,2,1,1", options="header"] |
| 27 | +|=== |
| 28 | +| Brand | Source language | `--face` | Status |
| 29 | + |
| 30 | +| (canonical) | AffineScript itself | `canonical` | Stable (default) |
| 31 | +| RattleScript | Python | `rattle` | Shipped |
| 32 | +| JaffaScript | JavaScript | `jaffa` | Shipped |
| 33 | +| ProperScript | TypeScript (+ AS3 preset) | `proper` | *New — planned* |
| 34 | +| RealScript | ReScript | `real` | *New — planned* |
| 35 | +| CafeScripto | CoffeeScript | `cafe` | Shipped |
| 36 | +| LucidScript | PureScript / Haskell | `lucid` | Shipped |
| 37 | +| PseudoScript | pseudocode | `pseudo` | Shipped |
| 38 | +|=== |
| 39 | + |
| 40 | +=== The naming decoder ring |
| 41 | + |
| 42 | +Each brand name is a lateral pun on its source language — either a *synonym* |
| 43 | +(`cafe`≈coffee, `lucid`≈pure) or an *associative leap* (`rattle`→snake→Python, |
| 44 | +`jaffa`→Java). The two new ECMAScript-descended faces form a coherent trio with |
| 45 | +JaffaScript: |
| 46 | + |
| 47 | +* *JaffaScript* — raw JavaScript, untyped. |
| 48 | +* *ProperScript* — TypeScript, now typed _properly_. TypeScript's types are |
| 49 | + structural and erased; routed through the affine core + typed-wasm they become |
| 50 | + sound. "The TypeScript that is finally typed properly." |
| 51 | +* *RealScript* — ReScript, whose ML-family types were _real_ (sound, |
| 52 | + non-nullable, no `any`) all along. The name is a homophone tribute |
| 53 | + (Re→Real), not a corrective. |
| 54 | + |
| 55 | +ProperScript and RealScript are the "typed twins" of the JaffaScript family. |
| 56 | + |
| 57 | +== Binding model: one extension, a pragma per face |
| 58 | + |
| 59 | +*Ratified (supersedes the `.pyaff/.jsaff/...` prose in older docs).* Every face |
| 60 | +shares the canonical `.affine` file extension. The active face is selected, in |
| 61 | +priority order: |
| 62 | + |
| 63 | +. an explicit `--face <name>` flag (always wins), then |
| 64 | +. a first-line `face:` pragma in the source, then |
| 65 | +. canonical (default). |
| 66 | + |
| 67 | +The legacy per-face extensions (`.pyaff`, `.jsaff`, `.pseudoaff`, …) and the |
| 68 | +`--face pyaff|jsaff|pseudoaff` spellings are *deprecated* (the compiler already |
| 69 | +emits a deprecation warning for them, `bin/main.ml`). New material MUST use |
| 70 | +`.affine` + pragma. |
| 71 | + |
| 72 | +.Pragma grammar |
| 73 | +The pragma is the file's first meaningful line, written in that face's own comment |
| 74 | +syntax, of the form `<comment> face: <long-name>`: |
| 75 | + |
| 76 | +[source] |
| 77 | +---- |
| 78 | +# face: rattlescript (Python-style comment) |
| 79 | +// face: jaffascript (JS-style comment) |
| 80 | +// face: properscript |
| 81 | +// face: realscript |
| 82 | +-- face: pseudoscript (or lucidscript) |
| 83 | +# face: cafescripto |
| 84 | +---- |
| 85 | + |
| 86 | +== Face identity table (normative) |
| 87 | + |
| 88 | +[cols="1,1,1,2,1", options="header"] |
| 89 | +|=== |
| 90 | +| Brand | `--face` short | pragma long | transformer + `Face` variant | brand repo |
| 91 | + |
| 92 | +| RattleScript | `rattle`, `python` | `rattlescript` | `lib/python_face.ml` · `Face.Python` | `hyperpolymath/rattlescript` |
| 93 | +| JaffaScript | `jaffa`, `js`, `javascript` | `jaffascript` | `lib/js_face.ml` · `Face.Js` | `hyperpolymath/jaffascript` |
| 94 | +| ProperScript | `proper`, `typescript` | `properscript` | `lib/proper_face.ml`* · `Face.Proper`* | `hyperpolymath/properscript`* |
| 95 | +| RealScript | `real`, `rescript` | `realscript` | `lib/real_face.ml`* · `Face.Real`* | `hyperpolymath/realscript`* |
| 96 | +| CafeScripto | `cafe`, `coffee` | `cafescripto` | `lib/cafe_face.ml` · `Face.Cafe` | `hyperpolymath/cafescripto` |
| 97 | +| LucidScript | `lucid` | `lucidscript` | `lib/lucid_face.ml` · `Face.Lucid` | `hyperpolymath/lucidscript` |
| 98 | +| PseudoScript | `pseudo`, `pseudocode` | `pseudoscript` | `lib/pseudocode_face.ml` · `Face.Pseudocode` | `hyperpolymath/pseudoscript` |
| 99 | +|=== |
| 100 | + |
| 101 | +`*` = does not exist yet; created under Part 2 of the plan. The CLI already |
| 102 | +accepts the brand short aliases `rattle`/`jaffa` alongside the canonical |
| 103 | +`python`/`js` names; it MUST be extended to accept `proper`/`real` (and |
| 104 | +`typescript`/`rescript`) once those `Face` variants land. Note: `bin/main.ml` |
| 105 | +already carries partial `is_rescript` error-vocabulary plumbing that RealScript |
| 106 | +will absorb. |
| 107 | + |
| 108 | +== Shim protocol (hardened) |
| 109 | + |
| 110 | +Each brand repo ships a `bin/<short>` shim carrying *no compiler* — it locates the |
| 111 | +canonical `affinescript` binary and injects `--face <short>`. The shim MUST resolve |
| 112 | +the binary in this order and fail with an actionable message if none is found: |
| 113 | + |
| 114 | +. `--affinescript <path>` / `$AFFINESCRIPT`, else |
| 115 | +. `affinescript` on `$PATH`, else |
| 116 | +. the sibling canonical checkout |
| 117 | + (`../γ-affinescript/affinescript/_build/default/bin/main.exe`), else |
| 118 | +. print: `affinescript not found — install via opam, or set $AFFINESCRIPT`. |
| 119 | + |
| 120 | +This replaces the current bare-`affinescript` shims, which fail with a raw |
| 121 | +`command not found`. |
| 122 | + |
| 123 | +== Canonical pin |
| 124 | + |
| 125 | +Every brand repo pins the canonical compiler as a *git submodule* (the mechanism |
| 126 | +RattleScript already uses) and bumps it via `just update-affinescript`. No brand |
| 127 | +repo floats against "whatever is on `$PATH`". All language, face, and bug-fix |
| 128 | +changes flow through a single submodule-pointer bump, so a face can never silently |
| 129 | +drift from the compiler it targets. |
| 130 | + |
| 131 | +[[actionscript]] |
| 132 | +== ActionScript: welcomed in, but not a standalone face |
| 133 | + |
| 134 | +*Decision (2026-07-08): ActionScript is NOT a standalone face.* It is folded into |
| 135 | +ProperScript as a migration preset. This retires the "ActionScript-face (strategic |
| 136 | +priority)" roadmap item in `docs/specs/faces.adoc §194`. |
| 137 | + |
| 138 | +*Why not a face.* A face must serve a distinct, living surface community. AS3 fails |
| 139 | +both halves: its surface is an ECMAScript-4 dialect (`var x:Type`, |
| 140 | +`function f():R`, classes, packages) that is essentially *JaffaScript + |
| 141 | +ProperScript already*, and its runtime (Flash) died in 2020, so there is no |
| 142 | +community writing new AS3 to rehome. Its `-Script` is native (like TypeScript / |
| 143 | +JavaScript), the tell that it is a product name, not a face-brand. |
| 144 | + |
| 145 | +*But the door is open.* Flash / Flex / AIR developers were displaced by platform |
| 146 | +death, not language quality, and they are genuinely welcome. Their on-ramp is |
| 147 | +*ProperScript plus an AS3-idioms migration preset* — `extends` → traits, |
| 148 | +`package { … }` stripped, `trace(x)` → `IO.println(x)`, `Number`/`int` → `Float`/ |
| 149 | +`Int`, `Array.<T>`/`Vector.<T>` → `Array[T]`, `null` → `()`. The mapping already |
| 150 | +drafted in `faces.adoc §194` is retargeted from a hypothetical `action_face` onto |
| 151 | +this preset. |
| 152 | + |
| 153 | +*Honest scope.* A dedicated `action_face` with its own brand repo is deferred |
| 154 | +indefinitely. This is a one-person, unfunded project; the resource-responsible |
| 155 | +choice is a preset that reuses the ProperScript machinery, not a whole face we |
| 156 | +cannot maintain. It will be reassessed only if a funded AS3 cohort materialises. |
| 157 | + |
| 158 | +== Conformance obligations |
| 159 | + |
| 160 | +A face is _conformant_ only when all of the following are green: |
| 161 | + |
| 162 | +. *Snapshot* — `tools/run_face_transformer_tests.sh` (the face's |
| 163 | + `tests/faces/hello-<x>.expected.txt` matches its `preview-<x>` output). |
| 164 | +. *Round-trip parse* — the face's examples parse via the normal |
| 165 | + pragma-auto-detecting pipeline. |
| 166 | +. *Same-cube* — `hyperpolymath/invariant-path/scripts/verify-same-cube.sh` |
| 167 | + places the face in the *single* canonical wasm equivalence class. |
| 168 | ++ |
| 169 | +[IMPORTANT] |
| 170 | +This gate is *currently RED (exit 1)*: the six existing faces split into two |
| 171 | +wasm classes — `{lucid, pseudo, rattle}` (tail-expression) vs |
| 172 | +`{cafe, canonical, jaffa}` (statement) — because terminator-free faces guess a |
| 173 | +unit-typed trailing call as a tail expression and drop the `;` |
| 174 | +(canonical issue *#601*). Closing it (Milestone B) is a plan deliverable, via a |
| 175 | +post-typecheck tail-normalisation pass, so the text transformer's guess no longer |
| 176 | +reaches codegen. |
| 177 | + |
| 178 | +Same-cube MUST be promoted from an external script to a *required CI check* in the |
| 179 | +canonical repo, and each brand repo MUST run a per-face conformance gate against |
| 180 | +its pinned submodule. |
| 181 | + |
| 182 | +== Proof obligations |
| 183 | + |
| 184 | +The canonical core is being proved against the real `lib/ast.ml` / `lib/wasm.ml` |
| 185 | +denotation (the "real lift"). The faces must ride that same rigour. Current status |
| 186 | +(`docs/PROOF-NEEDS.adoc`): |
| 187 | + |
| 188 | +[cols="1,3,1", options="header"] |
| 189 | +|=== |
| 190 | +| Obligation | Statement | Status |
| 191 | + |
| 192 | +| F-1 | every `lib/<face>_face.ml` transform preserves the typed-wasm denotation | *partial* (toy model only) |
| 193 | +| F-2 | tail/statement normalisation preserves denotation | *partial* (unit-tail case, `SameCube.agda`) |
| 194 | +| F-6 | preview round-trip totality (transform always yields parseable canonical) | *absent* |
| 195 | +| F-7 | face/canon confluence (all faces of one program reach one canonical AST) | *absent* |
| 196 | +|=== |
| 197 | + |
| 198 | +Sealing the connection "for real" (Milestone D) means moving F-1 to per-face |
| 199 | +preservation against the real AST, generalising F-2 to the normalisation pass of |
| 200 | +#601, and discharging F-6/F-7. Until then the real transformers are grounded by |
| 201 | +tests only; this table is the honest statement of that gap. |
| 202 | + |
| 203 | +== Open implementation tasks (tracked by the plan) |
| 204 | + |
| 205 | +* *P1.2* — add `proper`/`real` (+ `typescript`/`rescript`) to the CLI `--face` |
| 206 | + alias table and `Face` variant type. |
| 207 | +* *P2 (js_face split)* — JaffaScript narrows to plain JavaScript; ProperScript |
| 208 | + gets its own transformer + TS-idiom error vocabulary (interfaces→traits, `type`, |
| 209 | + `as`, generics→type params, `| null` unions). `lib/js_face.ml`'s current |
| 210 | + "JS/TS" double duty is split. |
| 211 | +* *Create brand repos* `properscript` and `realscript` (brand-surface only, per |
| 212 | + this contract). |
| 213 | +* *Span fidelity* (ADR-010 §4) — error spans map back to original face source, |
| 214 | + not transformed canonical text. |
| 215 | +* *Path* — `lucidscript` + `cafescripto` currently live under `γ-languages/`; |
| 216 | + ratify or relocate to match the flat `repos/` siblings. |
| 217 | +* *AFFIRMATION* — absent from all brand repos; add if RSR-required. |
| 218 | + |
| 219 | +== Provenance |
| 220 | + |
| 221 | +* Roster + names ratified in session 2026-07-08 (RattleScript/JaffaScript/ |
| 222 | + CafeScripto/LucidScript/PseudoScript pre-existing; ProperScript, RealScript, and |
| 223 | + the ActionScript fold-in decided this session). |
| 224 | +* Supersedes: `.pyaff`-style extensions; the ActionScript standalone-face roadmap |
| 225 | + item (`faces.adoc §194`). |
| 226 | +* Companion design doc: `docs/specs/faces.adoc` (ADR-010). |
0 commit comments