Skip to content

Commit 3dbee21

Browse files
hyperpolymathclaude
andcommitted
feat(python-face): wire --face to all commands + fix tail-position semicolons
--face [canonical|python] is now accepted by check, eval, compile, fmt, and lint — not just parse. The canonical face is the default for all commands; `fmt --face python` warns that reverse transform is pending. Tail-position fix in python_face.ml: a regular statement whose next meaningful line has a smaller indent (i.e. the last expression in a block) is now emitted WITHOUT a trailing `;`. Previously every statement got `;`, making non-unit blocks return () and breaking type-checking. The indexed lookahead in transform_source computes next_meaningful_indent for each line and suppresses `;` when is_tail = next_ind < current_ind. Seam check: `affinescript check --face python python_face_basic.pyaff` now reaches the type checker and passes — end-to-end from Python-face source through transform → lex → parse → resolve → typecheck. 73/73 E2E tests: 0 regressions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ff335d8 commit 3dbee21

2 files changed

Lines changed: 73 additions & 30 deletions

File tree

bin/main.ml

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ let lex_file path =
5959
Format.eprintf "@[<v>%s:%d:%d: error: %s@]@." path pos.line pos.col msg;
6060
`Error (false, "Lexer error")
6161

62+
(** Parse a file using the requested face. *)
63+
let parse_with_face face path =
64+
match face with
65+
| `Canonical -> Affinescript.Parse_driver.parse_file path
66+
| `Python -> Affinescript.Python_face.parse_file_python path
67+
6268
(** Preview the Python-face text transform (debug tool). *)
6369
let preview_python_transform path =
6470
let ch = open_in_bin path in
@@ -87,14 +93,14 @@ let parse_file face path =
8793

8894
(** Type-check a file. With [--json], emits a structured diagnostic
8995
report on stderr. *)
90-
let check_file json path =
96+
let check_file face json path =
9197
if json then begin
9298
let diags = ref [] in
9399
let add d = diags := d :: !diags in
94100
let symbols_table = ref None in
95101
let resolve_refs = ref [] in
96102
begin try
97-
let prog = Affinescript.Parse_driver.parse_file path in
103+
let prog = parse_with_face face path in
98104
let loader_config = Affinescript.Module_loader.default_config () in
99105
let loader = Affinescript.Module_loader.create loader_config in
100106
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
@@ -131,7 +137,7 @@ let check_file json path =
131137
json_finish final_diags)
132138
end else begin
133139
try
134-
let prog = Affinescript.Parse_driver.parse_file path in
140+
let prog = parse_with_face face path in
135141
let loader_config = Affinescript.Module_loader.default_config () in
136142
let loader = Affinescript.Module_loader.create loader_config in
137143
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
@@ -160,12 +166,12 @@ let check_file json path =
160166

161167
(** Evaluate a file with the interpreter. With [--json], emits
162168
diagnostics on stderr instead of human-readable error text. *)
163-
let eval_file json path =
169+
let eval_file face json path =
164170
if json then begin
165171
let diags = ref [] in
166172
let add d = diags := d :: !diags in
167173
begin try
168-
let prog = Affinescript.Parse_driver.parse_file path in
174+
let prog = parse_with_face face path in
169175
let loader_config = Affinescript.Module_loader.default_config () in
170176
let loader = Affinescript.Module_loader.create loader_config in
171177
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
@@ -194,7 +200,7 @@ let eval_file json path =
194200
json_finish (List.rev !diags)
195201
end else begin
196202
try
197-
let prog = Affinescript.Parse_driver.parse_file path in
203+
let prog = parse_with_face face path in
198204
let loader_config = Affinescript.Module_loader.default_config () in
199205
let loader = Affinescript.Module_loader.create loader_config in
200206
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
@@ -242,12 +248,12 @@ let repl_cmd_fn () =
242248
(** Compile a file. With [--json], emits diagnostics for any
243249
compilation errors. With [--wasm-gc], targets the WebAssembly GC
244250
proposal instead of WASM 1.0 linear memory. *)
245-
let compile_file json wasm_gc path output =
251+
let compile_file face json wasm_gc path output =
246252
if json then begin
247253
let diags = ref [] in
248254
let add d = diags := d :: !diags in
249255
begin try
250-
let prog = Affinescript.Parse_driver.parse_file path in
256+
let prog = parse_with_face face path in
251257
let loader_config = Affinescript.Module_loader.default_config () in
252258
let loader = Affinescript.Module_loader.create loader_config in
253259
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
@@ -298,7 +304,7 @@ let compile_file json wasm_gc path output =
298304
json_finish (List.rev !diags)
299305
end else begin
300306
try
301-
let prog = Affinescript.Parse_driver.parse_file path in
307+
let prog = parse_with_face face path in
302308
let loader_config = Affinescript.Module_loader.default_config () in
303309
let loader = Affinescript.Module_loader.create loader_config in
304310
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
@@ -356,8 +362,16 @@ let compile_file json wasm_gc path output =
356362
`Error (false, "Parse error")
357363
end
358364

