Skip to content

Commit 74024c6

Browse files
hyperpolymathclaude
andcommitted
feat(faces): add Lucid + Cafe faces, unify on .affine + face pragma
Adds two new faces — LucidScript (PureScript-style) and CafeScripto (CoffeeScript-style) — alongside the established Canonical / Rattle / Jaffa / Pseudo. Every face shares the .affine extension and is selected by an in-file `face:` pragma (or --face flag); deprecated per-face extensions still work with a stderr warning. - lib/face_pragma.ml: pragma detector covering all six brand names and their generic aliases. - lib/lucid_face.ml, lib/cafe_face.ml: text transformers at parity depth with python_face / js_face / pseudocode_face. - lib/face.ml: Lucid + Cafe variants plus error-vocabulary branches across all six format_*_for_face functions. - bin/main.ml: parse_with_face, face_arg, fmt_file, preview-* cmds extended; resolve_face now does pragma-first dispatch. - examples/faces/, tests/faces/, tools/run_face_transformer_tests.sh: six demo files, snapshot regression test, justfile recipes, and a CI step that catches transformer drift. - README.adoc: face listing now names all six. Known transformer gaps (each face's example sidesteps these and the examples/faces/README.adoc tracks them) are deferred follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 29f1097 commit 74024c6

23 files changed

Lines changed: 2382 additions & 46 deletions

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ jobs:
4040
- name: Run codegen WASM tests
4141
run: opam exec -- ./tools/run_codegen_wasm_tests.sh
4242

43+
- name: Run face-transformer regression tests
44+
run: opam exec -- ./tools/run_face_transformer_tests.sh
45+
4346
- name: Check formatting
4447
run: opam exec -- dune build @fmt
4548

README.adoc

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,16 @@ On top of that core, the language supports _faces_: sugared surface syntaxes tha
7676

7777
A face is not a separate language. It is a different presentation layer over the same core model.
7878

79-
Examples include:
79+
The established faces are:
8080

8181
- *AffineScript* — the canonical face
82-
- *JaffaScript* — a JavaScript-like face
83-
- *RattleScript* — a Python-like face
84-
- *PseudoScript* — a pseudocode-oriented face
82+
- *JaffaScript* — a JavaScript / TypeScript-like face
83+
- *RattleScript* — a Python-like face (also positioned as "Python for the web" via typed-wasm)
84+
- *PseudoScript* — a pseudocode-oriented face for CS pedagogy
85+
- *LucidScript* — a PureScript / Haskell-like face
86+
- *CafeScripto* — a CoffeeScript-like face
87+
88+
Every face shares the canonical `.affine` file extension; the active face is selected by an optional `face:` pragma on the first comment line of the file (e.g. `# face: rattlescript`, `// face: jaffascript`, `-- face: lucidscript`), or by `--face NAME` on the CLI.
8589

8690
This means people can bring familiarity from a language family they already love, while still entering a system with stronger guarantees around ownership, effects, state, and resource usage.
8791

@@ -312,11 +316,15 @@ face syntax -> AffineScript core AST -> type/effect/ownership checks -> ba
312316

313317
Examples:
314318

315-
- JaffaScript lowers JavaScript-like syntax into the AffineScript core
319+
- JaffaScript lowers JavaScript / TypeScript-like syntax into the AffineScript core
316320
- RattleScript lowers Python-like syntax into the same core
317321
- PseudoScript lowers structured pedagogical pseudocode into the same core
322+
- LucidScript lowers PureScript / Haskell-like syntax into the same core
323+
- CafeScripto lowers CoffeeScript-like syntax into the same core
324+
325+
Side-by-side examples for each face live under `examples/faces/`; you can preview the canonical lowering of any file with `affinescript preview-python` / `preview-js` / `preview-pseudocode` / `preview-lucid` / `preview-cafe`.
318326

319-
So the question is not “which face is the real language?”
327+
So the question is not “which face is the real language?”
320328
The answer is: the core semantics are the language; faces are entrances.
321329

322330
== Backends and Targets

bin/main.ml

Lines changed: 193 additions & 39 deletions
Large diffs are not rendered by default.

examples/faces/README.adoc

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3+
4+
= Faces — Different Surfaces, Same Cube
5+
6+
This directory contains the same logical "hello" program written once in each of the six established AffineScript faces. Every file:
7+
8+
* uses the canonical `.affine` extension,
9+
* declares its face with a pragma on the first non-blank comment line (`# face: …`, `// face: …`, or `-- face: …`),
10+
* compiles to the same canonical AST and the same typed-wasm output.
11+
12+
== The six files
13+
14+
[cols="1,2,3"]
15+
|===
16+
| File | Face | Demonstrates
17+
18+
| `hello-canonical.affine`
19+
| AffineScript (canonical)
20+
| The reference syntax — all other faces lower to this shape.
21+
22+
| `hello-rattle.affine`
23+
| RattleScript (Python)
24+
| `def`, indentation blocks, `#` comments, `True`/`False`/`None`, `and`/`or`/`not`, `import a.b`.
25+
26+
| `hello-jaffa.affine`
27+
| JaffaScript (JavaScript / TypeScript)
28+
| `function`, `const`/`let`, `import { x } from "m"`, `null`/`undefined`, `===`/`!==`.
29+
30+
| `hello-pseudo.affine`
31+
| PseudoScript (pseudocode)
32+
| `function … do … end`, `set X to Y`, `output X`, `is`/`is not`, `yes`/`no`, `and`/`or`.
33+
34+
| `hello-lucid.affine`
35+
| LucidScript (PureScript / Haskell)
36+
| `module … where`, dotted imports with `(name)` lists, `name :: Type` signatures, equation-style `f x = …`, `\x -> …` lambdas, `--` comments.
37+
38+
| `hello-cafe.affine`
39+
| CafeScripto (CoffeeScript)
40+
| `#` and `###` comments, `->` / `=>` arrows, `@` shorthand, `unless`/`until`, postfix-if, `Yes`/`No`/`On`/`Off`.
41+
|===
42+
43+
== See the lowering
44+
45+
The `preview-*` debug commands run a face through its transformer and print the canonical AffineScript text it produces — no parsing, no type-checking, just the string-to-string transform.
46+
47+
[source,bash]
48+
----
49+
# Show how RattleScript lowers
50+
affinescript preview-python examples/faces/hello-rattle.affine
51+
52+
# Show how JaffaScript lowers
53+
affinescript preview-js examples/faces/hello-jaffa.affine
54+
55+
# Show how PseudoScript lowers
56+
affinescript preview-pseudocode examples/faces/hello-pseudo.affine
57+
58+
# Show how LucidScript lowers
59+
affinescript preview-lucid examples/faces/hello-lucid.affine
60+
61+
# Show how CafeScripto lowers
62+
affinescript preview-cafe examples/faces/hello-cafe.affine
63+
----
64+
65+
The output of each `preview-*` command is the same logical program in canonical AffineScript syntax, modulo whitespace and comment placement. Diffing two previews is a good way to see what each transformer actually does.
66+
67+
== Parse-only verification
68+
69+
To confirm a face file lowers to syntactically valid canonical AffineScript:
70+
71+
[source,bash]
72+
----
73+
affinescript parse examples/faces/hello-rattle.affine
74+
----
75+
76+
(The face is auto-detected from the pragma; pass `--face NAME` to force a specific face.)
77+
78+
== Caveats
79+
80+
* These examples demonstrate *surface syntax*. They are written to round-trip through their respective transformers (preview → canonical → parse) so the regression test in `tests/faces/` can catch drift, not to be full type-correct, runnable programs. That depends on what's in scope from `stdlib/`.
81+
* Span fidelity: error messages from non-canonical faces refer to the canonical-text form (post-transform), not the original face source. This is a known limitation of all face transformers, including the original three.
82+
* The examples deliberately avoid features each transformer can't yet handle. Known transformer gaps that the simpler examples sidestep:
83+
+
84+
[cols="1,3"]
85+
|===
86+
| Face | Pending transformer work
87+
88+
| Python (Rattle)
89+
| Bare assignment `x = y` is not yet auto-lifted to `let x = y` (the example uses explicit `let`); `import a.b` lowering does not produce the canonical `use a::b::{…};` brace form.
90+
91+
| JS (Jaffa)
92+
| `import { x } from "module";` lowering has a string-stripping bug when the trailing `;` is present (the example avoids `import` entirely for now).
93+
94+
| Pseudocode (Pseudo)
95+
| No automatic `;` between non-tail statements — examples use single-statement function bodies. Token substitutions can bleed into comment text (e.g. `or` → `\|\|` inside a `//` comment). `output X` lowers to `IO.println(X)` rather than canonical `println(X)`.
96+
97+
| Lucid (PureScript)
98+
| Haskell-style currying calls `f x` are NOT converted to canonical `f(x)` — the example uses canonical paren syntax. Multi-clause definitions, `do`-notation, and `where`-block hoisting are deferred to AST-level rewrites.
99+
100+
| Cafe (CoffeeScript)
101+
| List comprehensions, no-paren calls, splat / destructuring, and string interpolation are deferred to AST-level rewrites.
102+
|===
103+
+
104+
These gaps are tracked as follow-up work; the regression test in this directory will catch drift in *what does work today*, and additional examples can be added once the corresponding transformer feature lands.
105+
106+
== Adding a new face
107+
108+
The architecture (ADR-010) makes adding a face mechanical:
109+
110+
1. Add a variant to `lib/face.ml`'s `face` type.
111+
2. Write a `lib/<name>_face.ml` text transformer (model on `python_face.ml`, `js_face.ml`, `pseudocode_face.ml`, `lucid_face.ml`, or `cafe_face.ml`).
112+
3. Add error-vocabulary branches in `lib/face.ml`'s six `format_*_for_face` functions.
113+
4. Register in `bin/main.ml`: `parse_with_face` arm, `face_arg` enum entry, optional `preview-*` debug command.
114+
5. Add name aliases to `lib/face_pragma.ml`.
115+
116+
No compiler internals change: the transformer outputs canonical text, which the existing pipeline (resolve → typecheck → borrow → quantity → codegen → tw_verify) processes unchanged.

examples/faces/hello-cafe.affine

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3+
#
4+
# CafeScripto face. Distinctive features exercised: `#` line comments,
5+
# `###` block comments, paren-arrow `(args) -> body`, bare-arrow `-> body`,
6+
# Yes/No/On/Off literal aliases.
7+
# face: cafescripto
8+
9+
###
10+
All other CoffeeScript surface conveniences — postfix-if, @ shorthand,
11+
`unless`/`until`, list comprehensions, no-paren calls — are documented
12+
in examples/faces/README.adoc with their current support status.
13+
###
14+
15+
effect IO {
16+
fn println(s: String) -> ();
17+
}
18+
19+
fn main() -{IO}-> () {
20+
let greeting = "Hello, CafeScripto!";
21+
let ready = Yes;
22+
println(greeting);
23+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3+
//
4+
// Canonical face — the AffineScript reference syntax. All other faces in
5+
// this directory lower to this same shape via their respective transformers.
6+
// face: canonical
7+
8+
effect IO {
9+
fn println(s: String) -> ();
10+
}
11+
12+
fn main() -{IO}-> () {
13+
let greeting = "Hello, AffineScript!";
14+
println(greeting);
15+
}

examples/faces/hello-jaffa.affine

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3+
//
4+
// JaffaScript face. Distinctive features exercised: `function` keyword,
5+
// `const`/`let` bindings (let lowers to let mut), brace-delimited
6+
// blocks, `===` equality.
7+
// face: jaffascript
8+
9+
effect IO {
10+
fn println(s: String) -> ();
11+
}
12+
13+
function main() -{IO}-> () {
14+
const greeting = "Hello, JaffaScript!";
15+
println(greeting);
16+
}

examples/faces/hello-lucid.affine

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-- SPDX-License-Identifier: AGPL-3.0-or-later
2+
-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3+
--
4+
-- LucidScript face. Distinctive features exercised: module declaration
5+
-- with `where`, type signatures kept as comments, equation-style
6+
-- function definitions, `--` line comments. (The unit-parameter idiom
7+
-- `main () = …` lowers to canonical `fn main() { … }`. Function
8+
-- application uses canonical paren syntax `f(x)` rather than Haskell
9+
-- currying `f x` — see examples/faces/README.adoc.)
10+
-- face: lucidscript
11+
12+
module Hello where
13+
14+
effect IO {
15+
fn println(s: String) -> ();
16+
}
17+
18+
main :: -{IO}-> ()
19+
main () = println("Hello, LucidScript!")

examples/faces/hello-pseudo.affine

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- SPDX-License-Identifier: AGPL-3.0-or-later
2+
-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3+
--
4+
-- PseudoScript face. Distinctive features exercised:
5+
-- function ... do ... end blocks, `--` Haskell/SQL-style comments.
6+
-- (Note: pseudocode-face does not yet auto-insert statement separators,
7+
-- so this minimal demo keeps each function body to a single expression.
8+
-- See examples/faces/README.adoc for a list of pending transformer gaps.)
9+
-- face: pseudoscript
10+
11+
effect IO {
12+
fn println(s: String) -> ();
13+
}
14+
15+
function main() -{IO}-> () do
16+
println("Hello, PseudoScript!")
17+
end

examples/faces/hello-rattle.affine

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# SPDX-License-Identifier: AGPL-3.0-or-later
2+
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
3+
#
4+
# RattleScript face. Distinctive features exercised: `def` keyword,
5+
# indentation as block delimiter, `#` line comments, `True/False/None`
6+
# literals, `and/or/not` boolean keywords, `:` block opener.
7+
# face: rattlescript
8+
9+
effect IO:
10+
fn println(s: String) -> ();
11+
12+
def main() -{IO}-> ():
13+
let greeting = "Hello, RattleScript!"
14+
println(greeting)

0 commit comments

Comments
 (0)