Skip to content

Latest commit

 

History

History
448 lines (327 loc) · 12.6 KB

File metadata and controls

448 lines (327 loc) · 12.6 KB

AffineScript Faces — Design & Implementation Reference

Motivation

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").

Architecture (ADR-010 §2)

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 / IDE

The compiler is face-agnostic throughout. The two face-aware layers are:

  1. lib/python_face.ml — source-level text preprocessor

  2. lib/face.ml — error formatter

Active Faces

Face CLI flag File extension Status

Canonical

--face canonical (default)

.affine

Stable

Python

--face python

.pyaff

Stable (ADR-010)

JS

--face js or --face javascript

.jsaff

Beta (2026-04-11)

Pseudocode

--face pseudocode or --face pseudo

.pseudoaff

Beta (2026-04-11)

Lucid (PureScript / Haskell)

--face lucid

.lucidaff

Beta (2026-06)

Cafe (CoffeeScript)

--face cafe or --face coffee

.cafeaff

Beta (2026-06)

Brand-surface repos

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)

hyperpolymath/rattlescript

lib/python_face.ml

JaffaScript (JS / TS)

hyperpolymath/jaffascript

lib/js_face.ml

PseudoScript (pseudocode)

hyperpolymath/pseudoscript

lib/pseudocode_face.ml

LucidScript (PureScript)

hyperpolymath/lucidscript

lib/lucid_face.ml

CafeScripto (CoffeeScript)

hyperpolymath/cafescripto

lib/cafe_face.ml

Same-cube grounding

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.

Roadmap Faces

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.

CoffeeScript-face — SHIPPED as CafeScripto

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

x = expr

let x = expr

f = (x, y) → expr

fn f(x, y) { expr }

f = (x, y) ⇒ expr

fn f(x, y) { expr } (bound form)

unless cond

if !cond

is / isnt

== / !=

and / or / not

&& / || / !

@field

self.field

class Name

type Name

null / undefined

()

yes / no

true / false

Significant whitespace

blocks (like Python-face)

# comment

// comment

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

ActionScript — ProperScript AS3 migration preset (not a standalone face)

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

var x: Type = expr

let x: Type = expr

function f(x: T): R { }

fn f(x: T) → R { }

public/private/protected

(access control — AffineScript is module-scoped; emit hint)

class Name extends Base

type Name (traits for inheritance)

new Name(args)

Name { fields } or constructor fn

null

()

Boolean / int / Number

Bool / Int / Float

String

String (direct)

Array.<T>

Array[T]

trace(expr)

IO.println(expr)

import pkg.Class

use pkg::Class

package { …​ }

module-level (stripped)

// / /* */ comments

(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> vs Array.<T> — both map to Array[T]?

Realised as: an AS3-idioms preset / migration guide under ProperScript (--face proper), not a standalone lib/action_face.ml.

Python Face Details

Source Transform (lib/python_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

def name(…​)

fn name(…​)

True / False

true / false

None

()

and / or

&& / ||

not EXPR

! EXPR

class Name

type Name

pass

()

# comment

// comment

import a.b

use a::b;

from a import b

use a::b;

if cond:

if cond {

else:

} else {

elif cond:

} else if cond {

while cond:

while cond {

for x in e:

for x in e {

match e:

match e {

handle e:

handle e {

INDENT

(block already opened by preceding {)

DEDENT

}

mid-block statement

statement;

tail expression

expression (no ; — preserves return-value semantics)

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).

Error Vocabulary (lib/face.ml)

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

LinearVariableUnused id

"Ownership error: single-use variable 'x' must be used exactly once, but was never used"

LinearVariableUsedMultiple id

"Ownership error: single-use variable 'x' can only be used once, but was used more than once"

ErasedVariableUsed id

"Ownership error: erased variable 'x' was declared as compile-time-only and cannot appear at runtime"

UnboundVariable v

"Name not found: 'v' — hint: check spelling or add a def declaration"

TypeMismatch

"Type error: expected T but got U"

BranchTypeMismatch

"if/else type mismatch: both branches must return the same type"

UndefinedVariable id

"Name not found: 'x' — hint: define it with def or let"

UndefinedType id

"Type not found: 'T' — hint: define it with class or type"

UndefinedModule id

"Module not found: 'M' — hint: use import M"

Unit type

rendered as None

Bool type

rendered as bool

CLI Usage

# 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

Adding a New Face

  1. Add a variant to type face in lib/face.ml.

  2. Add a preprocessor in a new lib/<name>_face.ml (if syntax differs).

  3. Add branches to each format_*_for_face function in lib/face.ml.

  4. Wire the face into bin/main.ml (face_arg, parse_with_face).

  5. Add the file extension to lib/dune modules list.

  6. Add E2E tests in test/test_e2e.ml.

  7. Update this document.

No compiler internals change.

Test Coverage

End-to-end Python-face tests live in test/test_e2e.ml under the "E2E Python-Face" section:

  • def → fn transformation

  • if/elif/else chain

  • 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.