359-
(** Format a file *)
360-
let fmt_file path =
365+
(** Format a file. Only canonical face is supported — Python-face formatting
366+
requires a reverse transform that is not yet implemented. *)
367+
let fmt_file face path =
368+
(match face with
369+
| `Python ->
370+
Format.eprintf "fmt --face python is not yet supported \
371+
(reverse Python transform is pending).@.";
372+
(* fall through; format the canonical parse anyway so the file still works *)
373+
()
374+
| `Canonical -> ());
361375
try
362376
Affinescript.Formatter.format_file path;
363377
Format.printf "Formatted %s@." path;
@@ -373,12 +387,12 @@ let fmt_file path =
373387

374388
(** Lint a file. With [--json], emits lint diagnostics as structured
375389
JSON on stderr. *)
376-
let lint_file json path =
390+
let lint_file face json path =
377391
if json then begin
378392
let diags = ref [] in
379393
let add d = diags := d :: !diags in
380394
begin try
381-
let prog = Affinescript.Parse_driver.parse_file path in
395+
let prog = parse_with_face face path in
382396
let loader_config = Affinescript.Module_loader.default_config () in
383397
let loader = Affinescript.Module_loader.create loader_config in
384398
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
@@ -398,7 +412,7 @@ let lint_file json path =
398412
json_finish (List.rev !diags)
399413
end else begin
400414
try
401-
let prog = Affinescript.Parse_driver.parse_file path in
415+
let prog = parse_with_face face path in
402416
let loader_config = Affinescript.Module_loader.default_config () in
403417
let loader = Affinescript.Module_loader.create loader_config in
404418
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
@@ -467,12 +481,12 @@ let parse_cmd =
467481
let check_cmd =
468482
let doc = "Type check a file" in
469483
let info = Cmd.info "check" ~doc in
470-
Cmd.v info Term.(ret (const check_file $ json_arg $ path_arg))
484+
Cmd.v info Term.(ret (const check_file $ face_arg $ json_arg $ path_arg))
471485

472486
let eval_cmd =
473487
let doc = "Evaluate a file with the interpreter" in
474488
let info = Cmd.info "eval" ~doc in
475-
Cmd.v info Term.(ret (const eval_file $ json_arg $ path_arg))
489+
Cmd.v info Term.(ret (const eval_file $ face_arg $ json_arg $ path_arg))
476490

477491
let repl_cmd =
478492
let doc = "Start the interactive REPL" in
@@ -482,17 +496,17 @@ let repl_cmd =
482496
let compile_cmd =
483497
let doc = "Compile a file to WebAssembly (1.0 or GC proposal) or Julia" in
484498
let info = Cmd.info "compile" ~doc in
485-
Cmd.v info Term.(ret (const compile_file $ json_arg $ wasm_gc_arg $ path_arg $ output_arg))
499+
Cmd.v info Term.(ret (const compile_file $ face_arg $ json_arg $ wasm_gc_arg $ path_arg $ output_arg))
486500

487501
let fmt_cmd =
488502
let doc = "Format a file" in
489503
let info = Cmd.info "fmt" ~doc in
490-
Cmd.v info Term.(ret (const fmt_file $ path_arg))
504+
Cmd.v info Term.(ret (const fmt_file $ face_arg $ path_arg))
491505

