Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions examples/faces/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,16 @@ affinescript parse examples/faces/hello-rattle.affine

== Caveats

* 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/`.
* 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 whats in scope from `stdlib/`.
* 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.
* The examples deliberately avoid features each transformer can't yet handle. Known transformer gaps that the simpler examples sidestep:
* The examples deliberately avoid features each transformer cant yet handle. Known transformer gaps that the simpler examples sidestep:
+
[cols="1,3"]
|===
| Face | Pending transformer work

| Python (Rattle)
| 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.

| JS (Jaffa)
| `import { x } from "module";` lowering has a string-stripping bug when the trailing `;` is present (the example avoids `import` entirely for now).

| Pseudocode (Pseudo)
| 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)`.
| 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.

| Lucid (PureScript)
| 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.
Expand Down
9 changes: 4 additions & 5 deletions examples/faces/hello-pseudo.affine
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
-- SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
--
-- PseudoScript face. Distinctive features exercised:
-- function ... do ... end blocks, `--` Haskell/SQL-style comments.
-- (Note: pseudocode-face does not yet auto-insert statement separators,
-- so this minimal demo keeps each function body to a single expression.
-- See examples/faces/README.adoc for a list of pending transformer gaps.)
-- function ... do ... end blocks, `--` Haskell/SQL-style comments,
-- `set X to Y` bindings, `output X` I/O, multi-statement function bodies.
-- face: pseudoscript

effect IO {
fn println(s: String) -> ();
}

function main() -{IO}-> () do
println("Hello, PseudoScript!")
set greeting to "Hello, PseudoScript!"
output greeting
end
72 changes: 53 additions & 19 deletions lib/pseudocode_face.ml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
None / nothing / null / nil → ()
yes / YES → true
no / NO → false
output expr / print expr / display expr → IO.println(expr)
output expr / print expr / display expr → println(expr)
// comment → (already valid)
-- comment (Haskell/SQL style) → // comment
v}
Expand All @@ -50,7 +50,7 @@
text.
*)

(* ─── Character helpers ────────────────────────────────────────────────── *)
(* ─── Character helpers ────────────────────────────────────────── *)

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

(* ─── Operator / keyword substitutions ────────────────────────────────── *)
(* ─── Operator / keyword substitutions ────────────────────────────────────── *)

(** Apply multi-word comparisons first (longest-match order). *)
let apply_comparisons line =
Expand Down Expand Up @@ -136,7 +136,7 @@ let apply_literal_subs line =
let line = subst_word line "NO" "false" in
line

(* ─── Statement-level transforms ──────────────────────────────────────── *)
(* ─── Statement-level transforms ────────────────────────────────────────────── *)

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

(** [set x to expr] → [let x = expr] *)
let transform_set line =
let line = String.trim line in
if starts_with line "set " then begin
let rest = String.sub line 4 (String.length line - 4) in
let trimmed = String.trim line in
let indent_len = String.length line - String.length trimmed in
let indent = String.sub line 0 indent_len in
if starts_with trimmed "set " then begin
let rest = String.sub trimmed 4 (String.length trimmed - 4) in
(* Look for " to " *)
match String.split_on_char ' ' rest with
| name :: "to" :: "mut" :: value_parts ->
Printf.sprintf "let mut %s = %s" name (String.concat " " value_parts)
indent ^ Printf.sprintf "let mut %s = %s" name (String.concat " " value_parts)
| name :: "to" :: value_parts ->
Printf.sprintf "let %s = %s" name (String.concat " " value_parts)
indent ^ Printf.sprintf "let %s = %s" name (String.concat " " value_parts)
| _ -> line
end else line

