Skip to content

Commit 8d845bc

Browse files
hyperpolymathclaude
andcommitted
feat(face-formatter): ADR-010 face-aware error vocabulary (items 2+3)
lib/face.ml: the single point of face-awareness for compiler errors. Architecture: compiler emits canonical structured errors; Face.format_* maps them to face-specific strings before reaching the terminal. Face.format_type_error, format_quantity_error, format_resolve_error all dispatch on face=Canonical (delegates to existing formatters, zero behaviour change) or face=Python (human-friendly vocabulary): Canonical: "Quantity error: @linear binding 'x' ... used multiple times" Python: "Ownership error: single-use variable 'x' can only be used once, but was used more than once hint: clone or copy the value before reusing it, ..." Python-face vocabulary highlights: @linear binding → single-use variable Quantity error → Ownership error UnboundVariable → Name not found (with def/let hint) TypeMismatch → Type error: expected X but got Y BranchTypeMismatch → if/else type mismatch: ...branches must match Unit → None (render_ty Python mapping) All variants covered with exhaustive pattern matching (no fallbacks). bin/main.ml: face_arg now produces Face.face (nominal type, not polymorphic variants). All show_resolve_error / format_type_error call sites replaced with Face.format_resolve_error face / format_type_error face. Seam check (items 1+2 complete together): `check --face python` on a @linear param used twice produces: "Ownership error: single-use variable 'x' can only be used once..." End-to-end: .pyaff → transform → lex → parse → resolve → typecheck → Face.format_type_error Python → terminal. ✓ 73/73 E2E tests: 0 regressions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3dbee21 commit 8d845bc

3 files changed

Lines changed: 199 additions & 21 deletions

File tree

bin/main.ml

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ let lex_file path =
6060
`Error (false, "Lexer error")
6161

6262
(** Parse a file using the requested face. *)
63-
let parse_with_face face path =
63+
let parse_with_face (face : Affinescript.Face.face) path =
6464
match face with
65-
| `Canonical -> Affinescript.Parse_driver.parse_file path
66-
| `Python -> Affinescript.Python_face.parse_file_python path
65+
| Affinescript.Face.Canonical -> Affinescript.Parse_driver.parse_file path
66+
| Affinescript.Face.Python -> Affinescript.Python_face.parse_file_python path
6767

6868
(** Preview the Python-face text transform (debug tool). *)
6969
let preview_python_transform path =
@@ -74,12 +74,9 @@ let preview_python_transform path =
7474
`Ok ()
7575

