Skip to content

Commit a3eda45

Browse files
docs(faces): FACE-CONTRACT + retire ActionScript standalone face (#685)
## Milestone A keystone — the normative face↔canonical binding Adds `docs/specs/FACE-CONTRACT.adoc`, the single normative source for how a language *face* binds to the canonical compiler, and retires the ActionScript standalone-face roadmap item in `faces.adoc §194`. ### Roster ratified (session 2026-07-08/09) | Brand | Source | `--face` | Status | |---|---|---|---| | RattleScript | Python | `rattle` | shipped | | JaffaScript | JavaScript | `jaffa` | shipped | | **ProperScript** | TypeScript (+ AS3 preset) | `proper` | new | | **RealScript** | ReScript | `real` | new | | CafeScripto | CoffeeScript | `cafe` | shipped | | LucidScript | PureScript | `lucid` | shipped | | PseudoScript | pseudocode | `pseudo` | shipped | ### What the contract fixes - `.affine` + `face:` pragma model (deprecates `.pyaff/.jsaff/...`). - CLI name table, hardened shim protocol, submodule-pin mechanism. - Conformance gate: snapshot + same-cube (**currently RED**, issue #601). - Proof obligations F-1/F-2/F-6/F-7 stated honestly (real transformers grounded by tests only). ### ActionScript Folded into ProperScript as an AS3-idioms migration preset — welcomed in, not a standalone face (resource-responsible for a solo/unfunded project). ### Deferred to Part 2 (not this PR) `--face proper/real` CLI wiring + the `js_face` split need new `Face` variants + transformers; adding the alias strings now would break the build. Recorded in the contract's open-tasks section. Draft pending owner review of the roster + names. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4b36de2 commit a3eda45

2 files changed

Lines changed: 247 additions & 8 deletions

File tree

docs/specs/FACE-CONTRACT.adoc

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
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).

docs/specs/faces.adoc

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,19 @@ agree on trailing-statement lowering would yield byte-level same-cube.
120120

121121
== Roadmap Faces
122122

123-
ActionScript-face remains on the roadmap (CoffeeScript has since shipped as
124-
CafeScripto — see Active Faces above). The face architecture makes
125-
implementation cheap once the surface mapping is settled; the bottleneck is
126-
design, not code.
123+
ActionScript is *no longer a roadmap face*. As of 2026-07-08 it has been folded
124+
into ProperScript (the TypeScript face) as an AS3-idioms migration preset — see
125+
`docs/specs/FACE-CONTRACT.adoc`, "ActionScript: welcomed in, but not a standalone
126+
face". The surface mapping retained in the section below now describes that
127+
preset, not a standalone `action_face`. No standalone faces remain on the
128+
roadmap; the two new faces (ProperScript / TypeScript, RealScript / ReScript) are
129+
tracked as active planned work in the Face Contract, not here.
127130

128131
=== CoffeeScript-face — SHIPPED as CafeScripto
129132

130133
NOTE: This face is now *established* (`lib/cafe_face.ml`, `--face cafe`; see the
131134
Active Faces table and `hyperpolymath/cafescripto`). The design notes below are
132-
retained as historical reference; only ActionScript-face remains on the roadmap.
135+
retained as historical reference. No standalone faces remain on the roadmap.
133136

134137
*Rationale:* CoffeeScript has a loyal displaced community that never loved
135138
JavaScript and were forced away when the ecosystem moved on. Their syntax
@@ -191,9 +194,18 @@ WASM output.
191194

192195
*File:* `lib/coffee_face.ml`, `--face coffee` / `--face coffeescript`
193196

194-
=== ActionScript-face (strategic priority)
197+
=== ActionScript — ProperScript AS3 migration preset (not a standalone face)
195198

196-
*Rationale:* ActionScript 3 was a capable, statically-typed OO language
199+
NOTE: *Decision 2026-07-08* (`docs/specs/FACE-CONTRACT.adoc`): ActionScript is not
200+
a standalone face. AS3's surface is an ECMAScript-4 dialect essentially covered by
201+
JaffaScript (JavaScript) + ProperScript (TypeScript), and its runtime (Flash) is
202+
dead, so there is no living community to rehome into a dedicated face. Flash /
203+
Flex / AIR developers are genuinely welcome — their on-ramp is ProperScript
204+
(`--face proper`) plus the AS3-idioms preset described by the mapping below. A
205+
dedicated `action_face` is deferred indefinitely as a resource-responsible choice
206+
for a solo, unfunded project.
207+
208+
*Rationale (historical):* ActionScript 3 was a capable, statically-typed OO language
197209
with a loyal community (Flash game developers, Flex/AIR developers). It was
198210
killed by platform death (Flash end-of-life), not by language quality.
199211
AS3 developers are comfortable with explicit types, classes, and access
@@ -253,7 +265,8 @@ modifiers — all of which map naturally to AffineScript concepts.
253265
- Event model (`addEventListener`) — emit a hint pointing to effects.
254266
- `Vector.<T>` vs `Array.<T>` — both map to `Array[T]`?
255267

256-
*File:* `lib/action_face.ml`, `--face action` / `--face actionscript`
268+
*Realised as:* an AS3-idioms preset / migration guide under ProperScript
269+
(`--face proper`), not a standalone `lib/action_face.ml`.
257270

258271
== Python Face Details
259272

0 commit comments

Comments
 (0)