Skip to content

Commit 133bb53

Browse files
hyperpolymathclaude
andcommitted
docs(faces): formalize 6 face-buildout lessons in frontier-programming-practices
Add a "Faces: Frontend Surfaces" section to the Human guide and a parallel (faces) section to AI.a2ml, mirroring the existing (backends) structure. Six rules drawn from the May-2026 Lucid+Cafe buildout and the brand-surface eject: 1. Faces are brand surfaces, not compiler forks (shim pattern recorded) 2. Identity belongs in content, not the filesystem (.affine + pragma) 3. Pure text-to-text transformers need snapshot + round-trip parse 4. Examples are tests, not demonstrations 5. Error-vocabulary cost scales O(faces × error_kinds) 6. Span fidelity has a UX cost — three honest options enumerated Also link the five non-canonical face brand repos from README.adoc and clarify they are brand surfaces, not compiler forks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 82eec92 commit 133bb53

3 files changed

Lines changed: 240 additions & 6 deletions

File tree

README.adoc

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,14 @@ A face is not a separate language. It is a different presentation layer over the
7878

7979
The established faces are:
8080

81-
- *AffineScript* — the canonical 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
81+
- *AffineScript* — the canonical face (this repo)
82+
- link:https://github.com/hyperpolymath/jaffascript[*JaffaScript*] — a JavaScript / TypeScript-like face
83+
- link:https://github.com/hyperpolymath/rattlescript[*RattleScript*] — a Python-like face (also positioned as "Python for the web" via typed-wasm)
84+
- link:https://github.com/hyperpolymath/pseudoscript[*PseudoScript*] — a pseudocode-oriented face for CS pedagogy
85+
- link:https://github.com/hyperpolymath/lucidscript[*LucidScript*] — a PureScript / Haskell-like face
86+
- link:https://github.com/hyperpolymath/cafescripto[*CafeScripto*] — a CoffeeScript-like face
87+
88+
The five non-canonical face repos are *brand surfaces only* — examples, docs, and a thin shim that defaults `--face`. The compiler, type checker, borrow checker, and codegen all live here in `affinescript`.
8789

8890
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.
8991

