This document describes the face architecture (ADR-010) for the AffineScript compiler and toolchain.
AffineScript has a canonical surface syntax. A face is an alternative surface presentation — a different syntax and error vocabulary — layered on top of the compiler without changing any compiler internals.
The canonical use-case is the Python face: developers who write Python can
write def f(x: Int) → Int: with indentation-delimited blocks, True/False,
None, and/or, etc. and receive compiler errors in Python-idiomatic terms
("single-use variable" rather than "linear binding", "Name not found" rather
than "UnboundVariable").
source text (face-specific)
↓
face preprocessor ← lib/python_face.ml (text → canonical text)
↓
standard lex + parse ← lib/lexer.ml / lib/parser.mly
↓
type-checker / QTT ← lib/typecheck.ml / lib/quantity.ml
↓
canonical error term ← face-agnostic; carries structured fields
↓
face-aware formatter ← lib/face.ml (error × face → display string)
↓
terminal / LSP / IDEThe compiler is face-agnostic throughout. The two face-aware layers are:
-
lib/python_face.ml— source-level text preprocessor -
lib/face.ml— error formatter
| Face | CLI flag | File extension | Status |
|---|---|---|---|
Canonical |
|
|
Stable |
Python |
|
|
Stable (ADR-010) |
JS |
|
|
Beta (2026-04-11) |
Pseudocode |
|
|
Beta (2026-04-11) |
Lucid (PureScript / Haskell) |
|
|
Beta (2026-06) |
Cafe (CoffeeScript) |
|
|
Beta (2026-06) |
Each non-canonical face has a brand-surface repo — examples, community docs,
migration guides, and a bin/<face> shim that injects --face. The compiler,
type checker, borrow checker, and codegen live only here in affinescript; the
brand repos carry no compiler.
| Face | Brand repo | Transformer |
|---|---|---|
RattleScript (Python) |
|
|
JaffaScript (JS / TS) |
|
|
PseudoScript (pseudocode) |
|
|
LucidScript (PureScript) |
|
|
CafeScripto (CoffeeScript) |
|
|
The "different faces, same cube" claim is grounded by
hyperpolymath/invariant-path (the faces profile + scripts/verify-same-cube.sh),
which compiles one program written in every face to typed-wasm and compares the
modules. Per-face snapshot stability is covered separately by
tools/run_face_transformer_tests.sh (currently 6/6 green).
Known transformer-consistency item (grounded 2026-06-18): on the greet
corpus the six faces compile to two wasm modules — {canonical, jaffa, cafe}
vs {rattle, pseudo, lucid} — because the transformers disagree on
trailing-statement lowering (statement { println(x); } vs tail-expression
{ println(x) }). The programs are observationally identical (same output,
same unit return) but the wasm is not byte-identical. Making the transformers
agree on trailing-statement lowering would yield byte-level same-cube.
ActionScript is no longer a roadmap face. As of 2026-07-08 it has been folded
into ProperScript (the TypeScript face) as an AS3-idioms migration preset — see
docs/specs/FACE-CONTRACT.adoc, "ActionScript: welcomed in, but not a standalone
face". The surface mapping retained in the section below now describes that
preset, not a standalone action_face. No standalone faces remain on the
roadmap; the two new faces (ProperScript / TypeScript, RealScript / ReScript) are
tracked as active planned work in the Face Contract, not here.
|
Note
|
This face is now established (lib/cafe_face.ml, --face cafe; see the
Active Faces table and hyperpolymath/cafescripto). The design notes below are
retained as historical reference. No standalone faces remain on the roadmap.
|
Rationale: CoffeeScript has a loyal displaced community that never loved
JavaScript and were forced away when the ecosystem moved on. Their syntax
preferences (significant whitespace, → / ⇒, is/isnt/unless,
@ for self, do, list comprehensions) are coherent and well-documented.
A CoffeeScript face would offer that community a new home: their familiar
syntax, but with a sound type system, affine resource semantics, and typed
WASM output.
Rough surface mapping (to be designed):
| CoffeeScript surface | Canonical AffineScript |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Significant whitespace |
blocks (like Python-face) |
|
|
Design questions before implementing:
-
How do CoffeeScript string interpolation (
"#{x}") map to AffineScript? -
List comprehensions — do we support them or emit a hint?
-
Splats (
args…) — map to variadic or array? -
Prototype chain (
::) — not applicable; needs a hint pointing to traits.
File: lib/coffee_face.ml, --face coffee / --face coffeescript
|
Note
|
Decision 2026-07-08 (docs/specs/FACE-CONTRACT.adoc): ActionScript is not
a standalone face. AS3’s surface is an ECMAScript-4 dialect essentially covered by
JaffaScript (JavaScript) + ProperScript (TypeScript), and its runtime (Flash) is
dead, so there is no living community to rehome into a dedicated face. Flash /
Flex / AIR developers are genuinely welcome — their on-ramp is ProperScript
(--face proper) plus the AS3-idioms preset described by the mapping below. A
dedicated action_face is deferred indefinitely as a resource-responsible choice
for a solo, unfunded project.
|
Rationale (historical): ActionScript 3 was a capable, statically-typed OO language with a loyal community (Flash game developers, Flex/AIR developers). It was killed by platform death (Flash end-of-life), not by language quality. AS3 developers are comfortable with explicit types, classes, and access modifiers — all of which map naturally to AffineScript concepts.
Rough surface mapping (to be designed):
| ActionScript surface | Canonical AffineScript |
|---|---|
|
|
|
|
|
(access control — AffineScript is module-scoped; emit hint) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
module-level (stripped) |
|
(already valid) |
Design questions before implementing:
-
AS3 inheritance (
extends) — map to trait bounds or emit a migration hint? -
Interfaces (
implements) — map to traits? -
override/super— no direct equivalent; design needed. -
Event model (
addEventListener) — emit a hint pointing to effects. -
Vector.<T>vsArray.<T>— both map toArray[T]?
Realised as: an AS3-idioms preset / migration guide under ProperScript
(--face proper), not a standalone lib/action_face.ml.
Python_face.transform_source : string → string converts Python-style
AffineScript source text to canonical AffineScript before lex and parse.
Surface mappings:
| Python surface | Canonical AffineScript |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
INDENT |
(block already opened by preceding |
DEDENT |
|
mid-block statement |
|
tail expression |
|
Tail-position detection: The preprocessor detects when a statement is the
last expression in its block (the next meaningful line has a smaller indent, or
EOF) and suppresses the trailing ;. Without this, every block would yield
() instead of the last expression’s value, breaking AffineScript’s
expression-oriented block semantics.
Limitation — span fidelity: Error spans currently refer to positions in
the transformed canonical text, not the original Python-face source. A span
remapping table (tracking line/column shifts introduced by {/} injection) is
planned as a follow-up (see docs/specs/SETTLED-DECISIONS.adoc, ADR-010 §4).
Face.format_type_error : face → Typecheck.type_error → string (and its
counterparts for quantity_error, unify_error, resolve_error) map
canonical error terms to face-appropriate display strings.
Python-face vocabulary selections:
| Canonical term | Python-face display |
|---|---|
|
"Ownership error: single-use variable 'x' must be used exactly once, but was never used" |
|
"Ownership error: single-use variable 'x' can only be used once, but was used more than once" |
|
"Ownership error: erased variable 'x' was declared as compile-time-only and cannot appear at runtime" |
|
"Name not found: 'v' — hint: check spelling or add a def declaration" |
|
"Type error: expected T but got U" |
|
"if/else type mismatch: both branches must return the same type" |
|
"Name not found: 'x' — hint: define it with def or let" |
|
"Type not found: 'T' — hint: define it with class or type" |
|
"Module not found: 'M' — hint: use import M" |
|
rendered as |
|
rendered as |
# Canonical face (default)
affinescript check foo.affine
# Python face
affinescript check --face python foo.pyaff
affinescript eval --face python foo.pyaff
affinescript compile --face python foo.pyaff
# Debug: preview what the Python preprocessor produces
affinescript preview-python-transform foo.pyaff-
Add a variant to
type faceinlib/face.ml. -
Add a preprocessor in a new
lib/<name>_face.ml(if syntax differs). -
Add branches to each
format_*_for_facefunction inlib/face.ml. -
Wire the face into
bin/main.ml(face_arg,parse_with_face). -
Add the file extension to
lib/dunemodules list. -
Add E2E tests in
test/test_e2e.ml. -
Update this document.
No compiler internals change.
End-to-end Python-face tests live in test/test_e2e.ml under the
"E2E Python-Face" section:
-
def → fntransformation -
if/elif/elsechain -
Keyword substitution (
True,False,None,and,or,not) -
Three-function fixture parses without error
-
Transform preview output correctness
Seam check (items 1 + 2): affinescript check --face python on a file with a
doubly-used @linear variable correctly produces the Python-face error message
"Ownership error: single-use variable 'x' can only be used once…" confirming
the full pipeline from .pyaff → text transform → canonical parse → typecheck
→ face-aware error formatter.