492506
let lint_cmd =
493507
let doc = "Lint a file for code quality issues" in
494508
let info = Cmd.info "lint" ~doc in
495-
Cmd.v info Term.(ret (const lint_file $ json_arg $ path_arg))
509+
Cmd.v info Term.(ret (const lint_file $ face_arg $ json_arg $ path_arg))
496510

497511
let preview_python_cmd =
498512
let doc = "Preview the Python-face text transform (debug)" in

lib/python_face.ml

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,23 @@ let elif_condition stripped =
218218

219219
(* ─── Main transformer ───────────────────────────────────────────────── *)
220220

221+
(** True if [raw_line] is blank or comment-only (carries no code). *)
222+
let is_blank_line raw =
223+
let (code, _) = strip_py_comment (String.trim raw) in
224+
String.trim code = ""
225+
221226
(** Transform Python-style AffineScript source text to canonical AffineScript.
222-
The result is valid input for the standard lexer + Menhir parser. *)
227+
The result is valid input for the standard lexer + Menhir parser.
228+
229+
Tail-position detection: a regular statement (non-block-opener) in the
230+
last position of a block — i.e. the next meaningful line's indent is
231+
strictly less than the current line's indent — is emitted WITHOUT a
232+
trailing `;`. This preserves the expression-as-return-value semantics
233+
that AffineScript blocks require (a trailing `;` would make the block
234+
yield unit rather than the expression's value). *)
223235
let transform_source source =
224-
let lines = String.split_on_char '\n' source in
236+
let lines = Array.of_list (String.split_on_char '\n' source) in
237+
let n = Array.length lines in
225238
let out = Buffer.create (String.length source + 256) in
226239
(* Indentation stack: innermost level at head, outermost (0) at tail. *)
227240
let stack = ref [0] in
@@ -235,7 +248,16 @@ let transform_source source =
235248
done
236249
in
237250

238-
List.iter (fun raw_line ->
251+
(* The indent level of the next non-blank/non-comment line after index [i],
252+
or [-1] when there is no such line (EOF). *)
253+
let next_meaningful_indent i =
254+
let j = ref (i + 1) in
255+
while !j < n && is_blank_line lines.(!j) do incr j done;
256+
if !j >= n then -1 else indent_of lines.(!j)
257+
in
258+
259+
for i = 0 to n - 1 do
260+
let raw_line = lines.(i) in
239261
let ind = indent_of raw_line in
240262
let (code_part, comment_opt) = strip_py_comment (String.trim raw_line) in
241263
let stripped = String.trim code_part in
@@ -272,21 +294,28 @@ let transform_source source =
272294

273295
let indent_str = String.make ind ' ' in
274296

297+
(* Tail-position check: the next meaningful line is less indented (or
298+
EOF), meaning this is the last expression in its block. Omit `;`
299+
so the block's value is this expression, not unit. *)
300+
let next_ind = next_meaningful_indent i in
301+
let is_tail = next_ind < ind in (* -1 (EOF) satisfies this for ind > 0 *)
302+
275303
let line_text = match transform_import_line stripped with
276-
| Some s -> s
304+
| Some s -> s (* imports are always top-level statements *)
277305
| None ->
278-
if is_block_opener stripped then begin
306+
if is_block_opener stripped then
279307
(* Replace trailing `:` with ` {` *)
280-
let body = apply_keywords (strip_block_colon stripped) in
281-
body ^ " {"
282-
end else begin
283-
(* Regular statement: terminate with `;` *)
308+
apply_keywords (strip_block_colon stripped) ^ " {"
309+
else if is_tail then
310+
(* Tail expression: no `;` — this is the block's return value *)
311+
apply_keywords stripped
312+
else
313+
(* Mid-block statement: terminate with `;` *)
284314
apply_keywords stripped ^ ";"
285-
end
286315
in
287316
Buffer.add_string out (indent_str ^ with_comment line_text)
288317
end
289-
) lines;
318+
done;
290319

291320
(* Close any blocks still open at EOF *)
292321
emit_dedents 0;

0 commit comments

Comments
 (0)