Expand All @@ -191,7 +193,7 @@ let transform_io_output line =
if starts_with t keyword then begin
let rest = String.trim (String.sub t (String.length keyword)
(String.length t - String.length keyword)) in
Some (indent ^ "IO.println(" ^ rest ^ ")")
Some (indent ^ "println(" ^ rest ^ ")")
end else None
in
match try_io "output " with
Expand All @@ -217,6 +219,9 @@ let transform_control_flow line =
else if ends_with cond_str " do" then String.sub cond_str 0 (String.length cond_str - 3)
else cond_str
in
let cond_str = apply_comparisons cond_str in
let cond_str = apply_boolean_ops cond_str in
let cond_str = apply_literal_subs cond_str in
ignore cond;
indent ^ "if " ^ cond_str ^ " {"
in
Expand All @@ -226,10 +231,13 @@ let transform_control_flow line =
let open_for cond =
indent ^ "for " ^ cond ^ " {"
in
let apply_cond_subs s =
apply_literal_subs (apply_boolean_ops (apply_comparisons s))
in
if starts_with t "else if " then begin
let rest = String.sub t 8 (String.length t - 8) in
let rest = subst_word rest "then" "" in
let rest = String.trim rest in
let rest = apply_cond_subs (String.trim rest) in
indent ^ "} else if " ^ rest ^ " {"
end else if t = "else" then
indent ^ "} else {"
Expand All @@ -238,21 +246,21 @@ let transform_control_flow line =
else if starts_with t "while " then begin
let rest = String.sub t 6 (String.length t - 6) in
let rest = if ends_with rest " do" then String.sub rest 0 (String.length rest - 3) else rest in
open_while (String.trim rest)
open_while (apply_cond_subs (String.trim rest))
end else if starts_with t "for " then begin
let rest = String.sub t 4 (String.length t - 4) in
let rest = if ends_with rest " do" then String.sub rest 0 (String.length rest - 3) else rest in
open_for (String.trim rest)
open_for (apply_cond_subs (String.trim rest))
end else if starts_with t "match " then begin
let rest = String.sub t 6 (String.length t - 6) in
let rest = if ends_with rest " on" then String.sub rest 0 (String.length rest - 3) else rest in
indent ^ "match " ^ String.trim rest ^ " {"
indent ^ "match " ^ apply_cond_subs (String.trim rest) ^ " {"
end else if t = "end if" || t = "end while" || t = "end for"
|| t = "end match" || t = "end" || t = "fi" || t = "od" then
indent ^ "}"
else line

(* ─── Line-by-line transform ────────────────────────────────────────────── *)
(* ─── Line-by-line transform ──────────────────────────────────────────────── *)

let transform_line line =
let t = String.trim line in
Expand All @@ -263,8 +271,9 @@ let transform_line line =
(* 1. Convert double-dash comments *)
let line = transform_double_dash_comment line in
let t = String.trim line in
if starts_with t "//" then line
(* 2. Function/procedure declarations *)
if starts_with t "function " || starts_with t "procedure " then
else if starts_with t "function " || starts_with t "procedure " then
transform_function_decl t
(* 3. set ... to ... *)
else if starts_with t "set " then
Expand Down Expand Up @@ -302,12 +311,37 @@ let transform_line line =
end
end

(* ─── File-level entry points ─────────────────────────────────────────── *)
(* ─── File-level entry points ────────────────────────────────────────────── *)

let transform_source source =
let lines = String.split_on_char '\n' source in
let out = List.map transform_line lines in
String.concat "\n" out
let transformed = Array.of_list (List.map transform_line lines) in
let n = Array.length transformed in
let result = Array.copy transformed in
let depth = ref 0 in
let next_non_empty i =
let j = ref (i + 1) in
while !j < n && String.trim transformed.(!j) = "" do incr j done;
if !j >= n then "" else String.trim transformed.(!j)
in
for i = 0 to n - 1 do
let line = transformed.(i) in
let t = String.trim line in
let len = String.length t in
let ends_with_brace = len > 0 && t.[len - 1] = '{' in
let is_close_brace = t = "}" in
if is_close_brace && !depth > 0 then decr depth;
if !depth > 0 && t <> "" && not ends_with_brace && not is_close_brace
&& not (len > 1 && t.[0] = '/' && t.[1] = '/')
then begin
let next = next_non_empty i in
let next_is_close = String.length next > 0 && next.[0] = '}' in
if not next_is_close && (len = 0 || t.[len - 1] <> ';') then
result.(i) <- line ^ ";"
end;
if ends_with_brace then incr depth
done;
String.concat "\n" (Array.to_list result)

let parse_file_pseudocode path =
let source = In_channel.with_open_text path In_channel.input_all in
Expand Down
11 changes: 5 additions & 6 deletions tests/faces/hello-pseudo.expected.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-||-later
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
//
// PseudoScript face. Distinctive features exercised:
// function ... do ... end blocks, `--` Haskell/SQL-style comments.
// (Note: pseudocode-face does not yet auto-insert statement separators,
// so this minimal demo keeps each function body to a single expression.
// See examples/faces/README.adoc for a list of pending transformer gaps.)
// function ... do ... end blocks, `--` Haskell/SQL-style comments,
// `set X to Y` bindings, `output X` I/O, multi-statement function bodies.
// face: pseudoscript

effect IO {
fn println(s: String) -> ();
}

fn main() -{IO}-> () {
println("Hello, PseudoScript!")
let greeting = "Hello, PseudoScript!";
println(greeting)
}
Loading