7676
(** Parse a file and print AST (no --json support). *)
77-
let parse_file face path =
77+
let parse_file (face : Affinescript.Face.face) path =
7878
try
79-
let prog = match face with
80-
| `Canonical -> Affinescript.Parse_driver.parse_file path
81-
| `Python -> Affinescript.Python_face.parse_file_python path
82-
in
79+
let prog = parse_with_face face path in
8380
Format.printf "%s@." (Affinescript.Ast.show_program prog);
8481
`Ok ()
8582
with
@@ -143,13 +140,13 @@ let check_file face json path =
143140
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
144141
| Error (e, _span) ->
145142
Format.eprintf "@[<v>Resolution error: %s@]@."
146-
(Affinescript.Resolve.show_resolve_error e);
143+
(Affinescript.Face.format_resolve_error face e);
147144
`Error (false, "Resolution error")
148145
| Ok (resolve_ctx, _type_ctx) ->
149146
(match Affinescript.Typecheck.check_program resolve_ctx.symbols prog with
150147
| Error e ->
151148
Format.eprintf "@[<v>%s@]@."
152-
(Affinescript.Typecheck.format_type_error e);
149+
(Affinescript.Face.format_type_error face e);
153150
`Error (false, "Type error")
154151
| Ok _ctx ->
155152
Format.printf "Type checking passed@.";
@@ -206,7 +203,7 @@ let eval_file face json path =
206203
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
207204
| Error (e, _span) ->
208205
Format.eprintf "@[<v>Resolution error: %s@]@."
209-
(Affinescript.Resolve.show_resolve_error e);
206+
(Affinescript.Face.format_resolve_error face e);
210207
`Error (false, "Resolution error")
211208
| Ok (resolve_ctx, type_ctx) ->
212209
let type_ctx = { type_ctx with symbols = resolve_ctx.symbols } in
@@ -217,7 +214,7 @@ let eval_file face json path =
217214
) (Ok ()) prog.prog_decls with
218215
| Error e ->
219216
Format.eprintf "@[<v>%s@]@."
220-
(Affinescript.Typecheck.format_type_error e);
217+
(Affinescript.Face.format_type_error face e);
221218
`Error (false, "Type error")
222219
| Ok () ->
223220
(match Affinescript.Interp.eval_program prog with
@@ -310,13 +307,13 @@ let compile_file face json wasm_gc path output =
310307
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
311308
| Error (e, _span) ->
312309
Format.eprintf "@[<v>Resolution error: %s@]@."
313-
(Affinescript.Resolve.show_resolve_error e);
310+
(Affinescript.Face.format_resolve_error face e);
314311
`Error (false, "Resolution error")
315312
| Ok (resolve_ctx, _type_ctx) ->
316313
(match Affinescript.Typecheck.check_program resolve_ctx.symbols prog with
317314
| Error e ->
318315
Format.eprintf "@[<v>%s@]@."
319-
(Affinescript.Typecheck.format_type_error e);
316+
(Affinescript.Face.format_type_error face e);
320317
`Error (false, "Type error")
321318
| Ok _type_ctx ->
322319
let is_julia = Filename.check_suffix output ".jl" in
@@ -366,12 +363,11 @@ let compile_file face json wasm_gc path output =
366363
requires a reverse transform that is not yet implemented. *)
367364
let fmt_file face path =
368365
(match face with
369-
| `Python ->
366+
| Affinescript.Face.Python ->
370367
Format.eprintf "fmt --face python is not yet supported \
371368
(reverse Python transform is pending).@.";
372-
(* fall through; format the canonical parse anyway so the file still works *)
373369
()
374-
| `Canonical -> ());
370+
| Affinescript.Face.Canonical -> ());
375371
try
376372
Affinescript.Formatter.format_file path;
377373
Format.printf "Formatted %s@." path;
@@ -418,7 +414,7 @@ let lint_file face json path =
418414
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
419415
| Error (e, _span) ->
420416
Format.eprintf "@[<v>Resolution error: %s@]@."
421-
(Affinescript.Resolve.show_resolve_error e);
417+
(Affinescript.Face.format_resolve_error face e);
422418
`Error (false, "Resolution error")
423419
| Ok (resolve_ctx, _type_ctx) ->
424420
let diagnostics = Affinescript.Linter.lint_program resolve_ctx.symbols prog in
@@ -460,8 +456,11 @@ let wasm_gc_arg =
460456

461457
(** Shared --face flag: select the parser surface-syntax face. *)
462458
let face_arg =
463-
let faces = Arg.enum [("canonical", `Canonical); ("python", `Python)] in
464-
Arg.(value & opt faces `Canonical & info ["face"]
459+
let faces = Arg.enum [
460+
("canonical", Affinescript.Face.Canonical);
461+
("python", Affinescript.Face.Python);
462+
] in
463+
Arg.(value & opt faces Affinescript.Face.Canonical & info ["face"]
465464
~docv:"FACE"
466465
~doc:"Parser face (surface-syntax variant). $(docv) must be $(b,canonical) \
467466
(default, standard AffineScript) or $(b,python) (Python-style syntax: \

lib/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
(name affinescript)
33
(public_name affinescript)
44
(modes byte native)
5-
(modules ast codegen codegen_gc desugar_traits effect error error_collector error_formatter formatter interp julia_codegen json_output lexer linter module_loader opt parse_driver parse parser parser_errors python_face quantity resolve span symbol token trait typecheck types unify value wasm wasm_encode wasm_gc wasm_gc_encode wasi_runtime)
5+
(modules ast codegen codegen_gc desugar_traits effect error error_collector error_formatter face formatter interp julia_codegen json_output lexer linter module_loader opt parse_driver parse parser parser_errors python_face quantity resolve span symbol token trait typecheck types unify value wasm wasm_encode wasm_gc wasm_gc_encode wasi_runtime)
66
(libraries str unix sedlex fmt menhirLib yojson)
77
(preprocess
88
(pps ppx_deriving.show ppx_deriving.eq ppx_deriving.ord sedlex.ppx)))

lib/face.ml

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
(* SPDX-License-Identifier: PMPL-1.0-or-later *)
2+
(* SPDX-FileCopyrightText: 2024-2026 Jonathan D.A. Jewell (hyperpolymath) *)
3+
4+
(** Face-aware error formatter (ADR-010).
5+
6+
The compiler's internal error representation is canonical and
7+
face-agnostic. This module is the single point where canonical error
8+
terms are mapped to face-specific vocabulary before being shown to the
9+
user.
10+
11+
Architecture (ADR-010 §2):
12+
{v
13+
compiler → face.ml → terminal / LSP / IDE
14+
v}
15+
16+
Adding a new face: add a variant to {!face} and a branch to each
17+
[format_*_for_face] function below. No compiler internals change.
18+
*)
19+
20+
(** Active parser/error face. *)
21+
type face =
22+
| Canonical (** Standard AffineScript syntax and vocabulary. *)
23+
| Python (** Python-style surface syntax; Python-friendly messages. *)
24+
25+
(* ─── Helpers ────────────────────────────────────────────────────────── *)
26+
27+
(** Render a type to a face-appropriate string.
28+
The canonical [Types.ty_to_string] output is always valid here;
29+
face-specific names for built-in types can be added per face. *)
30+
let render_ty (face : face) (ty : Types.ty) : string =
31+
let s = Types.ty_to_string ty in
32+
match face with
33+
| Canonical -> s
34+
| Python ->
35+
(* Map a few canonical names to Python-familiar names. *)
36+
let s = if s = "Unit" then "None" else s in
37+
let s = if s = "Bool" then "bool" else s in
38+
s
39+
40+
(* ─── Quantity / ownership errors ────────────────────────────────────── *)
41+
42+
(** Format a QTT quantity error for the given face.
43+
44+
Python-face replaces the @linear/@erased/@unrestricted annotation
45+
vocabulary with "single-use"/"erased"/"unrestricted" and frames
46+
errors in terms a Python developer can recognise. *)
47+
let format_quantity_error (face : face) (err : Quantity.quantity_error) : string =
48+
match face with
49+
| Canonical -> Quantity.format_quantity_error err
50+
| Python ->
51+
(match err with
52+
| Quantity.LinearVariableUnused id ->
53+
Printf.sprintf
54+
"Ownership error: single-use variable '%s' must be used exactly once, \
55+
but was never used\n\
56+
hint: either use '%s', or prefix its name with '_' to suppress this error"
57+
id.name id.name
58+
| Quantity.LinearVariableUsedMultiple id ->
59+
Printf.sprintf
60+
"Ownership error: single-use variable '%s' can only be used once, \
61+
but was used more than once\n\
62+
hint: clone or copy the value before reusing it, \
63+
or declare '%s' as an unrestricted variable"
64+
id.name id.name
65+
| Quantity.ErasedVariableUsed id ->
66+
Printf.sprintf
67+
"Ownership error: erased variable '%s' was declared as compile-time-only \
68+
(@erased / :0) and cannot appear at runtime"
69+
id.name
70+
| Quantity.QuantityMismatch (id, q, _u) ->
71+
let decl = match q with
72+
| Ast.QZero -> "erased (compile-time only)"
73+
| Ast.QOne -> "single-use"
74+
| Ast.QOmega -> "unrestricted"
75+
in
76+
Printf.sprintf
77+
"Ownership error: '%s' was declared as %s but used inconsistently"
78+
id.name decl)
79+
80+
(* ─── Unification errors ─────────────────────────────────────────────── *)
81+
82+
let format_unify_error (face : face) (ue : Unify.unify_error) : string =
83+
match face with
84+
| Canonical -> Unify.show_unify_error ue
85+
| Python ->
86+
(match ue with
87+
| Unify.TypeMismatch (expected, got) ->
88+
Printf.sprintf "Type error: expected %s, got %s"
89+
(render_ty face expected)
90+
(render_ty face got)
91+
| Unify.OccursCheck _ ->
92+
"Type error: recursive type — a type cannot contain itself"
93+
| Unify.RowMismatch _ ->
94+
"Type error: record field mismatch"
95+
| Unify.RowOccursCheck _ ->
96+
"Type error: recursive record type"
97+
| Unify.EffectMismatch _ ->
98+
"Type error: effect set mismatch"
99+
| Unify.EffectOccursCheck _ ->
100+
"Type error: recursive effect type"
101+
| Unify.KindMismatch _ ->
102+
"Type error: kind mismatch (e.g. used a type where a row was expected)"
103+
| Unify.LabelNotFound (label, _) ->
104+
Printf.sprintf "Type error: field '%s' not found in record" label)
105+
106+
(* ─── Type errors ────────────────────────────────────────────────────── *)
107+
108+
(** Format a type-checker error for the given face. *)
109+
let format_type_error (face : face) (err : Typecheck.type_error) : string =
110+
match face with
111+
| Canonical -> Typecheck.format_type_error err
112+
| Python ->
113+
(match err with
114+
| Typecheck.UnboundVariable v ->
115+
Printf.sprintf "Name not found: '%s'\n\
116+
hint: check spelling or add a 'def %s(...)' declaration" v v
117+
| Typecheck.TypeMismatch { expected; got } ->
118+
Printf.sprintf "Type error: expected %s but got %s"
119+
(render_ty face expected)
120+
(render_ty face got)
121+
| Typecheck.OccursCheck (v, ty) ->
122+
Printf.sprintf "Type error: cannot construct infinite type %s = %s"
123+
v (render_ty face ty)
124+
| Typecheck.NotImplemented msg ->
125+
Printf.sprintf "Compiler limitation: %s" msg
126+
| Typecheck.ArityMismatch { name; expected; got } ->
127+
Printf.sprintf "'%s' takes %d argument%s but was called with %d"
128+
name expected (if expected = 1 then "" else "s") got
129+
| Typecheck.NotAFunction ty ->
130+
Printf.sprintf "Cannot call a value of type %s (it is not a function)"
131+
(render_ty face ty)
132+
| Typecheck.FieldNotFound { field; record_ty } ->
133+
Printf.sprintf "Field '%s' does not exist on type %s"
134+
field (render_ty face record_ty)
135+
| Typecheck.TupleIndexOutOfBounds { index; length } ->
136+
Printf.sprintf "Tuple index %d is out of range (tuple has %d element%s)"
137+
index length (if length = 1 then "" else "s")
138+
| Typecheck.DuplicateField f ->
139+
Printf.sprintf "Duplicate field '%s' in record literal" f
140+
| Typecheck.UnificationError ue ->
141+
format_unify_error face ue
142+
| Typecheck.PatternTypeMismatch msg ->
143+
Printf.sprintf "Pattern error: %s" msg
144+
| Typecheck.BranchTypeMismatch { then_ty; else_ty } ->
145+
Printf.sprintf
146+
"if/else type mismatch: the if-branch returns %s \
147+
but the else-branch returns %s — both branches must return the same type"
148+
(render_ty face then_ty)
149+
(render_ty face else_ty)
150+
| Typecheck.QuantityError (qerr, _span) ->
151+
format_quantity_error face qerr)
152+
153+
(* ─── Resolve errors ─────────────────────────────────────────────────── *)
154+
155+
(** Format a name-resolution error for the given face. *)
156+
let format_resolve_error (face : face) (err : Resolve.resolve_error) : string =
157+
match face with
158+
| Canonical -> Resolve.show_resolve_error err
159+
| Python ->
160+
(match err with
161+
| Resolve.UndefinedVariable id ->
162+
Printf.sprintf "Name not found: '%s'\n\
163+
hint: define it with 'def %s(...):' or 'let %s = ...'"
164+
id.name id.name id.name
165+
| Resolve.UndefinedType id ->
166+
Printf.sprintf "Type not found: '%s'\n\
167+
hint: define it with 'class %s:' or 'type %s = ...'"
168+
id.name id.name id.name
169+
| Resolve.UndefinedEffect id ->
170+
Printf.sprintf "Effect not found: '%s'" id.name
171+
| Resolve.UndefinedModule id ->
172+
Printf.sprintf "Module not found: '%s'\n\
173+
hint: use 'import %s' to bring it into scope" id.name id.name
174+
| Resolve.DuplicateDefinition id ->
175+
Printf.sprintf "'%s' is already defined in this scope" id.name
176+
| Resolve.VisibilityError (id, msg) ->
177+
Printf.sprintf "'%s' is not accessible here: %s" id.name msg
178+
| Resolve.ImportError msg ->
179+
Printf.sprintf "Import error: %s" msg)

0 commit comments

Comments
 (0)