|
| 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. |
0 commit comments