docs/guides/frontier-programming-practices/AI.a2ml

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,153 @@
166166
(deferred-features "dependent types, tropical types, units of measure, shape-indexed vectors, and any advanced type-level computation live in Typed WASM, not in AffineScript.")
167167
(secondary-backend "Julia — secondary compile target for scientific computing use cases where AffineScript's type safety complements Julia's numeric ecosystem.")
168168
)
169+
170+
(faces
171+
(note "AffineScript is one core, many faces. A face is a sugared surface syntax that lowers to the canonical AST via a single OCaml module in lib/<name>_face.ml. Established faces: canonical, RattleScript (Python-like), JaffaScript (JS/TS-like), PseudoScript (pedagogy), LucidScript (PureScript/Haskell-like), CafeScripto (CoffeeScript-like). The rules below were earned during the May-2026 LucidScript+CafeScripto buildout and the eject of the five non-canonical faces into top-level brand-surface repos at github.com/hyperpolymath/{rattlescript,jaffascript,pseudoscript,lucidscript,cafescripto}.")
172+
173+
(rules
174+
(brand-surface-not-fork
175+
(rule "A face is a transformer module in this repo PLUS a brand repo carrying README, examples, justfile, and a thin shim that defaults --face. The brand repo MUST NOT carry a parser, type checker, borrow checker, codegen, or vendored copy of the canonical compiler. There is exactly one compiler.")
176+
(shim-pattern "#!/usr/bin/env bash\nif [ $# -eq 0 ]; then\n exec affinescript\nfi\nsubcmd=\"$1\"\nshift\nexec affinescript \"$subcmd\" --face <face_name> \"$@\"")
177+
(anti-pattern "Cargo.toml, vendored affinescript/ submodule, separate <face>script binary, per-face parser. Each is a divergence vector that forces re-porting every canonical change to every face."))
178+
179+
(identity-in-content
180+
(rule "Every face uses the canonical .affine extension. Face is selected by a face: pragma on the first comment line (# face:, // face:, -- face:, (* face: *)) or by --face on the CLI. Per-face extensions (.rattle, .pyaff, .jsaff) were tried and abandoned.")
181+
(why "Single extension means build tools, editors, and LSP configure once. File renames don't change semantics. Polyglot directories don't need per-face globs.")
182+
(deprecation-path "Extension dispatch is preserved in resolve_face for migration only; pragma takes precedence and emits a deprecation warning when extension dispatch is used."))
183+
184+
(snapshot-plus-roundtrip
185+
(rule "Every face transformer ships with a snapshot test PLUS a round-trip parse. The snapshot diffs preview-<face> output against tests/faces/<base>.expected.txt; the round-trip parses that output as canonical to confirm grammar legality. Either alone is insufficient; visual review without round-trip parse missed five distinct transformer bugs in the May-2026 buildout.")
186+
(harness "tools/run_face_transformer_tests.sh — modes: check (default), --update (rewrite snapshots), --record-missing (bootstrap)")
187+
(just-recipes "test-faces, test-faces-update, test-faces-record")
188+
(generalization "Any pure text-to-text transformer in the codebase (formatter, codegen text emitters, source rewriters) deserves the same harness. The pattern is not face-specific."))
189+
190+
(examples-are-tests
191+
(rule "Every file under examples/faces/ MUST round-trip clean through the snapshot test. If an example exercises a known transformer gap and won't round-trip, simplify the example to use the documented workaround — do not ship a broken example with a hand-wave.")
192+
(gap-disclosure "Known transformer gaps go in examples/faces/README.adoc under 'Caveats' → 'Known transformer gaps' with one-line workaround. The example file uses the workaround. Users find gaps via docs, not via confused parse errors on their own code.")
193+
(consequence-of-violation "An example that breaks round-trip but ships in the README is a documented falsehood. README and test suite must agree, or one is lying."))
194+
195+
(error-vocabulary-cost
196+
(rule "Adding a face means adding one arm per user-facing error kind to lib/face.ml's format_*_for_face dispatch tables. There is no shortcut. Face-shaped vocabulary is part of the face contract: a Haskell-shaped face must not say 'borrow checker rejected this binding'; a CoffeeScript-shaped face must not reference 'do notation' that the surface lacks.")
197+
(cost-shape "O(faces × error_kinds). Currently 6 faces × ~12 error formatters; growth in either dimension multiplies.")
198+
(drift-symptom "If the dispatch table lags the parser/checker, the face leaks canonical-flavoured errors and the brand promise erodes. Fix is mechanical, not creative — keep the table in sync."))
199+
200+
(span-fidelity-honesty
201+
(rule "A face transformer rewrites user source; subsequent error line numbers refer to the transformed canonical text, not the user's original face source. This is a genuine UX regression. Document it in the face's brand-repo README under 'Caveats'. Do NOT paper over it with phantom span improvements that don't actually translate locations.")
202+
(honest-options
203+
"1. document the regression in the brand-repo README (chosen for all 5 non-canonical faces, May-2026, alpha status)"
204+
"2. have the transformer record a canonical→source span map and have the diagnostic layer translate"
205+
"3. build the face into the parser proper (cmdliner already supports --face), trading parser complexity for span fidelity")
206+
(escalation "When promoting a face from alpha to beta, revisit the choice. The current option-1 stance is honest for alpha; for beta, option-2 is the minimum honest improvement.")))
207+
208+
(when-adding-face-N+1
209+
(step (n 1) (do "create lib/<name>_face.ml; clone the closest existing face module (python_face.ml for indent-based; js_face.ml for brace-based; pseudocode_face.ml for keyword-heavy) and substitute the lowering rules"))
210+
(step (n 2) (do "register the face in lib/face_pragma.ml's alias table and lib/face.ml's Face variant"))
211+
(step (n 3) (do "extend bin/main.ml: parse_with_face arm, fmt_file arm, face_arg cmdliner enum, preview-<name> subcommand"))
212+
(step (n 4) (do "add the face name to lib/dune modules list"))
213+
(step (n 5) (do "build: opam exec -- dune build lib/ bin/"))
214+
(step (n 6) (do "create examples/faces/hello-<name>.affine; iterate until snapshot+roundtrip pass via just test-faces-record then just test-faces"))
215+
(step (n 7) (do "extend lib/face.ml format_*_for_face dispatch tables — one arm per error kind, in face-appropriate vocabulary"))
216+
(step (n 8) (do "ship the brand repo: README.adoc + LICENSE + CONTRIBUTING.md + bin/<face> shim + justfile + examples/hello.affine + .gitignore. NO Cargo.toml, NO vendored compiler, NO separate binary"))
217+
(step (n 9) (do "add link to the brand repo in this repo's README.adoc faces list"))
218+
(step (n 10) (do "document any transformer gap in examples/faces/README.adoc 'Known transformer gaps'; if the gap is severe enough, demote the example to a workaround variant rather than ship a broken round-trip")))
219+
220+
(known-limitations
221+
(limitation
222+
(name "transformer-span-loss")
223+
(since "0.1.0")
224+
(impact "Diagnostic line numbers refer to transformed canonical, not original face source. Acceptable for alpha; must be addressed before beta. See span-fidelity-honesty rule for the three escalation options.")
225+
(recommendation "Document in each brand-repo README until span-map translation lands."))
226+
227+
(limitation
228+
(name "deferred-face-transformations")
229+
(since "0.1.0")
230+
(impact "Several Python/Haskell/Coffee idioms are not yet lowered: multi-clause def-bodies, do-notation, where-blocks, list comprehensions, splats, no-paren calls. These need AST-level rewrites rather than text-level substitution and are deferred to a face-AST-rewriter milestone.")
231+
(recommendation "When sweeping the gap list, skip these categories — they require an AST-rewriter pass that doesn't exist yet, not just sugar additions to the existing text transformer.")
232+
(tracked-in "examples/faces/README.adoc → Known transformer gaps"))
233+
234+
(limitation
235+
(name "no-face-aware-formatter")
236+
(since "0.1.0")
237+
(impact "affinescript fmt produces canonical output regardless of input face. Round-tripping face → canonical → face is not supported; once you canonicalize, you lose the face presentation.")
238+
(recommendation "Treat face source as the human-edited form and canonical as the build-time artifact. Do not run fmt on face files until per-face formatters exist.")))
239+
)
240+
241+
(backends
242+
(note "AffineScript is one frontend, many backends. There is no middle-end IR — each backend lowers AST→target directly. Adding a new backend is templated work; the lessons below were earned during the May-2026 backend buildout (JS/C/WGSL/Faust/ONNX/Rust/LLVM/OCaml/Lua/Bash/Nickel/ReScript/Verilog/Gleam landed alongside the pre-existing WASM/WASM-GC/Julia).")
243+
244+
(tiers
245+
(tier
246+
(name "A — host-language emitters")
247+
(members "WASM 1.0, WASM-GC, Julia, JS, C, Rust, OCaml, ReScript, Lua, Bash, Gleam, Nickel")
248+
(acceptance "full AffineScript surface; missing features error loudly at codegen time")
249+
(file-shape "lib/<name>_codegen.ml, ~200-400 lines, single-file output with a tiny inline runtime prelude"))
250+
(tier
251+
(name "B — IR / native")
252+
(members "LLVM IR (Cranelift candidate)")
253+
(acceptance "full surface, lower level of abstraction; some passes (closures, traits) need lowering that may not exist yet")
254+
(file-shape "SSA-shaped emitter; needs current-block tracking for phi predecessors"))
255+
(tier
256+
(name "C — kernel sublanguages")
257+
(members "WGSL, SPIR-V (planned), CUDA C++ (planned), Metal (planned), OpenCL (planned), Faust, ONNX, MLIR/StableHLO (planned)")
258+
(acceptance "DELIBERATELY restricted subset; everything outside that subset must reject explicitly via `unsupported`")
259+
(file-shape "validation pre-pass on the AST shape (e.g., 'first param is Int', 'all types are Float'), then targeted lowering")))
260+
261+
(rules
262+
(clone-don't-redesign
263+
(rule "When adding a new backend, clone the closest existing emitter and substitute lowering rules. Do NOT redesign the codegen module shape from scratch — every backend after the first cost ~200 lines because the modules are isomorphic.")
264+
(template-pairs
265+
"JS/C/Lua/Rust/OCaml/ReScript/Gleam/Nickel/Bash → julia_codegen.ml"
266+
"WGSL/SPIR-V/CUDA/Metal/OpenCL → wgsl_codegen.ml"
267+
"Faust/Csound → faust_codegen.ml"
268+
"ONNX/MLIR/TFLite → onnx_codegen.ml"
269+
"Verilog/VHDL → verilog_codegen.ml"))
270+
271+
(validate-end-to-end
272+
(rule "Every shipped backend must round-trip through the target's reference toolchain. Bytes a human read are not bytes that work. The honest MVP claim is 'the bytes round-trip,' not 'the bytes look right.'")
273+
(validators "naga (WGSL), faust (Faust), oxionnx-proto (ONNX), cc (C), rustc (Rust), ocaml (OCaml), lua5.4 (Lua), bash -n (Bash), nickel typecheck (Nickel), llc (LLVM), node/deno (JS)")
274+
(when-no-validator "If the target has no installable validator in the dev environment, document that explicitly in the recap. Do not claim end-to-end validation by inspection alone."))
275+
276+
(single-file-no-runtime
277+
(rule "Generated output is one file. Tiny runtime preludes (Some/None constructors, println shims, a few factories) go INLINE at the top of the emitted file, not as a separate runtime.h / runtime.lua / runtime.rs. This dodges version-skew bugs that would otherwise haunt every compile."))
278+
279+
(loud-failures
280+
(rule "Surface unsupported source shapes with `raise (X_unsupported \"...\")` so the codegen errors at compile time. Never emit silently miscompilable output. A loud `unsupported` is the load-bearing API contract of every backend."))
281+
282+
(frontend-gaps-don't-paper-at-backend
283+
(rule "If a backend can't lower something the surface syntax accepts, fix the frontend or document the restriction. Do NOT compensate at the backend layer — that produces silent miscompilation later. Examples from the May-2026 buildout: the typechecker's monomorphic-Int restriction (fixed in typecheck.ml) and the borrow checker's missing mut-param seeding (fixed in borrow.ml) both first surfaced as 'WGSL won't accept Float kernels' / 'Faust SAXPY won't borrow-check'; both were frontend bugs masquerading as backend limitations."))
284+
285+
(stubs-as-intrinsics
286+
(rule "When a target has runtime ops AffineScript's resolver doesn't know about (CUDA's __shfl_xor, ONNX's Relu, Faust's tanh, Verilog's $display), declare them in user code as stub functions and recognise them by name in the codegen. The stub body is a placeholder; the resolver passes; the backend pattern-matches. This is the canonical way to smuggle target intrinsics through a universal frontend.")
287+
(example "fn relu(x: Array[Float]) -> Array[Float] { x } // stub for ONNX Relu")
288+
(rationale "Avoids extending the language for every target's vocabulary; keeps each backend self-contained.")))
289+
290+
(when-adding-backend-N+1
291+
(step (n 1) (do "identify the tier (A/B/C above); pick the template emitter from the tier's template-pairs list"))
292+
(step (n 2) (do "create lib/<name>_codegen.ml as a clone with target-specific lowering rules; reuse mangle / type-mapping / gen_lit shape"))
293+
(step (n 3) (do "register the module in lib/dune (alphabetical) and add `.<ext>` dispatch in bin/main.ml (both --json and human branches)"))
294+
(step (n 4) (do "build: `eval $(opam env --switch=default) && dune build lib bin/main.exe`"))
295+
(step (n 5) (do "validate end-to-end with the target's reference toolchain on /tmp/cdemo.affine (the canonical Int/branch/let test); record the result in the recap"))
296+
(step (n 6) (do "for tier-C kernel backends: also test Float arithmetic, since the typechecker patch landed 2026-05-02"))
297+
(step (n 7) (do "document any frontend gap surfaced by the work in this AI.a2ml file under (backends → known-limitations); fix it BEFORE the next adaptive task per the corrective-before-adaptive ordering rule")))
298+
299+
(known-limitations
300+
(limitation
301+
(name "no-target-intrinsic-protocol")
302+
(since "0.1.0")
303+
(impact "Each backend keeps its own ad-hoc builtin allowlist (sin/cos/relu/etc). User stubs work as a workaround per the stubs-as-intrinsics rule, but unifying behind @target(\"foo\") fn declarations would let one stdlib serve all backends. Perfective task; not blocking.")
304+
(recommendation "Don't extend per-backend builtin lists ad-hoc; if a target needs a new op, prefer the stub-recognise-by-name pattern."))
305+
306+
(limitation
307+
(name "no-tensor-type")
308+
(since "0.1.0")
309+
(impact "The frontend has no Tensor[T, S]; ONNX is currently a 'proof of wire format' rather than a real ML target. Adding tensor types is multiple weeks of frontend work and belongs in (out-of-scope deferred-to-sibling-project) until/unless a user demands it.")
310+
(recommendation "When ONNX scope expands, file the request and check (out-of-scope) before extending."))
311+
312+
(limitation
313+
(name "kernel-sublangs-share-no-validator")
314+
(since "0.1.0")
315+
(impact "WGSL, Faust, ONNX each have their own restriction predicate. A shared 'kernel-sublanguage validator' module would let backends declare their accepted subset declaratively. Perfective task.")
316+
(recommendation "If two new tier-C backends ship with overlapping restrictions, factor the validator before adding the third.")))
317+
)
169318
)

0 commit comments

Comments
 (0)