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
58 changes: 30 additions & 28 deletions lib/lucid_face.ml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
- Span fidelity: errors refer to the canonical text, not Lucid source.
*)

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

let is_id_char c =
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
Expand All @@ -70,16 +70,7 @@ let indent_of line =
while !i < len && (line.[!i] = ' ' || line.[!i] = '\t') do incr i done;
!i

(* ─── Comment handling ───────────────────────────────────────────────── *)

(** Convert a leading [-- comment] to [// comment] at the original indent. *)
let convert_dashdash_comment line =
let trimmed = String.trim line in
if starts_with trimmed "--" then
let indent_len = String.length line - String.length trimmed in
let indent = String.sub line 0 indent_len in
indent ^ "//" ^ String.sub trimmed 2 (String.length trimmed - 2)
else line
(* ─── Comment handling ────────────────────────────────────────────── *)

(** Strip a trailing [-- ...] comment (respecting string literals) and
return [(code_part, comment_text option)]. *)
Expand All @@ -105,7 +96,7 @@ let strip_dashdash_comment line =
else (String.sub line 0 !cut,
Some (String.sub line (!cut + 2) (len - !cut - 2)))

(* ─── Word-level keyword substitution ────────────────────────────────── *)
(* ─── Word-level keyword substitution ───────────────────────────────────── *)

let replace_word ~from_w ~to_w s =
let flen = String.length from_w in
Expand Down Expand Up @@ -144,7 +135,7 @@ let apply_logic_subs s =
let s = replace_word ~from_w:"not" ~to_w:"!" s in
s

(* ─── Module path helpers ────────────────────────────────────────────── *)
(* ─── Module path helpers ────────────────────────────────────────────────── *)

(** [Data.Map.Strict] → [Data::Map::Strict]. *)
let dots_to_colons s =
Expand All @@ -156,7 +147,7 @@ let dots_to_colons s =
) s;
Buffer.contents buf

(* ─── Import translation ─────────────────────────────────────────────── *)
(* ─── Import translation ───────────────────────────────────────────────────── *)

(** Try to transform a PureScript [import …] line. *)
let transform_import_line stripped =
Expand Down Expand Up @@ -221,7 +212,7 @@ let transform_import_line stripped =
end
end

(* ─── Module declaration ─────────────────────────────────────────────── *)
(* ─── Module declaration ───────────────────────────────────────────────────── *)

(** [module Foo where] / [module Foo (exports) where] → [module Foo;].
Canonical AffineScript uses [module] as a file-level header (no
Expand Down Expand Up @@ -252,7 +243,7 @@ let transform_module_line stripped =
Some (Printf.sprintf "module %s;" mod_path)
end

(* ─── Type signature handling ────────────────────────────────────────── *)
(* ─── Type signature handling ──────────────────────────────────────────────── *)

(** A line of the form [name :: type ...] is a type signature. Lucid keeps
it as a comment so the canonical type inferer is the source of truth.
Expand All @@ -267,7 +258,7 @@ let is_type_signature stripped =
has_dcolon && len > 0
&& (stripped.[0] >= 'a' && stripped.[0] <= 'z' || stripped.[0] = '_')

(* ─── Data / class / instance declarations ───────────────────────────── *)
(* ─── Data / class / instance declarations ───────────────────────────────────── *)

(** [data Foo a b = Ctor1 a | Ctor2] → [type Foo[a, b] = Ctor1(a) | Ctor2].
Best-effort: parameterised constructor arguments wrapped in parens. *)
Expand Down Expand Up @@ -350,7 +341,7 @@ let transform_instance_decl stripped =
| _ -> Some (Printf.sprintf "impl %s {" body)
end

(* ─── Function equations ─────────────────────────────────────────────── *)
(* ─── Function equations ────────────────────────────────────────────────────── *)

(** [f x y = expr] — wrap parameters and emit canonical [fn].
Returns [None] when the line isn't a recognisable equation. *)
Expand Down Expand Up @@ -395,7 +386,7 @@ let transform_equation stripped =
end
end

(* ─── Expression-level substitutions ─────────────────────────────────── *)
(* ─── Expression-level substitutions ───────────────────────────────────────────── *)

(** [\x -> body] / [\x y -> body] → [(x, y) => body]. *)
let transform_lambda_inline s =
Expand Down Expand Up @@ -535,11 +526,14 @@ let render_block_head head marker =
head ^ " = {"
| _ -> head

(* ─── Main transformer ───────────────────────────────────────────────── *)
(* ─── Main transformer ───────────────────────────────────────────────────────── *)

let is_blank_line raw =
let (code, _) = strip_dashdash_comment (String.trim raw) in
String.trim code = ""
let t = String.trim raw in
if starts_with t "//" then true
else
let (code, _) = strip_dashdash_comment t in
String.trim code = ""

let transform_source source =
let lines = Array.of_list (String.split_on_char '\n' source) in
Expand Down Expand Up @@ -571,8 +565,7 @@ let transform_source source =
for i = 0 to n - 1 do
let raw_line = lines.(i) in
let ind = indent_of raw_line in
let line = convert_dashdash_comment raw_line in
let (code_part, comment_opt) = strip_dashdash_comment (String.trim line) in
let (code_part, comment_opt) = strip_dashdash_comment (String.trim raw_line) in
let stripped = String.trim code_part in

let with_comment line_text =
Expand All @@ -583,8 +576,14 @@ let transform_source source =

if stripped = "" then begin
(match comment_opt with
| Some c -> Buffer.add_string out ("// " ^ String.trim c ^ "\n")
| None -> Buffer.add_char out '\n')
| Some c ->
let c' = String.trim c in
Buffer.add_string out (if c' = "" then "//\n" else "// " ^ c' ^ "\n")
| None -> Buffer.add_char out '\n')
end else if starts_with stripped "//" || starts_with stripped "/*" then begin
(* Already-canonical comment lines pass through unchanged. *)
let indent_str = String.make ind ' ' in
Buffer.add_string out (indent_str ^ stripped ^ "\n")
end else if is_type_signature stripped then begin
(* Keep type signatures as a comment so the inferer drives types. *)
let indent_str = String.make ind ' ' in
Expand Down Expand Up @@ -659,9 +658,12 @@ let transform_source source =
for _ = 1 to !toplevel_braces do
Buffer.add_string out "}\n"
done;
Buffer.contents out
let s = Buffer.contents out in
let l = String.length s in
if l >= 2 && s.[l-1] = '\n' && s.[l-2] = '\n' then String.sub s 0 (l-1)
else s

(* ─── Entry points ───────────────────────────────────────────────────── *)
(* ─── Entry points ────────────────────────────────────────────────────────────── *)

let parse_string_lucid ~file content =
let canonical = transform_source content in
Expand Down
21 changes: 10 additions & 11 deletions tests/faces/hello-lucid.expected.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0-or-later;
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell;
//;
// LucidScript face. Distinctive features exercised: module declaration;
// with `where`, type signatures kept as comments, equation-style;
// function definitions, `; // ` line comments. (The unit-parameter idiom
// `main () = …` lowers to canonical `fn main() { … }`. Function;
// application uses canonical paren syntax `f(x)` rather than Haskell;
// currying `f x` — see examples/faces/README.adoc.);
// face: lucidscript;
// SPDX-License-Identifier: AGPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
//
// LucidScript face. Distinctive features exercised: module declaration
// with `where`, type signatures kept as comments, equation-style
// function definitions, `--` line comments. (The unit-parameter idiom
// `main () = …` lowers to canonical `fn main() { … }`. Function
// application uses canonical paren syntax `f(x)` rather than Haskell
// currying `f x` — see examples/faces/README.adoc.)
// face: lucidscript

module Hello;

Expand All @@ -17,4 +17,3 @@ effect IO {

// main :: -{IO}-> ()
fn main() { println("Hello, LucidScript!") }

Loading