Skip to content

Commit 872d13c

Browse files
fix(faces): fix four PseudoScript transformer bugs (#118)
* fix(faces): fix four pseudocode-face transformer bugs - `output X` / `print X` / `display X` now lowers to `println(X)` instead of `IO.println(X)` (dropped stray `IO.` prefix). - `--` comment lines converted by `transform_double_dash_comment` no longer fall through to `apply_boolean_ops`, so `or` inside a comment is not clobbered with `||`. - `if`/`else if`/`while`/`for`/`match` condition strings now go through `apply_comparisons`, `apply_boolean_ops`, and `apply_literal_subs` before being emitted, so pseudocode operators in conditions lower correctly. - `transform_source` replaced with a brace-depth-tracking pass that inserts `;` after non-tail statements inside blocks, enabling multi-statement function bodies. Updated `examples/faces/hello-pseudo.affine` to exercise `set X to Y` and `output X` in a two-statement body. Updated snapshot. Removed the now-fixed Pseudocode row from `examples/faces/README.adoc`. * fix(faces/pseudo): preserve indentation in transform_set `transform_set` was calling `String.trim line` immediately, discarding any leading whitespace. Statements like ` set x to expr` inside a function body would lower to `let x = expr` at column 0, producing a snapshot mismatch with the 4-space-indented expected output. Fix: capture the leading indent before trimming and prepend it to the generated `let`/`let mut` expression. https://claude.ai/code/session_01Vrh2f1G8tf7ZbNcKh9r5U9 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d2875a5 commit 872d13c

4 files changed

Lines changed: 65 additions & 39 deletions

File tree

‎examples/faces/README.adoc‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,22 +77,16 @@ affinescript parse examples/faces/hello-rattle.affine
7777

7878
== Caveats
7979

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/`.
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/`.
8181
* 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:
82+
* The examples deliberately avoid features each transformer can’t yet handle. Known transformer gaps that the simpler examples sidestep:
8383
+
8484
[cols="1,3"]
8585
|===
8686
| Face | Pending transformer work
8787

8888
| 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)`.
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.
9690

9791
| Lucid (PureScript)
9892
| 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.

‎examples/faces/hello-pseudo.affine‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22
-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
33
--
44
-- 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.)
5+
-- function ... do ... end blocks, `--` Haskell/SQL-style comments,
6+
-- `set X to Y` bindings, `output X` I/O, multi-statement function bodies.
97
-- face: pseudoscript
108

119
effect IO {
1210
fn println(s: String) -> ();
1311
}
1412

1513
function main() -{IO}-> () do
16-
println("Hello, PseudoScript!")
14+
set greeting to "Hello, PseudoScript!"
15+
output greeting
1716
end

‎lib/pseudocode_face.ml‎

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
None / nothing / null / nil → ()
3636
yes / YES → true
3737
no / NO → false
38-
output expr / print expr / display expr → IO.println(expr)
38+
output expr / print expr / display expr → println(expr)
3939
// comment → (already valid)
4040
-- comment (Haskell/SQL style) → // comment
4141
v}
@@ -50,7 +50,7 @@
5050
text.
5151
*)
5252

53-
(* ─── Character helpers ────────────────────────────────────────────────── *)
53+
(* ─── Character helpers ────────────────────────────────────────── *)
5454

5555
let is_id_char c =
5656
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
@@ -98,7 +98,7 @@ let transform_double_dash_comment line =
9898
indent ^ "//" ^ String.sub trimmed 2 (String.length trimmed - 2)
9999
else line
100100

101-
(* ─── Operator / keyword substitutions ────────────────────────────────── *)
101+
(* ─── Operator / keyword substitutions ────────────────────────────────────── *)
102102

103103
(** Apply multi-word comparisons first (longest-match order). *)
104104
let apply_comparisons line =
@@ -136,7 +136,7 @@ let apply_literal_subs line =
136136
let line = subst_word line "NO" "false" in
137137
line
138138

139-
(* ─── Statement-level transforms ──────────────────────────────────────── *)
139+
(* ─── Statement-level transforms ────────────────────────────────────────────── *)
140140

141141
(** [function/procedure name(params) returns T {] → [fn name(params) -> T {] *)
142142
let transform_function_decl trimmed =
@@ -161,15 +161,17 @@ let transform_function_decl trimmed =
161161

162162
(** [set x to expr] → [let x = expr] *)
163163
let transform_set line =
164-
let line = String.trim line in
165-
if starts_with line "set " then begin
166-
let rest = String.sub line 4 (String.length line - 4) in
164+
let trimmed = String.trim line in
165+
let indent_len = String.length line - String.length trimmed in
166+
let indent = String.sub line 0 indent_len in
167+
if starts_with trimmed "set " then begin
168+
let rest = String.sub trimmed 4 (String.length trimmed - 4) in
167169
(* Look for " to " *)
168170
match String.split_on_char ' ' rest with
169171
| name :: "to" :: "mut" :: value_parts ->
170-
Printf.sprintf "let mut %s = %s" name (String.concat " " value_parts)
172+
indent ^ Printf.sprintf "let mut %s = %s" name (String.concat " " value_parts)
171173
| name :: "to" :: value_parts ->
172-
Printf.sprintf "let %s = %s" name (String.concat " " value_parts)
174+
indent ^ Printf.sprintf "let %s = %s" name (String.concat " " value_parts)
173175
| _ -> line
174176
end else line
175177

@@ -191,7 +193,7 @@ let transform_io_output line =
191193
if starts_with t keyword then begin
192194
let rest = String.trim (String.sub t (String.length keyword)
193195
(String.length t - String.length keyword)) in
194-
Some (indent ^ "IO.println(" ^ rest ^ ")")
196+
Some (indent ^ "println(" ^ rest ^ ")")
195197
end else None
196198
in
197199
match try_io "output " with
@@ -217,6 +219,9 @@ let transform_control_flow line =
217219
else if ends_with cond_str " do" then String.sub cond_str 0 (String.length cond_str - 3)
218220
else cond_str
219221
in
222+
let cond_str = apply_comparisons cond_str in
223+
let cond_str = apply_boolean_ops cond_str in
224+
let cond_str = apply_literal_subs cond_str in
220225
ignore cond;
221226
indent ^ "if " ^ cond_str ^ " {"
222227
in
@@ -226,10 +231,13 @@ let transform_control_flow line =
226231
let open_for cond =
227232
indent ^ "for " ^ cond ^ " {"
228233
in
234+
let apply_cond_subs s =
235+
apply_literal_subs (apply_boolean_ops (apply_comparisons s))
236+
in
229237
if starts_with t "else if " then begin
230238
let rest = String.sub t 8 (String.length t - 8) in
231239
let rest = subst_word rest "then" "" in
232-
let rest = String.trim rest in
240+
let rest = apply_cond_subs (String.trim rest) in
233241
indent ^ "} else if " ^ rest ^ " {"
234242
end else if t = "else" then
235243
indent ^ "} else {"
@@ -238,21 +246,21 @@ let transform_control_flow line =
238246
else if starts_with t "while " then begin
239247
let rest = String.sub t 6 (String.length t - 6) in
240248
let rest = if ends_with rest " do" then String.sub rest 0 (String.length rest - 3) else rest in
241-
open_while (String.trim rest)
249+
open_while (apply_cond_subs (String.trim rest))
242250
end else if starts_with t "for " then begin
243251
let rest = String.sub t 4 (String.length t - 4) in
244252
let rest = if ends_with rest " do" then String.sub rest 0 (String.length rest - 3) else rest in
245-
open_for (String.trim rest)
253+
open_for (apply_cond_subs (String.trim rest))
246254
end else if starts_with t "match " then begin
247255
let rest = String.sub t 6 (String.length t - 6) in
248256
let rest = if ends_with rest " on" then String.sub rest 0 (String.length rest - 3) else rest in
249-
indent ^ "match " ^ String.trim rest ^ " {"
257+
indent ^ "match " ^ apply_cond_subs (String.trim rest) ^ " {"
250258
end else if t = "end if" || t = "end while" || t = "end for"
251259
|| t = "end match" || t = "end" || t = "fi" || t = "od" then
252260
indent ^ "}"
253261
else line
254262

255-
(* ─── Line-by-line transform ────────────────────────────────────────────── *)
263+
(* ─── Line-by-line transform ──────────────────────────────────────────────── *)
256264

257265
let transform_line line =
258266
let t = String.trim line in
@@ -263,8 +271,9 @@ let transform_line line =
263271
(* 1. Convert double-dash comments *)
264272
let line = transform_double_dash_comment line in
265273
let t = String.trim line in
274+
if starts_with t "//" then line
266275
(* 2. Function/procedure declarations *)
267-
if starts_with t "function " || starts_with t "procedure " then
276+
else if starts_with t "function " || starts_with t "procedure " then
268277
transform_function_decl t
269278
(* 3. set ... to ... *)
270279
else if starts_with t "set " then
@@ -302,12 +311,37 @@ let transform_line line =
302311
end
303312
end
304313

305-
(* ─── File-level entry points ─────────────────────────────────────────── *)
314+
(* ─── File-level entry points ────────────────────────────────────────────── *)
306315

307316
let transform_source source =
308317
let lines = String.split_on_char '\n' source in
309-
let out = List.map transform_line lines in
310-
String.concat "\n" out
318+
let transformed = Array.of_list (List.map transform_line lines) in
319+
let n = Array.length transformed in
320+
let result = Array.copy transformed in
321+
let depth = ref 0 in
322+
let next_non_empty i =
323+
let j = ref (i + 1) in
324+
while !j < n && String.trim transformed.(!j) = "" do incr j done;
325+
if !j >= n then "" else String.trim transformed.(!j)
326+
in
327+
for i = 0 to n - 1 do
328+
let line = transformed.(i) in
329+
let t = String.trim line in
330+
let len = String.length t in
331+
let ends_with_brace = len > 0 && t.[len - 1] = '{' in
332+
let is_close_brace = t = "}" in
333+
if is_close_brace && !depth > 0 then decr depth;
334+
if !depth > 0 && t <> "" && not ends_with_brace && not is_close_brace
335+
&& not (len > 1 && t.[0] = '/' && t.[1] = '/')
336+
then begin
337+
let next = next_non_empty i in
338+
let next_is_close = String.length next > 0 && next.[0] = '}' in
339+
if not next_is_close && (len = 0 || t.[len - 1] <> ';') then
340+
result.(i) <- line ^ ";"
341+
end;
342+
if ends_with_brace then incr depth
343+
done;
344+
String.concat "\n" (Array.to_list result)
311345

312346
let parse_file_pseudocode path =
313347
let source = In_channel.with_open_text path In_channel.input_all in
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
// SPDX-License-Identifier: AGPL-3.0-||-later
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
22
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
33
//
44
// 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.)
5+
// function ... do ... end blocks, `--` Haskell/SQL-style comments,
6+
// `set X to Y` bindings, `output X` I/O, multi-statement function bodies.
97
// face: pseudoscript
108

119
effect IO {
1210
fn println(s: String) -> ();
1311
}
1412

1513
fn main() -{IO}-> () {
16-
println("Hello, PseudoScript!")
14+
let greeting = "Hello, PseudoScript!";
15+
println(greeting)
1716
}

0 commit comments

Comments
 (0)