Compare commits
129 Commits
loops/mini
...
loops/kern
| Author | SHA1 | Date | |
|---|---|---|---|
| f7bd3a6bf1 | |||
| d5d77a3611 | |||
| 67449f5b0c | |||
| 6d8f11e093 | |||
| 78dab5b28c | |||
| 1fb852ef64 | |||
| b80871ac4f | |||
| 9ff5d1b464 | |||
| 5fa6c6ecc1 | |||
| a4a7753314 | |||
| af8d10a717 | |||
| c21eb9d5ad | |||
| d896685555 | |||
| bf7ec55e92 | |||
| 45789520ce | |||
| b91d8cf72e | |||
| 0da39de68a | |||
| 7e57e0b215 | |||
| cbba642d7f | |||
| 57a84b372d | |||
| 416546cc07 | |||
| f0c0a5e19f | |||
| 55ecdf24bb | |||
| 50b69bcbd0 | |||
| 14986d787d | |||
| 9dd9fb9c37 | |||
| e8246340fc | |||
| 92619301e2 | |||
| 59bec68dcc | |||
| e9d4d107a6 | |||
| 92f6f187b7 | |||
| c361946974 | |||
| 62da10030b | |||
| 0e30cf1af6 | |||
| 21028c4fb0 | |||
| b3c9d9eb3a | |||
| 7415dd020e | |||
| f4c155c9c5 | |||
| 0528a5cfa7 | |||
| 2fa0bb4df1 | |||
| 0d2eede5fb | |||
| a9eb821cce | |||
| d0b358eca2 | |||
| badb428100 | |||
| e83c01cdcc | |||
| 69078a59a9 | |||
| 982b9d6be6 | |||
| f5d3b1df19 | |||
| bf782d9c49 | |||
| bcdd137d6f | |||
| 0b3610a63a | |||
| 544e79f533 | |||
| 2b8c1a506c | |||
| 197c073308 | |||
| 203f81004d | |||
| 04b0e61a33 | |||
| 1eb9d0f8d2 | |||
| f182d04e6a | |||
| ab2c40c14c | |||
| d3c34b46b9 | |||
| 80dac0051d | |||
| b661318a45 | |||
| 47d9d07f2e | |||
| d75c61d408 | |||
| f1fea0f2f1 | |||
| 21e6351657 | |||
| a677585639 | |||
| c04f38a1ba | |||
| 0b4b7c9dbc | |||
| b13819c50c | |||
| f26f25f146 | |||
| d9cf00f287 | |||
| 0c0ed0605a | |||
| 63c1e17c75 | |||
| a4fd57cff1 | |||
| 76d141737a | |||
| 9307437679 | |||
| b89e321007 | |||
| ca9e12fc57 | |||
| f0e1d2d615 | |||
| 2adbc101fa | |||
| 4205989aee | |||
| 49252eaa5c | |||
| ebbf0fc10c | |||
| 8dfb3f6387 | |||
| 5a8c25bec7 | |||
| c821e21f94 | |||
| 5605fe1cc2 | |||
| 379bb93f14 | |||
| 7ce0c797f3 | |||
| 34513908df | |||
| 208953667b | |||
| e6d6273265 | |||
| e95ca4624b | |||
| e1a020dc90 | |||
| b0974b58c0 | |||
| 6620c0ac06 | |||
| 95cf653ba9 | |||
| 12de24e3a0 | |||
| 180b9009bf | |||
| 9b0f42defb | |||
| a29bb6feca | |||
| d2638170db | |||
| a5c41d2573 | |||
| 882815e612 | |||
| e27daee4a8 | |||
| ef33e9a43a | |||
| 1b7bd86b43 | |||
| e5fe9ad2d4 | |||
| 2d373da06b | |||
| 25cf832998 | |||
| 29542ba9d2 | |||
| c2de220cce | |||
| d523df30c2 | |||
| 1b844f6a19 | |||
| 5f758d27c1 | |||
| 51f57aa2fa | |||
| 31308602ca | |||
| 788e8682f5 | |||
| bb134b88e3 | |||
| d8dec07df3 | |||
| 39c7baa44c | |||
| ee74a396c5 | |||
| a8997ab452 | |||
| 54b7a6aed0 | |||
| 80d6507e57 | |||
| 685fcd11d5 | |||
| f6efba410a | |||
| 4a35998469 |
@@ -528,6 +528,183 @@ let () =
|
||||
| [Rational (_, d)] -> Integer d
|
||||
| [Integer _] -> Integer 1
|
||||
| _ -> raise (Eval_error "denominator: expected rational or integer"));
|
||||
(* printf-spec: apply one Tcl/printf format spec to one arg.
|
||||
spec is like "%5.2f", "%-10s", "%x", "%c", "%d". Always starts with %
|
||||
and ends with the conversion char. Supports d i u x X o c s f e g.
|
||||
Coerces arg to the right type per conversion. *)
|
||||
register "printf-spec" (fun args ->
|
||||
let spec_str, arg = match args with
|
||||
| [String s; v] -> (s, v)
|
||||
| _ -> raise (Eval_error "printf-spec: (spec arg)")
|
||||
in
|
||||
let n = String.length spec_str in
|
||||
if n < 2 || spec_str.[0] <> '%' then
|
||||
raise (Eval_error ("printf-spec: invalid spec " ^ spec_str));
|
||||
let type_char = spec_str.[n - 1] in
|
||||
let to_int v = match v with
|
||||
| Integer i -> i
|
||||
| Number f -> int_of_float f
|
||||
| String s ->
|
||||
let s = String.trim s in
|
||||
(try int_of_string s
|
||||
with _ ->
|
||||
try int_of_float (float_of_string s)
|
||||
with _ -> 0)
|
||||
| Bool true -> 1 | Bool false -> 0
|
||||
| _ -> 0
|
||||
in
|
||||
let to_float v = match v with
|
||||
| Number f -> f
|
||||
| Integer i -> float_of_int i
|
||||
| String s ->
|
||||
let s = String.trim s in
|
||||
(try float_of_string s with _ -> 0.0)
|
||||
| _ -> 0.0
|
||||
in
|
||||
let to_string v = match v with
|
||||
| String s -> s
|
||||
| Integer i -> string_of_int i
|
||||
| Number f -> Sx_types.format_number f
|
||||
| Bool true -> "1" | Bool false -> "0"
|
||||
| Nil -> ""
|
||||
| _ -> Sx_types.inspect v
|
||||
in
|
||||
try
|
||||
match type_char with
|
||||
| 'd' | 'i' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%d" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'u' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%u" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'x' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%x" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'X' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%X" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'o' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%o" in
|
||||
String (Printf.sprintf fmt (to_int arg))
|
||||
| 'c' ->
|
||||
let n_val = to_int arg in
|
||||
let body = String.sub spec_str 0 (n - 1) in
|
||||
let fmt = Scanf.format_from_string (body ^ "s") "%s" in
|
||||
String (Printf.sprintf fmt (String.make 1 (Char.chr (n_val land 0xff))))
|
||||
| 's' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%s" in
|
||||
String (Printf.sprintf fmt (to_string arg))
|
||||
| 'f' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%f" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| 'e' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%e" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| 'E' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%E" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| 'g' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%g" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| 'G' ->
|
||||
let fmt = Scanf.format_from_string spec_str "%G" in
|
||||
String (Printf.sprintf fmt (to_float arg))
|
||||
| _ -> raise (Eval_error ("printf-spec: unsupported conversion " ^ String.make 1 type_char))
|
||||
with
|
||||
| Eval_error _ as e -> raise e
|
||||
| _ -> raise (Eval_error ("printf-spec: invalid format " ^ spec_str)));
|
||||
|
||||
(* scan-spec: apply one Tcl/scanf format spec to a string.
|
||||
Returns (consumed-count . parsed-value), or nil on failure. *)
|
||||
register "scan-spec" (fun args ->
|
||||
let spec_str, str = match args with
|
||||
| [String s; String input] -> (s, input)
|
||||
| _ -> raise (Eval_error "scan-spec: (spec input)")
|
||||
in
|
||||
let n = String.length spec_str in
|
||||
if n < 2 || spec_str.[0] <> '%' then
|
||||
raise (Eval_error ("scan-spec: invalid spec " ^ spec_str));
|
||||
let type_char = spec_str.[n - 1] in
|
||||
let len = String.length str in
|
||||
(* skip leading whitespace for non-%c/%s conversions *)
|
||||
let i = ref 0 in
|
||||
if type_char <> 'c' then
|
||||
while !i < len && (str.[!i] = ' ' || str.[!i] = '\t' || str.[!i] = '\n') do incr i done;
|
||||
let start = !i in
|
||||
try
|
||||
match type_char with
|
||||
| 'd' | 'i' ->
|
||||
let j = ref !i in
|
||||
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||
while !j < len && str.[!j] >= '0' && str.[!j] <= '9' do incr j done;
|
||||
if !j > start && (str.[start] >= '0' && str.[start] <= '9'
|
||||
|| (!j > start + 1 && (str.[start] = '-' || str.[start] = '+'))) then
|
||||
let n_val = int_of_string (String.sub str start (!j - start)) in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Integer n_val);
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 'x' | 'X' ->
|
||||
let j = ref !i in
|
||||
while !j < len &&
|
||||
((str.[!j] >= '0' && str.[!j] <= '9') ||
|
||||
(str.[!j] >= 'a' && str.[!j] <= 'f') ||
|
||||
(str.[!j] >= 'A' && str.[!j] <= 'F')) do incr j done;
|
||||
if !j > start then
|
||||
let n_val = int_of_string ("0x" ^ String.sub str start (!j - start)) in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Integer n_val);
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 'o' ->
|
||||
let j = ref !i in
|
||||
while !j < len && str.[!j] >= '0' && str.[!j] <= '7' do incr j done;
|
||||
if !j > start then
|
||||
let n_val = int_of_string ("0o" ^ String.sub str start (!j - start)) in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Integer n_val);
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 'f' | 'e' | 'g' ->
|
||||
let j = ref !i in
|
||||
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||
while !j < len && ((str.[!j] >= '0' && str.[!j] <= '9') || str.[!j] = '.') do incr j done;
|
||||
if !j < len && (str.[!j] = 'e' || str.[!j] = 'E') then begin
|
||||
incr j;
|
||||
if !j < len && (str.[!j] = '-' || str.[!j] = '+') then incr j;
|
||||
while !j < len && str.[!j] >= '0' && str.[!j] <= '9' do incr j done
|
||||
end;
|
||||
if !j > start then
|
||||
let f_val = float_of_string (String.sub str start (!j - start)) in
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Number f_val);
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 's' ->
|
||||
let j = ref !i in
|
||||
while !j < len && str.[!j] <> ' ' && str.[!j] <> '\t' && str.[!j] <> '\n' do incr j done;
|
||||
if !j > start then
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (String (String.sub str start (!j - start)));
|
||||
Hashtbl.replace d "consumed" (Integer !j);
|
||||
Dict d
|
||||
else Nil
|
||||
| 'c' ->
|
||||
if !i < len then
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "value" (Integer (Char.code str.[!i]));
|
||||
Hashtbl.replace d "consumed" (Integer (!i + 1));
|
||||
Dict d
|
||||
else Nil
|
||||
| _ -> raise (Eval_error ("scan-spec: unsupported conversion " ^ String.make 1 type_char))
|
||||
with
|
||||
| Eval_error _ as e -> raise e
|
||||
| _ -> Nil);
|
||||
|
||||
register "parse-int" (fun args ->
|
||||
let parse_leading_int s =
|
||||
let len = String.length s in
|
||||
@@ -3399,6 +3576,204 @@ let () =
|
||||
Nil
|
||||
| _ -> raise (Eval_error "channel-set-blocking!: (channel bool)"));
|
||||
|
||||
(* === Exec === run an external process; capture stdout *)
|
||||
register "exec-process" (fun args ->
|
||||
let items = match args with
|
||||
| [List xs] | [ListRef { contents = xs }] -> xs
|
||||
| _ -> raise (Eval_error "exec-process: (cmd-list)")
|
||||
in
|
||||
let argv = Array.of_list (List.map (function
|
||||
| String s -> s
|
||||
| v -> Sx_types.inspect v
|
||||
) items) in
|
||||
if Array.length argv = 0 then raise (Eval_error "exec: empty command");
|
||||
let (out_r, out_w) = Unix.pipe () in
|
||||
let (err_r, err_w) = Unix.pipe () in
|
||||
let pid =
|
||||
try Unix.create_process argv.(0) argv Unix.stdin out_w err_w
|
||||
with Unix.Unix_error (e, _, _) ->
|
||||
Unix.close out_r; Unix.close out_w;
|
||||
Unix.close err_r; Unix.close err_w;
|
||||
raise (Eval_error ("exec: " ^ Unix.error_message e))
|
||||
in
|
||||
Unix.close out_w;
|
||||
Unix.close err_w;
|
||||
let buf = Buffer.create 256 in
|
||||
let errbuf = Buffer.create 64 in
|
||||
let chunk = Bytes.create 4096 in
|
||||
let read_all fd target =
|
||||
try
|
||||
let stop = ref false in
|
||||
while not !stop do
|
||||
let n = Unix.read fd chunk 0 (Bytes.length chunk) in
|
||||
if n = 0 then stop := true
|
||||
else Buffer.add_subbytes target chunk 0 n
|
||||
done
|
||||
with _ -> ()
|
||||
in
|
||||
read_all out_r buf;
|
||||
read_all err_r errbuf;
|
||||
Unix.close out_r;
|
||||
Unix.close err_r;
|
||||
let (_, status) = Unix.waitpid [] pid in
|
||||
let exit_code = match status with
|
||||
| Unix.WEXITED n -> n
|
||||
| Unix.WSIGNALED _ | Unix.WSTOPPED _ -> 1
|
||||
in
|
||||
let s = Buffer.contents buf in
|
||||
let trimmed =
|
||||
if String.length s > 0 && s.[String.length s - 1] = '\n'
|
||||
then String.sub s 0 (String.length s - 1) else s
|
||||
in
|
||||
if exit_code <> 0 then
|
||||
raise (Eval_error ("exec: child exited " ^ string_of_int exit_code
|
||||
^ (if Buffer.length errbuf > 0
|
||||
then ": " ^ Buffer.contents errbuf
|
||||
else "")))
|
||||
else String trimmed);
|
||||
|
||||
(* exec-pipeline: takes a list of words like Tcl `exec` would receive.
|
||||
Recognizes `|` as a stage separator and `> file`, `>> file`, `< file`,
|
||||
`2>@1` (stderr→stdout), `2> file`. Returns trimmed stdout of the last
|
||||
stage; raises Eval_error if the last stage exits non-zero. *)
|
||||
register "exec-pipeline" (fun args ->
|
||||
let items = match args with
|
||||
| [List xs] | [ListRef { contents = xs }] -> xs
|
||||
| _ -> raise (Eval_error "exec-pipeline: (word-list)")
|
||||
in
|
||||
let words = List.map (function
|
||||
| String s -> s
|
||||
| v -> Sx_types.inspect v
|
||||
) items in
|
||||
if words = [] then raise (Eval_error "exec: empty command");
|
||||
let split_stages ws =
|
||||
let rec loop acc cur = function
|
||||
| [] -> List.rev (List.rev cur :: acc)
|
||||
| "|" :: rest -> loop (List.rev cur :: acc) [] rest
|
||||
| w :: rest -> loop acc (w :: cur) rest
|
||||
in
|
||||
loop [] [] ws
|
||||
in
|
||||
let extract_redirs ws =
|
||||
let in_path = ref None in
|
||||
let out_path = ref None in
|
||||
let out_append = ref false in
|
||||
let err_path = ref None in
|
||||
let merge_err = ref false in
|
||||
let cleaned = ref [] in
|
||||
let rec loop = function
|
||||
| [] -> ()
|
||||
| "<" :: p :: rest -> in_path := Some p; loop rest
|
||||
| ">" :: p :: rest -> out_path := Some p; out_append := false; loop rest
|
||||
| ">>" :: p :: rest -> out_path := Some p; out_append := true; loop rest
|
||||
| "2>@1" :: rest -> merge_err := true; loop rest
|
||||
| "2>" :: p :: rest -> err_path := Some p; loop rest
|
||||
| w :: rest -> cleaned := w :: !cleaned; loop rest
|
||||
in
|
||||
loop ws;
|
||||
(List.rev !cleaned, !in_path, !out_path, !out_append, !err_path, !merge_err)
|
||||
in
|
||||
let stages = List.map extract_redirs (split_stages words) in
|
||||
if stages = [] then raise (Eval_error "exec: no stages");
|
||||
let n = List.length stages in
|
||||
let pipes = Array.init (max 0 (n - 1)) (fun _ -> Unix.pipe ()) in
|
||||
let (final_r, final_w) = Unix.pipe () in
|
||||
let (errstash_r, errstash_w) = Unix.pipe () in
|
||||
let pids = ref [] in
|
||||
let close_safe fd = try Unix.close fd with _ -> () in
|
||||
let open_in_redir = function
|
||||
| None -> Unix.stdin
|
||||
| Some path ->
|
||||
(try Unix.openfile path [Unix.O_RDONLY] 0o644
|
||||
with Unix.Unix_error (e, _, _) ->
|
||||
raise (Eval_error ("exec: open <" ^ path ^ ": " ^ Unix.error_message e)))
|
||||
in
|
||||
let open_out_redir path append =
|
||||
let flags = Unix.O_WRONLY :: Unix.O_CREAT :: (if append then [Unix.O_APPEND] else [Unix.O_TRUNC]) in
|
||||
try Unix.openfile path flags 0o644
|
||||
with Unix.Unix_error (e, _, _) ->
|
||||
raise (Eval_error ("exec: open >" ^ path ^ ": " ^ Unix.error_message e))
|
||||
in
|
||||
let stages_arr = Array.of_list stages in
|
||||
(try
|
||||
Array.iteri (fun i (cleaned, ip, op, app, ep, merge) ->
|
||||
if cleaned = [] then raise (Eval_error "exec: empty stage in pipeline");
|
||||
let argv = Array.of_list cleaned in
|
||||
let stdin_fd =
|
||||
if i = 0 then open_in_redir ip
|
||||
else fst pipes.(i - 1)
|
||||
in
|
||||
let stdout_fd =
|
||||
if i = n - 1 then
|
||||
(match op with
|
||||
| None -> final_w
|
||||
| Some path -> open_out_redir path app)
|
||||
else snd pipes.(i)
|
||||
in
|
||||
let stderr_fd =
|
||||
if merge then stdout_fd
|
||||
else (match ep with
|
||||
| None -> if i = n - 1 then errstash_w else Unix.stderr
|
||||
| Some path -> open_out_redir path false)
|
||||
in
|
||||
let pid =
|
||||
try Unix.create_process argv.(0) argv stdin_fd stdout_fd stderr_fd
|
||||
with Unix.Unix_error (e, _, _) ->
|
||||
raise (Eval_error ("exec: " ^ argv.(0) ^ ": " ^ Unix.error_message e))
|
||||
in
|
||||
pids := pid :: !pids;
|
||||
if i > 0 then close_safe (fst pipes.(i - 1));
|
||||
if i < n - 1 then close_safe (snd pipes.(i));
|
||||
if i = 0 && ip <> None then close_safe stdin_fd;
|
||||
if i = n - 1 && op <> None then close_safe stdout_fd;
|
||||
if not merge && ep <> None then close_safe stderr_fd
|
||||
) stages_arr
|
||||
with e ->
|
||||
close_safe final_r; close_safe final_w;
|
||||
close_safe errstash_r; close_safe errstash_w;
|
||||
Array.iter (fun (a,b) -> close_safe a; close_safe b) pipes;
|
||||
raise e);
|
||||
close_safe final_w;
|
||||
close_safe errstash_w;
|
||||
let buf = Buffer.create 256 in
|
||||
let errbuf = Buffer.create 64 in
|
||||
let chunk = Bytes.create 4096 in
|
||||
let read_all fd target =
|
||||
try
|
||||
let stop = ref false in
|
||||
while not !stop do
|
||||
let r = Unix.read fd chunk 0 (Bytes.length chunk) in
|
||||
if r = 0 then stop := true
|
||||
else Buffer.add_subbytes target chunk 0 r
|
||||
done
|
||||
with _ -> ()
|
||||
in
|
||||
read_all final_r buf;
|
||||
read_all errstash_r errbuf;
|
||||
close_safe final_r;
|
||||
close_safe errstash_r;
|
||||
let exit_codes = List.rev_map (fun pid ->
|
||||
let (_, st) = Unix.waitpid [] pid in
|
||||
match st with
|
||||
| Unix.WEXITED c -> c
|
||||
| _ -> 1
|
||||
) !pids in
|
||||
let final_code = match List.rev exit_codes with
|
||||
| [] -> 0
|
||||
| last :: _ -> last
|
||||
in
|
||||
let s = Buffer.contents buf in
|
||||
let trimmed =
|
||||
if String.length s > 0 && s.[String.length s - 1] = '\n'
|
||||
then String.sub s 0 (String.length s - 1) else s
|
||||
in
|
||||
if final_code <> 0 then
|
||||
raise (Eval_error ("exec: pipeline last stage exited " ^ string_of_int final_code
|
||||
^ (if Buffer.length errbuf > 0
|
||||
then ": " ^ Buffer.contents errbuf
|
||||
else "")))
|
||||
else String trimmed);
|
||||
|
||||
(* === Sockets === wrapping Unix.socket/connect/bind/listen/accept *)
|
||||
let resolve_inet_addr host =
|
||||
if host = "" || host = "0.0.0.0" then Unix.inet_addr_any
|
||||
|
||||
@@ -25,8 +25,9 @@
|
||||
; Glyph classification sets
|
||||
; ============================================================
|
||||
|
||||
(define apl-parse-op-glyphs
|
||||
(list "/" "\\" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@"))
|
||||
(define
|
||||
apl-parse-op-glyphs
|
||||
(list "/" "⌿" "\\" "⍀" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@"))
|
||||
|
||||
(define
|
||||
apl-parse-fn-glyphs
|
||||
@@ -82,22 +83,48 @@
|
||||
"⍎"
|
||||
"⍕"))
|
||||
|
||||
(define apl-quad-fn-names (list "⎕FMT"))
|
||||
(define apl-quad-fn-names (list "⎕FMT" "⎕←"))
|
||||
|
||||
(define
|
||||
apl-parse-op-glyph?
|
||||
(fn (v) (some (fn (g) (= g v)) apl-parse-op-glyphs)))
|
||||
(define apl-known-fn-names (list))
|
||||
|
||||
; ============================================================
|
||||
; Token accessors
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
apl-collect-fn-bindings
|
||||
(fn
|
||||
(stmt-groups)
|
||||
(set! apl-known-fn-names (list))
|
||||
(for-each
|
||||
(fn
|
||||
(toks)
|
||||
(when
|
||||
(and
|
||||
(>= (len toks) 3)
|
||||
(= (tok-type (nth toks 0)) :name)
|
||||
(= (tok-type (nth toks 1)) :assign)
|
||||
(= (tok-type (nth toks 2)) :lbrace))
|
||||
(set!
|
||||
apl-known-fn-names
|
||||
(cons (tok-val (nth toks 0)) apl-known-fn-names))))
|
||||
stmt-groups)))
|
||||
|
||||
(define
|
||||
apl-parse-op-glyph?
|
||||
(fn (v) (some (fn (g) (= g v)) apl-parse-op-glyphs)))
|
||||
|
||||
(define
|
||||
apl-parse-fn-glyph?
|
||||
(fn (v) (some (fn (g) (= g v)) apl-parse-fn-glyphs)))
|
||||
|
||||
(define tok-type (fn (tok) (get tok :type)))
|
||||
|
||||
; ============================================================
|
||||
; Collect trailing operators starting at index i
|
||||
; Returns {:ops (op ...) :end new-i}
|
||||
; ============================================================
|
||||
|
||||
(define tok-val (fn (tok) (get tok :value)))
|
||||
|
||||
(define
|
||||
@@ -107,8 +134,8 @@
|
||||
(and (= (tok-type tok) :glyph) (apl-parse-op-glyph? (tok-val tok)))))
|
||||
|
||||
; ============================================================
|
||||
; Collect trailing operators starting at index i
|
||||
; Returns {:ops (op ...) :end new-i}
|
||||
; Build a derived-fn node by chaining operators left-to-right
|
||||
; (+/¨ → (:derived-fn "¨" (:derived-fn "/" (:fn-glyph "+"))))
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
@@ -119,15 +146,17 @@
|
||||
(and (= (tok-type tok) :glyph) (apl-parse-fn-glyph? (tok-val tok)))
|
||||
(and
|
||||
(= (tok-type tok) :name)
|
||||
(some (fn (q) (= q (tok-val tok))) apl-quad-fn-names)))))
|
||||
(or
|
||||
(some (fn (q) (= q (tok-val tok))) apl-quad-fn-names)
|
||||
(some (fn (q) (= q (tok-val tok))) apl-known-fn-names))))))
|
||||
|
||||
; ============================================================
|
||||
; Find matching close bracket/paren/brace
|
||||
; Returns the index of the matching close token
|
||||
; ============================================================
|
||||
|
||||
(define collect-ops (fn (tokens i) (collect-ops-loop tokens i (list))))
|
||||
|
||||
; ============================================================
|
||||
; Build a derived-fn node by chaining operators left-to-right
|
||||
; (+/¨ → (:derived-fn "¨" (:derived-fn "/" (:fn-glyph "+"))))
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
collect-ops-loop
|
||||
(fn
|
||||
@@ -143,8 +172,10 @@
|
||||
{:end i :ops acc})))))
|
||||
|
||||
; ============================================================
|
||||
; Find matching close bracket/paren/brace
|
||||
; Returns the index of the matching close token
|
||||
; Segment collection: scan tokens left-to-right, building
|
||||
; a list of {:kind "val"/"fn" :node ast} segments.
|
||||
; Operators following function glyphs are merged into
|
||||
; derived-fn nodes during this pass.
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
@@ -163,12 +194,20 @@
|
||||
(find-matching-close-loop tokens start open-type close-type 1)))
|
||||
|
||||
; ============================================================
|
||||
; Segment collection: scan tokens left-to-right, building
|
||||
; a list of {:kind "val"/"fn" :node ast} segments.
|
||||
; Operators following function glyphs are merged into
|
||||
; derived-fn nodes during this pass.
|
||||
; Build tree from segment list
|
||||
;
|
||||
; The segments are in left-to-right order.
|
||||
; APL evaluates right-to-left, so the LEFTMOST function is
|
||||
; the outermost (last-evaluated) node.
|
||||
;
|
||||
; Patterns:
|
||||
; [val] → val node
|
||||
; [fn val ...] → (:monad fn (build-tree rest))
|
||||
; [val fn val ...] → (:dyad fn val (build-tree rest))
|
||||
; [val val ...] → (:vec val1 val2 ...) — strand
|
||||
; ============================================================
|
||||
|
||||
; Find the index of the first function segment (returns -1 if none)
|
||||
(define
|
||||
find-matching-close-loop
|
||||
(fn
|
||||
@@ -208,21 +247,9 @@
|
||||
collect-segments
|
||||
(fn (tokens) (collect-segments-loop tokens 0 (list))))
|
||||
|
||||
; ============================================================
|
||||
; Build tree from segment list
|
||||
;
|
||||
; The segments are in left-to-right order.
|
||||
; APL evaluates right-to-left, so the LEFTMOST function is
|
||||
; the outermost (last-evaluated) node.
|
||||
;
|
||||
; Patterns:
|
||||
; [val] → val node
|
||||
; [fn val ...] → (:monad fn (build-tree rest))
|
||||
; [val fn val ...] → (:dyad fn val (build-tree rest))
|
||||
; [val val ...] → (:vec val1 val2 ...) — strand
|
||||
; ============================================================
|
||||
|
||||
; Find the index of the first function segment (returns -1 if none)
|
||||
; Build an array node from 0..n value segments
|
||||
; If n=1 → return that segment's node
|
||||
; If n>1 → return (:vec node1 node2 ...)
|
||||
(define
|
||||
collect-segments-loop
|
||||
(fn
|
||||
@@ -242,24 +269,47 @@
|
||||
((= tt :str)
|
||||
(collect-segments-loop tokens (+ i 1) (append acc {:kind "val" :node (list :str tv)})))
|
||||
((= tt :name)
|
||||
(if
|
||||
(some (fn (q) (= q tv)) apl-quad-fn-names)
|
||||
(let
|
||||
((op-result (collect-ops tokens (+ i 1))))
|
||||
(cond
|
||||
((and (< (+ i 1) (len tokens)) (= (tok-type (nth tokens (+ i 1))) :assign))
|
||||
(let
|
||||
((ops (get op-result :ops)) (ni (get op-result :end)))
|
||||
((rhs-tokens (slice tokens (+ i 2) (len tokens))))
|
||||
(let
|
||||
((fn-node (build-derived-fn (list :fn-glyph tv) ops)))
|
||||
((rhs-expr (parse-apl-expr rhs-tokens)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
ni
|
||||
(append acc {:kind "fn" :node fn-node})))))
|
||||
(let
|
||||
((br (maybe-bracket (list :name tv) tokens (+ i 1))))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(nth br 1)
|
||||
(append acc {:kind "val" :node (nth br 0)})))))
|
||||
(len tokens)
|
||||
(append acc {:kind "val" :node (list :assign-expr tv rhs-expr)})))))
|
||||
((some (fn (q) (= q tv)) apl-quad-fn-names)
|
||||
(let
|
||||
((op-result (collect-ops tokens (+ i 1))))
|
||||
(let
|
||||
((ops (get op-result :ops))
|
||||
(ni (get op-result :end)))
|
||||
(let
|
||||
((fn-node (build-derived-fn (list :fn-glyph tv) ops)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
ni
|
||||
(append acc {:kind "fn" :node fn-node}))))))
|
||||
((some (fn (q) (= q tv)) apl-known-fn-names)
|
||||
(let
|
||||
((op-result (collect-ops tokens (+ i 1))))
|
||||
(let
|
||||
((ops (get op-result :ops))
|
||||
(ni (get op-result :end)))
|
||||
(let
|
||||
((fn-node (build-derived-fn (list :fn-name tv) ops)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
ni
|
||||
(append acc {:kind "fn" :node fn-node}))))))
|
||||
(else
|
||||
(let
|
||||
((br (maybe-bracket (list :name tv) tokens (+ i 1))))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(nth br 1)
|
||||
(append acc {:kind "val" :node (nth br 0)}))))))
|
||||
((= tt :lparen)
|
||||
(let
|
||||
((end (find-matching-close tokens (+ i 1) :lparen :rparen)))
|
||||
@@ -267,11 +317,23 @@
|
||||
((inner-tokens (slice tokens (+ i 1) end))
|
||||
(after (+ end 1)))
|
||||
(let
|
||||
((br (maybe-bracket (parse-apl-expr inner-tokens) tokens after)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(nth br 1)
|
||||
(append acc {:kind "val" :node (nth br 0)}))))))
|
||||
((inner-segs (collect-segments inner-tokens)))
|
||||
(if
|
||||
(and
|
||||
(>= (len inner-segs) 2)
|
||||
(every? (fn (s) (= (get s :kind) "fn")) inner-segs))
|
||||
(let
|
||||
((train-node (cons :train (map (fn (s) (get s :node)) inner-segs))))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
after
|
||||
(append acc {:kind "fn" :node train-node})))
|
||||
(let
|
||||
((br (maybe-bracket (parse-apl-expr inner-tokens) tokens after)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(nth br 1)
|
||||
(append acc {:kind "val" :node (nth br 0)}))))))))
|
||||
((= tt :lbrace)
|
||||
(let
|
||||
((end (find-matching-close tokens (+ i 1) :lbrace :rbrace)))
|
||||
@@ -282,10 +344,22 @@
|
||||
((= tt :glyph)
|
||||
(cond
|
||||
((or (= tv "⍺") (= tv "⍵"))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
(append acc {:kind "val" :node (list :name tv)})))
|
||||
(if
|
||||
(and
|
||||
(< (+ i 1) (len tokens))
|
||||
(= (tok-type (nth tokens (+ i 1))) :assign))
|
||||
(let
|
||||
((rhs-tokens (slice tokens (+ i 2) (len tokens))))
|
||||
(let
|
||||
((rhs-expr (parse-apl-expr rhs-tokens)))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(len tokens)
|
||||
(append acc {:kind "val" :node (list :assign-expr tv rhs-expr)}))))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
(append acc {:kind "val" :node (list :name tv)}))))
|
||||
((= tv "∇")
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
@@ -340,15 +414,24 @@
|
||||
ni
|
||||
(append acc {:kind "fn" :node fn-node})))))))
|
||||
((apl-parse-op-glyph? tv)
|
||||
(collect-segments-loop tokens (+ i 1) acc))
|
||||
(if
|
||||
(or (= tv "/") (= tv "⌿") (= tv "\\") (= tv "⍀"))
|
||||
(collect-segments-loop
|
||||
tokens
|
||||
(+ i 1)
|
||||
(append acc {:kind "fn" :node (list :fn-glyph tv)}))
|
||||
(collect-segments-loop tokens (+ i 1) acc)))
|
||||
(true (collect-segments-loop tokens (+ i 1) acc))))
|
||||
(true (collect-segments-loop tokens (+ i 1) acc))))))))
|
||||
|
||||
(define find-first-fn (fn (segs) (find-first-fn-loop segs 0)))
|
||||
|
||||
; Build an array node from 0..n value segments
|
||||
; If n=1 → return that segment's node
|
||||
; If n>1 → return (:vec node1 node2 ...)
|
||||
|
||||
; ============================================================
|
||||
; Split token list on statement separators (diamond / newline)
|
||||
; Only splits at depth 0 (ignores separators inside { } or ( ) )
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
find-first-fn-loop
|
||||
(fn
|
||||
@@ -370,10 +453,9 @@
|
||||
(get (first segs) :node)
|
||||
(cons :vec (map (fn (s) (get s :node)) segs)))))
|
||||
|
||||
|
||||
; ============================================================
|
||||
; Split token list on statement separators (diamond / newline)
|
||||
; Only splits at depth 0 (ignores separators inside { } or ( ) )
|
||||
; Parse a dfn body (tokens between { and })
|
||||
; Handles guard expressions: cond : expr
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
@@ -408,11 +490,6 @@
|
||||
split-statements
|
||||
(fn (tokens) (split-statements-loop tokens (list) (list) 0)))
|
||||
|
||||
; ============================================================
|
||||
; Parse a dfn body (tokens between { and })
|
||||
; Handles guard expressions: cond : expr
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
split-statements-loop
|
||||
(fn
|
||||
@@ -467,6 +544,10 @@
|
||||
((stmt-groups (split-statements tokens)))
|
||||
(let ((stmts (map parse-dfn-stmt stmt-groups))) (cons :dfn stmts)))))
|
||||
|
||||
; ============================================================
|
||||
; Parse a single statement (assignment or expression)
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
parse-dfn-stmt
|
||||
(fn
|
||||
@@ -483,12 +564,17 @@
|
||||
(parse-apl-expr body-tokens)))
|
||||
(parse-stmt tokens)))))
|
||||
|
||||
; ============================================================
|
||||
; Parse an expression from a flat token list
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
find-top-level-colon
|
||||
(fn (tokens i) (find-top-level-colon-loop tokens i 0)))
|
||||
|
||||
; ============================================================
|
||||
; Parse a single statement (assignment or expression)
|
||||
; Main entry point
|
||||
; parse-apl: string → AST
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
@@ -508,10 +594,6 @@
|
||||
((and (= tt :colon) (= depth 0)) i)
|
||||
(true (find-top-level-colon-loop tokens (+ i 1) depth)))))))
|
||||
|
||||
; ============================================================
|
||||
; Parse an expression from a flat token list
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
parse-stmt
|
||||
(fn
|
||||
@@ -526,11 +608,6 @@
|
||||
(parse-apl-expr (slice tokens 2)))
|
||||
(parse-apl-expr tokens))))
|
||||
|
||||
; ============================================================
|
||||
; Main entry point
|
||||
; parse-apl: string → AST
|
||||
; ============================================================
|
||||
|
||||
(define
|
||||
parse-apl-expr
|
||||
(fn
|
||||
@@ -547,13 +624,52 @@
|
||||
((tokens (apl-tokenize src)))
|
||||
(let
|
||||
((stmt-groups (split-statements tokens)))
|
||||
(if
|
||||
(= (len stmt-groups) 0)
|
||||
nil
|
||||
(begin
|
||||
(apl-collect-fn-bindings stmt-groups)
|
||||
(if
|
||||
(= (len stmt-groups) 1)
|
||||
(parse-stmt (first stmt-groups))
|
||||
(cons :program (map parse-stmt stmt-groups))))))))
|
||||
(= (len stmt-groups) 0)
|
||||
nil
|
||||
(if
|
||||
(= (len stmt-groups) 1)
|
||||
(parse-stmt (first stmt-groups))
|
||||
(cons :program (map parse-stmt stmt-groups)))))))))
|
||||
|
||||
(define
|
||||
split-bracket-loop
|
||||
(fn
|
||||
(tokens current acc depth)
|
||||
(if
|
||||
(= (len tokens) 0)
|
||||
(append acc (list current))
|
||||
(let
|
||||
((tok (first tokens)) (more (rest tokens)))
|
||||
(let
|
||||
((tt (tok-type tok)))
|
||||
(cond
|
||||
((or (= tt :lparen) (= tt :lbrace) (= tt :lbracket))
|
||||
(split-bracket-loop
|
||||
more
|
||||
(append current (list tok))
|
||||
acc
|
||||
(+ depth 1)))
|
||||
((or (= tt :rparen) (= tt :rbrace) (= tt :rbracket))
|
||||
(split-bracket-loop
|
||||
more
|
||||
(append current (list tok))
|
||||
acc
|
||||
(- depth 1)))
|
||||
((and (= tt :semi) (= depth 0))
|
||||
(split-bracket-loop
|
||||
more
|
||||
(list)
|
||||
(append acc (list current))
|
||||
depth))
|
||||
(else
|
||||
(split-bracket-loop more (append current (list tok)) acc depth))))))))
|
||||
|
||||
(define
|
||||
split-bracket-content
|
||||
(fn (tokens) (split-bracket-loop tokens (list) (list) 0)))
|
||||
|
||||
(define
|
||||
maybe-bracket
|
||||
@@ -569,8 +685,17 @@
|
||||
((inner-tokens (slice tokens (+ after 1) end))
|
||||
(next-after (+ end 1)))
|
||||
(let
|
||||
((idx-expr (parse-apl-expr inner-tokens)))
|
||||
(let
|
||||
((indexed (list :dyad (list :fn-glyph "⌷") idx-expr val-node)))
|
||||
(maybe-bracket indexed tokens next-after)))))
|
||||
((sections (split-bracket-content inner-tokens)))
|
||||
(if
|
||||
(= (len sections) 1)
|
||||
(let
|
||||
((idx-expr (parse-apl-expr inner-tokens)))
|
||||
(let
|
||||
((indexed (list :dyad (list :fn-glyph "⌷") idx-expr val-node)))
|
||||
(maybe-bracket indexed tokens next-after)))
|
||||
(let
|
||||
((axis-exprs (map (fn (toks) (if (= (len toks) 0) :all (parse-apl-expr toks))) sections)))
|
||||
(let
|
||||
((indexed (cons :bracket (cons val-node axis-exprs))))
|
||||
(maybe-bracket indexed tokens next-after)))))))
|
||||
(list val-node after))))
|
||||
|
||||
@@ -808,6 +808,25 @@
|
||||
((picked (map (fn (i) (nth arr-ravel i)) kept)))
|
||||
(make-array (list (len picked)) picked))))))
|
||||
|
||||
(define
|
||||
apl-compress-first
|
||||
(fn
|
||||
(mask arr)
|
||||
(let
|
||||
((mask-ravel (get mask :ravel))
|
||||
(shape (get arr :shape))
|
||||
(ravel (get arr :ravel)))
|
||||
(if
|
||||
(< (len shape) 2)
|
||||
(apl-compress mask arr)
|
||||
(let
|
||||
((rows (first shape)) (cols (last shape)))
|
||||
(let
|
||||
((kept-rows (filter (fn (i) (not (= 0 (nth mask-ravel i)))) (range 0 rows))))
|
||||
(let
|
||||
((new-ravel (reduce (fn (acc r) (append acc (map (fn (j) (nth ravel (+ (* r cols) j))) (range 0 cols)))) (list) kept-rows)))
|
||||
(make-array (cons (len kept-rows) (rest shape)) new-ravel))))))))
|
||||
|
||||
(define
|
||||
apl-primes
|
||||
(fn
|
||||
@@ -883,7 +902,7 @@
|
||||
(let
|
||||
((sub (apl-permutations (- n 1))))
|
||||
(reduce
|
||||
(fn (acc p) (append acc (apl-insert-everywhere n p)))
|
||||
(fn (acc p) (append (apl-insert-everywhere n p) acc))
|
||||
(list)
|
||||
sub)))))
|
||||
|
||||
@@ -985,6 +1004,60 @@
|
||||
(some (fn (c) (= c 0)) codes)
|
||||
(some (fn (c) (= c (nth e 1))) codes)))))
|
||||
|
||||
(define apl-rng-state 12345)
|
||||
|
||||
(define apl-rng-seed! (fn (s) (set! apl-rng-state s)))
|
||||
|
||||
(define
|
||||
apl-rng-next!
|
||||
(fn
|
||||
()
|
||||
(begin
|
||||
(set!
|
||||
apl-rng-state
|
||||
(mod (+ (* apl-rng-state 1103515245) 12345) 2147483648))
|
||||
apl-rng-state)))
|
||||
|
||||
(define
|
||||
apl-roll
|
||||
(fn
|
||||
(arr)
|
||||
(let
|
||||
((n (if (scalar? arr) (first (get arr :ravel)) (first (get arr :ravel)))))
|
||||
(apl-scalar (+ apl-io (mod (apl-rng-next!) n))))))
|
||||
|
||||
(define
|
||||
apl-cartesian
|
||||
(fn
|
||||
(lists)
|
||||
(if
|
||||
(= (len lists) 0)
|
||||
(list (list))
|
||||
(let
|
||||
((rest-prods (apl-cartesian (rest lists))))
|
||||
(reduce
|
||||
(fn (acc x) (append acc (map (fn (p) (cons x p)) rest-prods)))
|
||||
(list)
|
||||
(first lists))))))
|
||||
|
||||
(define
|
||||
apl-bracket-multi
|
||||
(fn
|
||||
(axes arr)
|
||||
(let
|
||||
((shape (get arr :shape)) (ravel (get arr :ravel)))
|
||||
(let
|
||||
((rank (len shape)) (strides (apl-strides shape)))
|
||||
(let
|
||||
((axis-info (map (fn (i) (let ((a (nth axes i))) (cond ((= a nil) {:idxs (range 0 (nth shape i)) :scalar? false}) ((= (len (get a :shape)) 0) {:idxs (list (- (first (get a :ravel)) apl-io)) :scalar? true}) (else {:idxs (map (fn (x) (- x apl-io)) (get a :ravel)) :scalar? false})))) (range 0 rank))))
|
||||
(let
|
||||
((cells (apl-cartesian (map (fn (a) (get a :idxs)) axis-info))))
|
||||
(let
|
||||
((result-ravel (map (fn (cell) (let ((flat (reduce + 0 (map (fn (i) (* (nth cell i) (nth strides i))) (range 0 rank))))) (nth ravel flat))) cells)))
|
||||
(let
|
||||
((result-shape (filter (fn (x) (>= x 0)) (map (fn (i) (let ((a (nth axis-info i))) (if (get a :scalar?) -1 (len (get a :idxs))))) (range 0 rank)))))
|
||||
(make-array result-shape result-ravel)))))))))
|
||||
|
||||
(define
|
||||
apl-reduce
|
||||
(fn
|
||||
|
||||
@@ -39,6 +39,7 @@ cat > "$TMPFILE" << 'EPOCHS'
|
||||
(load "lib/apl/tests/idioms.sx")
|
||||
(load "lib/apl/tests/eval-ops.sx")
|
||||
(load "lib/apl/tests/pipeline.sx")
|
||||
(load "lib/apl/tests/programs-e2e.sx")
|
||||
(epoch 4)
|
||||
(eval "(list apl-test-pass apl-test-fail)")
|
||||
EPOCHS
|
||||
|
||||
@@ -178,3 +178,280 @@
|
||||
"apl-run \"(⍳5)[3] × 7\" → 21"
|
||||
(mkrv (apl-run "(⍳5)[3] × 7"))
|
||||
(list 21))
|
||||
|
||||
(apl-test "decimal: 3.7 → 3.7" (mkrv (apl-run "3.7")) (list 3.7))
|
||||
|
||||
(apl-test "decimal: ¯2.5 → -2.5" (mkrv (apl-run "¯2.5")) (list -2.5))
|
||||
|
||||
(apl-test "decimal: 1.5 + 2.5 → 4" (mkrv (apl-run "1.5 + 2.5")) (list 4))
|
||||
|
||||
(apl-test "decimal: ⌊3.7 → 3" (mkrv (apl-run "⌊ 3.7")) (list 3))
|
||||
|
||||
(apl-test "decimal: ⌈3.7 → 4" (mkrv (apl-run "⌈ 3.7")) (list 4))
|
||||
|
||||
(apl-test
|
||||
"⎕← scalar passthrough"
|
||||
(mkrv (apl-run "⎕← 42"))
|
||||
(list 42))
|
||||
|
||||
(apl-test
|
||||
"⎕← vector passthrough"
|
||||
(mkrv (apl-run "⎕← 1 2 3"))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"string: 'abc' → 3-char vector"
|
||||
(mkrv (apl-run "'abc'"))
|
||||
(list "a" "b" "c"))
|
||||
|
||||
(apl-test "string: 'a' is rank-0 scalar" (mksh (apl-run "'a'")) (list))
|
||||
|
||||
(apl-test "string: 'hello' shape (5)" (mksh (apl-run "'hello'")) (list 5))
|
||||
|
||||
(apl-test
|
||||
"named-fn: f ← {⍺+⍵} ⋄ 3 f 4 → 7"
|
||||
(mkrv (apl-run "f ← {⍺+⍵} ⋄ 3 f 4"))
|
||||
(list 7))
|
||||
|
||||
(apl-test
|
||||
"named-fn monadic: sq ← {⍵×⍵} ⋄ sq 7 → 49"
|
||||
(mkrv (apl-run "sq ← {⍵×⍵} ⋄ sq 7"))
|
||||
(list 49))
|
||||
|
||||
(apl-test
|
||||
"named-fn dyadic: hyp ← {((⍺×⍺)+⍵×⍵)} ⋄ 3 hyp 4 → 25"
|
||||
(mkrv (apl-run "hyp ← {((⍺×⍺)+⍵×⍵)} ⋄ 3 hyp 4"))
|
||||
(list 25))
|
||||
|
||||
(apl-test
|
||||
"named-fn: dbl ← {⍵+⍵} ⋄ dbl ⍳5"
|
||||
(mkrv (apl-run "dbl ← {⍵+⍵} ⋄ dbl ⍳5"))
|
||||
(list 2 4 6 8 10))
|
||||
|
||||
(apl-test
|
||||
"named-fn factorial via ∇ recursion"
|
||||
(mkrv (apl-run "fact ← {0=⍵:1 ⋄ ⍵×∇⍵-1} ⋄ fact 5"))
|
||||
(list 120))
|
||||
|
||||
(apl-test
|
||||
"named-fn used twice in expr: dbl ← {⍵+⍵} ⋄ (dbl 3) + dbl 4"
|
||||
(mkrv (apl-run "dbl ← {⍵+⍵} ⋄ (dbl 3) + dbl 4"))
|
||||
(list 14))
|
||||
|
||||
(apl-test
|
||||
"named-fn with vector arg: neg ← {-⍵} ⋄ neg 1 2 3"
|
||||
(mkrv (apl-run "neg ← {-⍵} ⋄ neg 1 2 3"))
|
||||
(list -1 -2 -3))
|
||||
|
||||
(apl-test
|
||||
"multi-axis: M[2;2] → center"
|
||||
(mkrv (apl-run "M ← (3 3) ⍴ ⍳9 ⋄ M[2;2]"))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"multi-axis: M[1;] → first row"
|
||||
(mkrv (apl-run "M ← (3 3) ⍴ ⍳9 ⋄ M[1;]"))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"multi-axis: M[;2] → second column"
|
||||
(mkrv (apl-run "M ← (3 3) ⍴ ⍳9 ⋄ M[;2]"))
|
||||
(list 2 5 8))
|
||||
|
||||
(apl-test
|
||||
"multi-axis: M[1 2;1 2] → 2x2 block"
|
||||
(mkrv (apl-run "M ← (2 3) ⍴ ⍳6 ⋄ M[1 2;1 2]"))
|
||||
(list 1 2 4 5))
|
||||
|
||||
(apl-test
|
||||
"multi-axis: M[1 2;1 2] shape (2 2)"
|
||||
(mksh (apl-run "M ← (2 3) ⍴ ⍳6 ⋄ M[1 2;1 2]"))
|
||||
(list 2 2))
|
||||
|
||||
(apl-test
|
||||
"multi-axis: M[;] full matrix"
|
||||
(mkrv (apl-run "M ← (2 2) ⍴ 10 20 30 40 ⋄ M[;]"))
|
||||
(list 10 20 30 40))
|
||||
|
||||
(apl-test
|
||||
"multi-axis: M[1;] shape collapsed"
|
||||
(mksh (apl-run "M ← (3 3) ⍴ ⍳9 ⋄ M[1;]"))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"multi-axis: select all rows of column 3"
|
||||
(mkrv (apl-run "M ← (4 3) ⍴ 1 2 3 4 5 6 7 8 9 10 11 12 ⋄ M[;3]"))
|
||||
(list 3 6 9 12))
|
||||
|
||||
(apl-test
|
||||
"train: mean = (+/÷≢) on 1..5"
|
||||
(mkrv (apl-run "(+/÷≢) 1 2 3 4 5"))
|
||||
(list 3))
|
||||
|
||||
(apl-test
|
||||
"train: mean of 2 4 6 8 10"
|
||||
(mkrv (apl-run "(+/÷≢) 2 4 6 8 10"))
|
||||
(list 6))
|
||||
|
||||
(apl-test
|
||||
"train 2-atop: (- ⌊) 5 → -5"
|
||||
(mkrv (apl-run "(- ⌊) 5"))
|
||||
(list -5))
|
||||
|
||||
(apl-test
|
||||
"train 3-fork dyadic: 2(+×-)5 → -21"
|
||||
(mkrv (apl-run "2 (+ × -) 5"))
|
||||
(list -21))
|
||||
|
||||
(apl-test
|
||||
"train: range = (⌈/-⌊/) on vector"
|
||||
(mkrv (apl-run "(⌈/-⌊/) 3 1 4 1 5 9 2 6"))
|
||||
(list 8))
|
||||
|
||||
(apl-test
|
||||
"train: mean of ⍳10 has shape ()"
|
||||
(mksh (apl-run "(+/÷≢) ⍳10"))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"compress: 1 0 1 0 1 / 10 20 30 40 50"
|
||||
(mkrv (apl-run "1 0 1 0 1 / 10 20 30 40 50"))
|
||||
(list 10 30 50))
|
||||
|
||||
(apl-test
|
||||
"compress: empty mask → empty"
|
||||
(mkrv (apl-run "0 0 0 / 1 2 3"))
|
||||
(list))
|
||||
|
||||
(apl-test
|
||||
"primes via classic idiom (multi-stmt)"
|
||||
(mkrv (apl-run "P ← ⍳ 30 ⋄ (2 = +⌿ 0 = P ∘.| P) / P"))
|
||||
(list 2 3 5 7 11 13 17 19 23 29))
|
||||
|
||||
(apl-test
|
||||
"primes via classic idiom (n=20)"
|
||||
(mkrv (apl-run "P ← ⍳ 20 ⋄ (2 = +⌿ 0 = P ∘.| P) / P"))
|
||||
(list 2 3 5 7 11 13 17 19))
|
||||
|
||||
(apl-test
|
||||
"compress: filter even values"
|
||||
(mkrv (apl-run "(0 = 2 | 1 2 3 4 5 6) / 1 2 3 4 5 6"))
|
||||
(list 2 4 6))
|
||||
|
||||
(apl-test "inline-assign: x ← 5" (mkrv (apl-run "x ← 5")) (list 5))
|
||||
|
||||
(apl-test
|
||||
"inline-assign: (2×x) + x←10 → 30"
|
||||
(mkrv (apl-run "(2 × x) + x ← 10"))
|
||||
(list 30))
|
||||
|
||||
(apl-test
|
||||
"inline-assign primes one-liner: (2=+⌿0=a∘.|a)/a←⍳30"
|
||||
(mkrv (apl-run "(2 = +⌿ 0 = a ∘.| a) / a ← ⍳ 30"))
|
||||
(list 2 3 5 7 11 13 17 19 23 29))
|
||||
|
||||
(apl-test
|
||||
"inline-assign: x is reusable — x + x ← 7 → 14"
|
||||
(mkrv (apl-run "x + x ← 7"))
|
||||
(list 14))
|
||||
|
||||
(apl-test
|
||||
"inline-assign in dfn: f ← {x + x ← ⍵} ⋄ f 8 → 16"
|
||||
(mkrv (apl-run "f ← {x + x ← ⍵} ⋄ f 8"))
|
||||
(list 16))
|
||||
|
||||
(begin (apl-rng-seed! 42) nil)
|
||||
|
||||
(apl-test
|
||||
"?10 with seed 42 → 8 (deterministic)"
|
||||
(mkrv (apl-run "?10"))
|
||||
(list 8))
|
||||
|
||||
(apl-test "?10 next call → 5" (mkrv (apl-run "?10")) (list 5))
|
||||
|
||||
(apl-test
|
||||
"?100 stays in range"
|
||||
(let ((v (first (mkrv (apl-run "?100"))))) (and (>= v 1) (<= v 100)))
|
||||
true)
|
||||
|
||||
(begin (apl-rng-seed! 42) nil)
|
||||
|
||||
(apl-test
|
||||
"?10 with re-seed 42 → 8 (reproducible)"
|
||||
(mkrv (apl-run "?10"))
|
||||
(list 8))
|
||||
|
||||
(apl-test
|
||||
"apl-run-file: load primes.apl returns dfn AST"
|
||||
(first (apl-run-file "lib/apl/tests/programs/primes.apl"))
|
||||
:dfn)
|
||||
|
||||
(apl-test
|
||||
"apl-run-file: life.apl parses without error"
|
||||
(first (apl-run-file "lib/apl/tests/programs/life.apl"))
|
||||
:dfn)
|
||||
|
||||
(apl-test
|
||||
"apl-run-file: quicksort.apl parses without error"
|
||||
(first (apl-run-file "lib/apl/tests/programs/quicksort.apl"))
|
||||
:dfn)
|
||||
|
||||
(apl-test
|
||||
"apl-run-file: source-then-call returns primes count"
|
||||
(mksh
|
||||
(apl-run
|
||||
(str (file-read "lib/apl/tests/programs/primes.apl") " ⋄ primes 30")))
|
||||
(list 10))
|
||||
|
||||
(apl-test
|
||||
"primes one-liner with ⍵-rebind: primes 30"
|
||||
(mkrv
|
||||
(apl-run "primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵} ⋄ primes 30"))
|
||||
(list 2 3 5 7 11 13 17 19 23 29))
|
||||
|
||||
(apl-test
|
||||
"primes one-liner: primes 50"
|
||||
(mkrv
|
||||
(apl-run "primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵} ⋄ primes 50"))
|
||||
(list 2 3 5 7 11 13 17 19 23 29 31 37 41 43 47))
|
||||
|
||||
(apl-test
|
||||
"primes.apl loaded + called via apl-run-file"
|
||||
(mkrv
|
||||
(apl-run
|
||||
(str (file-read "lib/apl/tests/programs/primes.apl") " ⋄ primes 20")))
|
||||
(list 2 3 5 7 11 13 17 19))
|
||||
|
||||
(apl-test
|
||||
"primes.apl loaded — count of primes ≤ 100"
|
||||
(first
|
||||
(mksh
|
||||
(apl-run
|
||||
(str
|
||||
(file-read "lib/apl/tests/programs/primes.apl")
|
||||
" ⋄ primes 100"))))
|
||||
25)
|
||||
|
||||
(apl-test
|
||||
"⍉ monadic transpose 2x3 → 3x2"
|
||||
(mkrv (apl-run "⍉ (2 3) ⍴ ⍳6"))
|
||||
(list 1 4 2 5 3 6))
|
||||
|
||||
(apl-test
|
||||
"⍉ transpose shape (3 2)"
|
||||
(mksh (apl-run "⍉ (2 3) ⍴ ⍳6"))
|
||||
(list 3 2))
|
||||
|
||||
(apl-test "⊢ monadic identity" (mkrv (apl-run "⊢ 1 2 3")) (list 1 2 3))
|
||||
|
||||
(apl-test
|
||||
"5 ⊣ 1 2 3 → 5 (left)"
|
||||
(mkrv (apl-run "5 ⊣ 1 2 3"))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"5 ⊢ 1 2 3 → 1 2 3 (right)"
|
||||
(mkrv (apl-run "5 ⊢ 1 2 3"))
|
||||
(list 1 2 3))
|
||||
|
||||
(apl-test "⍕ 42 → \"42\" (alias for ⎕FMT)" (apl-run "⍕ 42") "42")
|
||||
|
||||
96
lib/apl/tests/programs-e2e.sx
Normal file
96
lib/apl/tests/programs-e2e.sx
Normal file
@@ -0,0 +1,96 @@
|
||||
; End-to-end tests of the classic-program archetypes — running APL
|
||||
; source through the full pipeline (tokenize → parse → eval-ast → runtime).
|
||||
;
|
||||
; These mirror the algorithms documented in lib/apl/tests/programs/*.apl
|
||||
; but use forms our pipeline supports today (named functions instead of
|
||||
; the inline ⍵← rebinding idiom; multi-stmt over single one-liners).
|
||||
|
||||
(define mkrv (fn (arr) (get arr :ravel)))
|
||||
(define mksh (fn (arr) (get arr :shape)))
|
||||
|
||||
; ---------- factorial via ∇ recursion (cf. n-queens style) ----------
|
||||
|
||||
(apl-test
|
||||
"e2e: factorial 5! = 120"
|
||||
(mkrv (apl-run "fact ← {0=⍵:1 ⋄ ⍵×∇⍵-1} ⋄ fact 5"))
|
||||
(list 120))
|
||||
|
||||
(apl-test
|
||||
"e2e: factorial 7! = 5040"
|
||||
(mkrv (apl-run "fact ← {0=⍵:1 ⋄ ⍵×∇⍵-1} ⋄ fact 7"))
|
||||
(list 5040))
|
||||
|
||||
(apl-test
|
||||
"e2e: factorial via ×/⍳N (no recursion)"
|
||||
(mkrv (apl-run "fact ← {×/⍳⍵} ⋄ fact 6"))
|
||||
(list 720))
|
||||
|
||||
; ---------- sum / triangular numbers (sum-1..N) ----------
|
||||
|
||||
(apl-test
|
||||
"e2e: triangular(10) = 55"
|
||||
(mkrv (apl-run "tri ← {+/⍳⍵} ⋄ tri 10"))
|
||||
(list 55))
|
||||
|
||||
(apl-test
|
||||
"e2e: triangular(100) = 5050"
|
||||
(mkrv (apl-run "tri ← {+/⍳⍵} ⋄ tri 100"))
|
||||
(list 5050))
|
||||
|
||||
; ---------- sum of squares ----------
|
||||
|
||||
(apl-test
|
||||
"e2e: sum-of-squares 1..5 = 55"
|
||||
(mkrv (apl-run "ss ← {+/⍵×⍵} ⋄ ss ⍳5"))
|
||||
(list 55))
|
||||
|
||||
(apl-test
|
||||
"e2e: sum-of-squares 1..10 = 385"
|
||||
(mkrv (apl-run "ss ← {+/⍵×⍵} ⋄ ss ⍳10"))
|
||||
(list 385))
|
||||
|
||||
; ---------- divisor-counting (prime-sieve building blocks) ----------
|
||||
|
||||
(apl-test
|
||||
"e2e: divisor counts 1..5 via outer mod"
|
||||
(mkrv (apl-run "P ← ⍳ 5 ⋄ +⌿ 0 = P ∘.| P"))
|
||||
(list 1 2 2 3 2))
|
||||
|
||||
(apl-test
|
||||
"e2e: divisor counts 1..10"
|
||||
(mkrv (apl-run "P ← ⍳ 10 ⋄ +⌿ 0 = P ∘.| P"))
|
||||
(list 1 2 2 3 2 4 2 4 3 4))
|
||||
|
||||
(apl-test
|
||||
"e2e: prime-mask 1..10 (count==2)"
|
||||
(mkrv (apl-run "P ← ⍳ 10 ⋄ 2 = +⌿ 0 = P ∘.| P"))
|
||||
(list 0 1 1 0 1 0 1 0 0 0))
|
||||
|
||||
; ---------- monadic primitives chained ----------
|
||||
|
||||
(apl-test
|
||||
"e2e: sum of |abs| = 15"
|
||||
(mkrv (apl-run "+/|¯1 ¯2 ¯3 ¯4 ¯5"))
|
||||
(list 15))
|
||||
|
||||
(apl-test
|
||||
"e2e: max of squares 1..6"
|
||||
(mkrv (apl-run "⌈/(⍳6)×⍳6"))
|
||||
(list 36))
|
||||
|
||||
; ---------- nested named functions ----------
|
||||
|
||||
(apl-test
|
||||
"e2e: compose dbl and sq via two named fns"
|
||||
(mkrv (apl-run "dbl ← {⍵+⍵} ⋄ sq ← {⍵×⍵} ⋄ sq dbl 3"))
|
||||
(list 36))
|
||||
|
||||
(apl-test
|
||||
"e2e: max-of-two as named dyadic fn"
|
||||
(mkrv (apl-run "mx ← {⍺⌈⍵} ⋄ 5 mx 3"))
|
||||
(list 5))
|
||||
|
||||
(apl-test
|
||||
"e2e: sqrt-via-newton 1 step from 1 → 2.5"
|
||||
(mkrv (apl-run "step ← {(⍵+⍺÷⍵)÷2} ⋄ 4 step 1"))
|
||||
(list 2.5))
|
||||
@@ -2,7 +2,7 @@
|
||||
(list "+" "-" "×" "÷" "*" "⍟" "⌈" "⌊" "|" "!" "?" "○" "~" "<" "≤" "=" "≥" ">" "≠"
|
||||
"≢" "≡" "∊" "∧" "∨" "⍱" "⍲" "," "⍪" "⍴" "⌽" "⊖" "⍉" "↑" "↓" "⊂" "⊃" "⊆"
|
||||
"∪" "∩" "⍳" "⍸" "⌷" "⍋" "⍒" "⊥" "⊤" "⊣" "⊢" "⍎" "⍕"
|
||||
"⍺" "⍵" "∇" "/" "\\" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@" "¯"))
|
||||
"⍺" "⍵" "∇" "/" "⌿" "\\" "⍀" "¨" "⍨" "∘" "." "⍣" "⍤" "⍥" "@" "¯"))
|
||||
|
||||
(define apl-glyph?
|
||||
(fn (ch)
|
||||
@@ -138,12 +138,22 @@
|
||||
(begin
|
||||
(consume! "¯")
|
||||
(let ((digits (read-digits! "")))
|
||||
(tok-push! :num (- 0 (parse-int digits 0))))
|
||||
(if (and (< pos src-len) (= (cur-byte) ".")
|
||||
(< (+ pos 1) src-len) (apl-digit? (nth source (+ pos 1))))
|
||||
(begin (advance!)
|
||||
(let ((frac (read-digits! "")))
|
||||
(tok-push! :num (- 0 (string->number (str digits "." frac))))))
|
||||
(tok-push! :num (- 0 (parse-int digits 0)))))
|
||||
(scan!)))
|
||||
((apl-digit? ch)
|
||||
(begin
|
||||
(let ((digits (read-digits! "")))
|
||||
(tok-push! :num (parse-int digits 0)))
|
||||
(if (and (< pos src-len) (= (cur-byte) ".")
|
||||
(< (+ pos 1) src-len) (apl-digit? (nth source (+ pos 1))))
|
||||
(begin (advance!)
|
||||
(let ((frac (read-digits! "")))
|
||||
(tok-push! :num (string->number (str digits "." frac)))))
|
||||
(tok-push! :num (parse-int digits 0))))
|
||||
(scan!)))
|
||||
((= ch "'")
|
||||
(begin
|
||||
@@ -155,7 +165,9 @@
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(if (cur-sw? "⎕") (consume! "⎕") (advance!))
|
||||
(read-ident-cont!)
|
||||
(if (and (< pos src-len) (cur-sw? "←"))
|
||||
(consume! "←")
|
||||
(read-ident-cont!))
|
||||
(tok-push! :name (slice source start pos))
|
||||
(scan!))))
|
||||
(true
|
||||
|
||||
@@ -39,7 +39,13 @@
|
||||
((= g "⊖") apl-reverse-first)
|
||||
((= g "⍋") apl-grade-up)
|
||||
((= g "⍒") apl-grade-down)
|
||||
((= g "?") apl-roll)
|
||||
((= g "⍉") apl-transpose)
|
||||
((= g "⊢") (fn (a) a))
|
||||
((= g "⊣") (fn (a) a))
|
||||
((= g "⍕") apl-quad-fmt)
|
||||
((= g "⎕FMT") apl-quad-fmt)
|
||||
((= g "⎕←") apl-quad-print)
|
||||
(else (error "no monadic fn for glyph")))))
|
||||
|
||||
(define
|
||||
@@ -79,6 +85,11 @@
|
||||
((= g "∊") apl-member)
|
||||
((= g "⍳") apl-index-of)
|
||||
((= g "~") apl-without)
|
||||
((= g "/") apl-compress)
|
||||
((= g "⌿") apl-compress-first)
|
||||
((= g "⍉") apl-transpose-dyadic)
|
||||
((= g "⊢") (fn (a b) b))
|
||||
((= g "⊣") (fn (a b) a))
|
||||
(else (error "no dyadic fn for glyph")))))
|
||||
|
||||
(define
|
||||
@@ -97,6 +108,15 @@
|
||||
((tag (first node)))
|
||||
(cond
|
||||
((= tag :num) (apl-scalar (nth node 1)))
|
||||
((= tag :str)
|
||||
(let
|
||||
((s (nth node 1)))
|
||||
(if
|
||||
(= (len s) 1)
|
||||
(apl-scalar s)
|
||||
(make-array
|
||||
(list (len s))
|
||||
(map (fn (i) (slice s i (+ i 1))) (range 0 (len s)))))))
|
||||
((= tag :vec)
|
||||
(let
|
||||
((items (rest node)))
|
||||
@@ -109,8 +129,14 @@
|
||||
(let
|
||||
((nm (nth node 1)))
|
||||
(cond
|
||||
((= nm "⍺") (get env "alpha"))
|
||||
((= nm "⍵") (get env "omega"))
|
||||
((= nm "⍺")
|
||||
(let
|
||||
((v (get env "⍺")))
|
||||
(if (= v nil) (get env "alpha") v)))
|
||||
((= nm "⍵")
|
||||
(let
|
||||
((v (get env "⍵")))
|
||||
(if (= v nil) (get env "omega") v)))
|
||||
((= nm "⎕IO") (apl-quad-io))
|
||||
((= nm "⎕ML") (apl-quad-ml))
|
||||
((= nm "⎕FR") (apl-quad-fr))
|
||||
@@ -122,7 +148,11 @@
|
||||
(if
|
||||
(and (= (first fn-node) :fn-glyph) (= (nth fn-node 1) "∇"))
|
||||
(apl-call-dfn-m (get env "nabla") (apl-eval-ast arg env))
|
||||
((apl-resolve-monadic fn-node env) (apl-eval-ast arg env)))))
|
||||
(let
|
||||
((arg-val (apl-eval-ast arg env)))
|
||||
(let
|
||||
((new-env (if (and (list? arg) (> (len arg) 0) (= (first arg) :assign-expr)) (assoc env (nth arg 1) arg-val) env)))
|
||||
((apl-resolve-monadic fn-node new-env) arg-val))))))
|
||||
((= tag :dyad)
|
||||
(let
|
||||
((fn-node (nth node 1))
|
||||
@@ -134,11 +164,27 @@
|
||||
(get env "nabla")
|
||||
(apl-eval-ast lhs env)
|
||||
(apl-eval-ast rhs env))
|
||||
((apl-resolve-dyadic fn-node env)
|
||||
(apl-eval-ast lhs env)
|
||||
(apl-eval-ast rhs env)))))
|
||||
(let
|
||||
((rhs-val (apl-eval-ast rhs env)))
|
||||
(let
|
||||
((new-env (if (and (list? rhs) (> (len rhs) 0) (= (first rhs) :assign-expr)) (assoc env (nth rhs 1) rhs-val) env)))
|
||||
((apl-resolve-dyadic fn-node new-env)
|
||||
(apl-eval-ast lhs new-env)
|
||||
rhs-val))))))
|
||||
((= tag :program) (apl-eval-stmts (rest node) env))
|
||||
((= tag :dfn) node)
|
||||
((= tag :bracket)
|
||||
(let
|
||||
((arr-expr (nth node 1)) (axis-exprs (rest (rest node))))
|
||||
(let
|
||||
((arr (apl-eval-ast arr-expr env))
|
||||
(axes
|
||||
(map
|
||||
(fn (a) (if (= a :all) nil (apl-eval-ast a env)))
|
||||
axis-exprs)))
|
||||
(apl-bracket-multi axes arr))))
|
||||
((= tag :assign-expr) (apl-eval-ast (nth node 2) env))
|
||||
((= tag :assign) (apl-eval-ast (nth node 2) env))
|
||||
(else (error (list "apl-eval-ast: unknown node tag" tag node)))))))
|
||||
|
||||
(define
|
||||
@@ -419,6 +465,36 @@
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (arr) (apl-commute f arr))))
|
||||
(else (error "apl-resolve-monadic: unsupported op")))))
|
||||
((= tag :fn-name)
|
||||
(let
|
||||
((nm (nth fn-node 1)))
|
||||
(let
|
||||
((bound (get env nm)))
|
||||
(if
|
||||
(and
|
||||
(list? bound)
|
||||
(> (len bound) 0)
|
||||
(= (first bound) :dfn))
|
||||
(fn (arg) (apl-call-dfn-m bound arg))
|
||||
(error "apl-resolve-monadic: name not bound to dfn")))))
|
||||
((= tag :train)
|
||||
(let
|
||||
((fns (rest fn-node)))
|
||||
(let
|
||||
((n (len fns)))
|
||||
(cond
|
||||
((= n 2)
|
||||
(let
|
||||
((g (apl-resolve-monadic (nth fns 0) env))
|
||||
(h (apl-resolve-monadic (nth fns 1) env)))
|
||||
(fn (arg) (g (h arg)))))
|
||||
((= n 3)
|
||||
(let
|
||||
((f (apl-resolve-monadic (nth fns 0) env))
|
||||
(g (apl-resolve-dyadic (nth fns 1) env))
|
||||
(h (apl-resolve-monadic (nth fns 2) env)))
|
||||
(fn (arg) (g (f arg) (h arg)))))
|
||||
(else (error "monadic train arity not 2 or 3"))))))
|
||||
(else (error "apl-resolve-monadic: unknown fn-node tag"))))))
|
||||
|
||||
(define
|
||||
@@ -442,6 +518,18 @@
|
||||
((f (apl-resolve-dyadic inner env)))
|
||||
(fn (a b) (apl-commute-dyadic f a b))))
|
||||
(else (error "apl-resolve-dyadic: unsupported op")))))
|
||||
((= tag :fn-name)
|
||||
(let
|
||||
((nm (nth fn-node 1)))
|
||||
(let
|
||||
((bound (get env nm)))
|
||||
(if
|
||||
(and
|
||||
(list? bound)
|
||||
(> (len bound) 0)
|
||||
(= (first bound) :dfn))
|
||||
(fn (a b) (apl-call-dfn bound a b))
|
||||
(error "apl-resolve-dyadic: name not bound to dfn")))))
|
||||
((= tag :outer)
|
||||
(let
|
||||
((inner (nth fn-node 2)))
|
||||
@@ -455,6 +543,26 @@
|
||||
((f (apl-resolve-dyadic f-node env))
|
||||
(g (apl-resolve-dyadic g-node env)))
|
||||
(fn (a b) (apl-inner f g a b)))))
|
||||
((= tag :train)
|
||||
(let
|
||||
((fns (rest fn-node)))
|
||||
(let
|
||||
((n (len fns)))
|
||||
(cond
|
||||
((= n 2)
|
||||
(let
|
||||
((g (apl-resolve-monadic (nth fns 0) env))
|
||||
(h (apl-resolve-dyadic (nth fns 1) env)))
|
||||
(fn (a b) (g (h a b)))))
|
||||
((= n 3)
|
||||
(let
|
||||
((f (apl-resolve-dyadic (nth fns 0) env))
|
||||
(g (apl-resolve-dyadic (nth fns 1) env))
|
||||
(h (apl-resolve-dyadic (nth fns 2) env)))
|
||||
(fn (a b) (g (f a b) (h a b)))))
|
||||
(else (error "dyadic train arity not 2 or 3"))))))
|
||||
(else (error "apl-resolve-dyadic: unknown fn-node tag"))))))
|
||||
|
||||
(define apl-run (fn (src) (apl-eval-ast (parse-apl src) {})))
|
||||
|
||||
(define apl-run-file (fn (path) (apl-run (file-read path))))
|
||||
|
||||
@@ -76,7 +76,7 @@ cat > "$TMPFILE" << 'EPOCHS'
|
||||
(eval "(list er-fib-test-pass er-fib-test-count)")
|
||||
EPOCHS
|
||||
|
||||
timeout 120 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
||||
timeout 600 "$SX_SERVER" < "$TMPFILE" > "$OUTFILE" 2>&1
|
||||
|
||||
# Parse "(N M)" from the line after each "(ok-len <epoch> ...)" marker.
|
||||
parse_pair() {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"language": "erlang",
|
||||
"total_pass": 0,
|
||||
"total": 0,
|
||||
"total_pass": 530,
|
||||
"total": 530,
|
||||
"suites": [
|
||||
{"name":"tokenize","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"parse","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"eval","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"runtime","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"ring","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"ping-pong","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"bank","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"echo","pass":0,"total":0,"status":"ok"},
|
||||
{"name":"fib","pass":0,"total":0,"status":"ok"}
|
||||
{"name":"tokenize","pass":62,"total":62,"status":"ok"},
|
||||
{"name":"parse","pass":52,"total":52,"status":"ok"},
|
||||
{"name":"eval","pass":346,"total":346,"status":"ok"},
|
||||
{"name":"runtime","pass":39,"total":39,"status":"ok"},
|
||||
{"name":"ring","pass":4,"total":4,"status":"ok"},
|
||||
{"name":"ping-pong","pass":4,"total":4,"status":"ok"},
|
||||
{"name":"bank","pass":8,"total":8,"status":"ok"},
|
||||
{"name":"echo","pass":7,"total":7,"status":"ok"},
|
||||
{"name":"fib","pass":8,"total":8,"status":"ok"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# Erlang-on-SX Scoreboard
|
||||
|
||||
**Total: 0 / 0 tests passing**
|
||||
**Total: 530 / 530 tests passing**
|
||||
|
||||
| | Suite | Pass | Total |
|
||||
|---|---|---|---|
|
||||
| ✅ | tokenize | 0 | 0 |
|
||||
| ✅ | parse | 0 | 0 |
|
||||
| ✅ | eval | 0 | 0 |
|
||||
| ✅ | runtime | 0 | 0 |
|
||||
| ✅ | ring | 0 | 0 |
|
||||
| ✅ | ping-pong | 0 | 0 |
|
||||
| ✅ | bank | 0 | 0 |
|
||||
| ✅ | echo | 0 | 0 |
|
||||
| ✅ | fib | 0 | 0 |
|
||||
| ✅ | tokenize | 62 | 62 |
|
||||
| ✅ | parse | 52 | 52 |
|
||||
| ✅ | eval | 346 | 346 |
|
||||
| ✅ | runtime | 39 | 39 |
|
||||
| ✅ | ring | 4 | 4 |
|
||||
| ✅ | ping-pong | 4 | 4 |
|
||||
| ✅ | bank | 8 | 8 |
|
||||
| ✅ | echo | 7 | 7 |
|
||||
| ✅ | fib | 8 | 8 |
|
||||
|
||||
|
||||
Generated by `lib/erlang/conformance.sh`.
|
||||
|
||||
180
lib/guest/hm.sx
Normal file
180
lib/guest/hm.sx
Normal file
@@ -0,0 +1,180 @@
|
||||
;; lib/guest/hm.sx — Hindley-Milner type-inference foundations.
|
||||
;;
|
||||
;; Builds on lib/guest/match.sx (terms + unify) and ast.sx (canonical
|
||||
;; AST shapes). This file ships the ALGEBRA — types, schemes, free
|
||||
;; type-vars, generalize / instantiate, substitution composition — so a
|
||||
;; full Algorithm W (or J) can be assembled on top either inside this
|
||||
;; file or in a host-specific consumer (haskell/infer.sx,
|
||||
;; lib/ocaml/types.sx, …).
|
||||
;;
|
||||
;; Per the brief the second consumer for this step is OCaml-on-SX
|
||||
;; Phase 5 (paired sequencing). Until that lands, the algebra is the
|
||||
;; deliverable; the host-flavoured assembly (lambda / app / let
|
||||
;; inference rules with substitution threading) lives in the host.
|
||||
;;
|
||||
;; Types
|
||||
;; -----
|
||||
;; A type is a canonical match.sx term — type variables use mk-var,
|
||||
;; type constructors use mk-ctor:
|
||||
;; (hm-tv NAME) type variable
|
||||
;; (hm-arrow A B) A -> B
|
||||
;; (hm-con NAME ARGS) named n-ary constructor
|
||||
;; (hm-int) / (hm-bool) / (hm-string) primitive constructors
|
||||
;;
|
||||
;; Schemes
|
||||
;; -------
|
||||
;; (hm-scheme VARS TYPE) ∀ VARS . TYPE
|
||||
;; (hm-monotype TYPE) empty quantifier
|
||||
;; (hm-scheme? S) (hm-scheme-vars S) (hm-scheme-type S)
|
||||
;;
|
||||
;; Free type variables
|
||||
;; -------------------
|
||||
;; (hm-ftv TYPE) names occurring in TYPE
|
||||
;; (hm-ftv-scheme S) free names (minus quantifiers)
|
||||
;; (hm-ftv-env ENV) free across an env (name -> scheme)
|
||||
;;
|
||||
;; Substitution
|
||||
;; ------------
|
||||
;; (hm-apply SUBST TYPE) substitute through a type
|
||||
;; (hm-apply-scheme SUBST S) leaves bound vars alone
|
||||
;; (hm-apply-env SUBST ENV)
|
||||
;; (hm-compose S2 S1) apply S1 then S2
|
||||
;;
|
||||
;; Generalize / Instantiate
|
||||
;; ------------------------
|
||||
;; (hm-generalize TYPE ENV) → scheme over ftv(t) - ftv(env)
|
||||
;; (hm-instantiate SCHEME COUNTER) → fresh-var instance
|
||||
;; (hm-fresh-tv COUNTER) → (:var "tN"), bumps COUNTER
|
||||
;;
|
||||
;; Inference (literal only — the rest of Algorithm W lives in the host)
|
||||
;; --------------------------------------------------------------------
|
||||
;; (hm-infer-literal EXPR) → {:subst {} :type T}
|
||||
;;
|
||||
;; A complete Algorithm W consumes this kit by assembling lambda / app
|
||||
;; / let rules in the host language file.
|
||||
|
||||
(define hm-tv (fn (name) (list :var name)))
|
||||
(define hm-con (fn (name args) (list :ctor name args)))
|
||||
(define hm-arrow (fn (a b) (hm-con "->" (list a b))))
|
||||
(define hm-int (fn () (hm-con "Int" (list))))
|
||||
(define hm-bool (fn () (hm-con "Bool" (list))))
|
||||
(define hm-string (fn () (hm-con "String" (list))))
|
||||
|
||||
(define hm-scheme (fn (vars t) (list :scheme vars t)))
|
||||
(define hm-monotype (fn (t) (hm-scheme (list) t)))
|
||||
(define hm-scheme? (fn (s) (and (list? s) (not (empty? s)) (= (first s) :scheme))))
|
||||
(define hm-scheme-vars (fn (s) (nth s 1)))
|
||||
(define hm-scheme-type (fn (s) (nth s 2)))
|
||||
|
||||
(define
|
||||
hm-fresh-tv
|
||||
(fn (counter)
|
||||
(let ((n (first counter)))
|
||||
(begin
|
||||
(set-nth! counter 0 (+ n 1))
|
||||
(hm-tv (str "t" (+ n 1)))))))
|
||||
|
||||
(define
|
||||
hm-ftv-acc
|
||||
(fn (t acc)
|
||||
(cond
|
||||
((is-var? t)
|
||||
(if (some (fn (n) (= n (var-name t))) acc) acc (cons (var-name t) acc)))
|
||||
((is-ctor? t)
|
||||
(let ((a acc))
|
||||
(begin
|
||||
(for-each (fn (x) (set! a (hm-ftv-acc x a))) (ctor-args t))
|
||||
a)))
|
||||
(:else acc))))
|
||||
|
||||
(define hm-ftv (fn (t) (hm-ftv-acc t (list))))
|
||||
|
||||
(define
|
||||
hm-ftv-scheme
|
||||
(fn (s)
|
||||
(let ((qs (hm-scheme-vars s))
|
||||
(all (hm-ftv (hm-scheme-type s))))
|
||||
(filter (fn (n) (not (some (fn (q) (= q n)) qs))) all))))
|
||||
|
||||
(define
|
||||
hm-ftv-env
|
||||
(fn (env)
|
||||
(let ((acc (list)))
|
||||
(begin
|
||||
(for-each
|
||||
(fn (k)
|
||||
(for-each
|
||||
(fn (n)
|
||||
(when (not (some (fn (m) (= m n)) acc))
|
||||
(set! acc (cons n acc))))
|
||||
(hm-ftv-scheme (get env k))))
|
||||
(keys env))
|
||||
acc))))
|
||||
|
||||
(define hm-apply (fn (subst t) (walk* t subst)))
|
||||
|
||||
(define
|
||||
hm-apply-scheme
|
||||
(fn (subst s)
|
||||
(let ((qs (hm-scheme-vars s))
|
||||
(d {}))
|
||||
(begin
|
||||
(for-each
|
||||
(fn (k)
|
||||
(when (not (some (fn (q) (= q k)) qs))
|
||||
(dict-set! d k (get subst k))))
|
||||
(keys subst))
|
||||
(hm-scheme qs (walk* (hm-scheme-type s) d))))))
|
||||
|
||||
(define
|
||||
hm-apply-env
|
||||
(fn (subst env)
|
||||
(let ((d {}))
|
||||
(begin
|
||||
(for-each
|
||||
(fn (k) (dict-set! d k (hm-apply-scheme subst (get env k))))
|
||||
(keys env))
|
||||
d))))
|
||||
|
||||
(define
|
||||
hm-compose
|
||||
(fn (s2 s1)
|
||||
(let ((d {}))
|
||||
(begin
|
||||
(for-each (fn (k) (dict-set! d k (walk* (get s1 k) s2))) (keys s1))
|
||||
(for-each
|
||||
(fn (k) (when (not (has-key? d k)) (dict-set! d k (get s2 k))))
|
||||
(keys s2))
|
||||
d))))
|
||||
|
||||
(define
|
||||
hm-generalize
|
||||
(fn (t env)
|
||||
(let ((tvars (hm-ftv t))
|
||||
(evars (hm-ftv-env env)))
|
||||
(let ((qs (filter (fn (n) (not (some (fn (m) (= m n)) evars))) tvars)))
|
||||
(hm-scheme qs t)))))
|
||||
|
||||
(define
|
||||
hm-instantiate
|
||||
(fn (s counter)
|
||||
(let ((qs (hm-scheme-vars s))
|
||||
(subst {}))
|
||||
(begin
|
||||
(for-each
|
||||
(fn (q) (set! subst (assoc subst q (hm-fresh-tv counter))))
|
||||
qs)
|
||||
(walk* (hm-scheme-type s) subst)))))
|
||||
|
||||
;; Literal inference — the only AST kind whose typing rule is closed
|
||||
;; in the kit. Lambda / app / let live in host code so the host's own
|
||||
;; AST conventions stay untouched.
|
||||
(define
|
||||
hm-infer-literal
|
||||
(fn (expr)
|
||||
(let ((v (ast-literal-value expr)))
|
||||
(cond
|
||||
((number? v) {:subst {} :type (hm-int)})
|
||||
((string? v) {:subst {} :type (hm-string)})
|
||||
((boolean? v) {:subst {} :type (hm-bool)})
|
||||
(:else (error (str "hm-infer-literal: unknown kind: " v)))))))
|
||||
145
lib/guest/layout.sx
Normal file
145
lib/guest/layout.sx
Normal file
@@ -0,0 +1,145 @@
|
||||
;; lib/guest/layout.sx — configurable off-side / layout-sensitive lexer.
|
||||
;;
|
||||
;; Inserts virtual open / close / separator tokens based on indentation.
|
||||
;; Configurable enough to encode either the Haskell 98 layout rule (let /
|
||||
;; where / do / of opens a virtual brace at the next token's column) or
|
||||
;; a Python-ish indent / dedent rule (a colon at the end of a line opens
|
||||
;; a block at the next non-blank line's column).
|
||||
;;
|
||||
;; Token shape (input + output)
|
||||
;; ----------------------------
|
||||
;; Each token is a dict {:type :value :line :col …}. The kit reads
|
||||
;; only :type / :value / :line / :col and passes everything else
|
||||
;; through. The input stream MUST be free of newline filler tokens
|
||||
;; (preprocess them away with your tokenizer) — line breaks are detected
|
||||
;; by comparing :line of consecutive tokens.
|
||||
;;
|
||||
;; Config
|
||||
;; ------
|
||||
;; :open-keywords list of strings; a token whose :value matches
|
||||
;; opens a new layout block at the next token's
|
||||
;; column (Haskell: let/where/do/of).
|
||||
;; :open-trailing-fn (fn (tok) -> bool) — alternative trigger that
|
||||
;; fires AFTER the token is emitted. Use for
|
||||
;; Python-style trailing `:`.
|
||||
;; :open-token / :close-token / :sep-token
|
||||
;; templates {:type :value} merged with :line and
|
||||
;; :col when virtual tokens are emitted.
|
||||
;; :explicit-open? (fn (tok) -> bool) — if the next token after a
|
||||
;; trigger satisfies this, suppress virtual layout
|
||||
;; for that block (Haskell: `{`).
|
||||
;; :module-prelude? if true, wrap whole input in an implicit block
|
||||
;; at the first token's column (Haskell yes,
|
||||
;; Python no).
|
||||
;;
|
||||
;; Public entry
|
||||
;; ------------
|
||||
;; (layout-pass cfg tokens) -> tokens with virtual layout inserted.
|
||||
|
||||
(define
|
||||
layout-mk-virtual
|
||||
(fn (template line col)
|
||||
(assoc (assoc template :line line) :col col)))
|
||||
|
||||
(define
|
||||
layout-is-open-kw?
|
||||
(fn (tok open-kws)
|
||||
(and (= (get tok :type) "reserved")
|
||||
(some (fn (k) (= k (get tok :value))) open-kws))))
|
||||
|
||||
(define
|
||||
layout-pass
|
||||
(fn (cfg tokens)
|
||||
(let ((open-kws (get cfg :open-keywords))
|
||||
(trailing-fn (get cfg :open-trailing-fn))
|
||||
(open-tmpl (get cfg :open-token))
|
||||
(close-tmpl (get cfg :close-token))
|
||||
(sep-tmpl (get cfg :sep-token))
|
||||
(mod-prelude? (get cfg :module-prelude?))
|
||||
(expl?-fn (get cfg :explicit-open?))
|
||||
(out (list))
|
||||
(stack (list))
|
||||
(n (len tokens))
|
||||
(i 0)
|
||||
(prev-line -1)
|
||||
(pending-open false)
|
||||
(just-opened false))
|
||||
(define
|
||||
emit-closes-while-greater
|
||||
(fn (col line)
|
||||
(when (and (not (empty? stack)) (> (first stack) col))
|
||||
(do
|
||||
(append! out (layout-mk-virtual close-tmpl line col))
|
||||
(set! stack (rest stack))
|
||||
(emit-closes-while-greater col line)))))
|
||||
(define
|
||||
emit-pending-open
|
||||
(fn (line col)
|
||||
(do
|
||||
(append! out (layout-mk-virtual open-tmpl line col))
|
||||
(set! stack (cons col stack))
|
||||
(set! pending-open false)
|
||||
(set! just-opened true))))
|
||||
(define
|
||||
layout-step
|
||||
(fn ()
|
||||
(when (< i n)
|
||||
(let ((tok (nth tokens i)))
|
||||
(let ((line (get tok :line)) (col (get tok :col)))
|
||||
(cond
|
||||
(pending-open
|
||||
(cond
|
||||
((and (not (= expl?-fn nil)) (expl?-fn tok))
|
||||
(do
|
||||
(set! pending-open false)
|
||||
(append! out tok)
|
||||
(set! prev-line line)
|
||||
(set! i (+ i 1))
|
||||
(layout-step)))
|
||||
(:else
|
||||
(do
|
||||
(emit-pending-open line col)
|
||||
(layout-step)))))
|
||||
(:else
|
||||
(let ((on-fresh-line? (and (> prev-line 0) (> line prev-line))))
|
||||
(do
|
||||
(when on-fresh-line?
|
||||
(let ((stack-before stack))
|
||||
(begin
|
||||
(emit-closes-while-greater col line)
|
||||
(when (and (not (empty? stack))
|
||||
(= (first stack) col)
|
||||
(not just-opened)
|
||||
;; suppress separator if a dedent fired
|
||||
;; — the dedent is itself the separator
|
||||
(= (len stack) (len stack-before)))
|
||||
(append! out (layout-mk-virtual sep-tmpl line col))))))
|
||||
(set! just-opened false)
|
||||
(append! out tok)
|
||||
(set! prev-line line)
|
||||
(set! i (+ i 1))
|
||||
(cond
|
||||
((layout-is-open-kw? tok open-kws)
|
||||
(set! pending-open true))
|
||||
((and (not (= trailing-fn nil)) (trailing-fn tok))
|
||||
(set! pending-open true)))
|
||||
(layout-step))))))))))
|
||||
(begin
|
||||
;; Module prelude: implicit layout block at the first token's column.
|
||||
(when (and mod-prelude? (> n 0))
|
||||
(let ((tok (nth tokens 0)))
|
||||
(do
|
||||
(append! out (layout-mk-virtual open-tmpl (get tok :line) (get tok :col)))
|
||||
(set! stack (cons (get tok :col) stack))
|
||||
(set! just-opened true))))
|
||||
(layout-step)
|
||||
;; EOF: close every remaining block.
|
||||
(define close-rest
|
||||
(fn ()
|
||||
(when (not (empty? stack))
|
||||
(do
|
||||
(append! out (layout-mk-virtual close-tmpl 0 0))
|
||||
(set! stack (rest stack))
|
||||
(close-rest)))))
|
||||
(close-rest)
|
||||
out))))
|
||||
89
lib/guest/tests/hm.sx
Normal file
89
lib/guest/tests/hm.sx
Normal file
@@ -0,0 +1,89 @@
|
||||
;; lib/guest/tests/hm.sx — exercises lib/guest/hm.sx algebra.
|
||||
|
||||
(define ghm-test-pass 0)
|
||||
(define ghm-test-fail 0)
|
||||
(define ghm-test-fails (list))
|
||||
|
||||
(define
|
||||
ghm-test
|
||||
(fn (name actual expected)
|
||||
(if (= actual expected)
|
||||
(set! ghm-test-pass (+ ghm-test-pass 1))
|
||||
(begin
|
||||
(set! ghm-test-fail (+ ghm-test-fail 1))
|
||||
(append! ghm-test-fails {:name name :expected expected :actual actual})))))
|
||||
|
||||
;; ── Type constructors ─────────────────────────────────────────────
|
||||
(ghm-test "tv" (hm-tv "a") (list :var "a"))
|
||||
(ghm-test "int" (hm-int) (list :ctor "Int" (list)))
|
||||
(ghm-test "arrow" (ctor-head (hm-arrow (hm-int) (hm-bool))) "->")
|
||||
(ghm-test "arrow-args-len" (len (ctor-args (hm-arrow (hm-int) (hm-bool)))) 2)
|
||||
|
||||
;; ── Schemes ───────────────────────────────────────────────────────
|
||||
(ghm-test "scheme-vars" (hm-scheme-vars (hm-scheme (list "a") (hm-tv "a"))) (list "a"))
|
||||
(ghm-test "monotype-vars" (hm-scheme-vars (hm-monotype (hm-int))) (list))
|
||||
(ghm-test "scheme?-yes" (hm-scheme? (hm-monotype (hm-int))) true)
|
||||
(ghm-test "scheme?-no" (hm-scheme? (hm-int)) false)
|
||||
|
||||
;; ── Fresh tyvars ──────────────────────────────────────────────────
|
||||
(ghm-test "fresh-1"
|
||||
(let ((c (list 0))) (var-name (hm-fresh-tv c))) "t1")
|
||||
(ghm-test "fresh-bumps"
|
||||
(let ((c (list 5))) (begin (hm-fresh-tv c) (first c))) 6)
|
||||
|
||||
;; ── Free type variables ──────────────────────────────────────────
|
||||
(ghm-test "ftv-int" (hm-ftv (hm-int)) (list))
|
||||
(ghm-test "ftv-tv" (hm-ftv (hm-tv "a")) (list "a"))
|
||||
(ghm-test "ftv-arrow"
|
||||
(len (hm-ftv (hm-arrow (hm-tv "a") (hm-arrow (hm-tv "b") (hm-tv "a"))))) 2)
|
||||
(ghm-test "ftv-scheme-quantified"
|
||||
(hm-ftv-scheme (hm-scheme (list "a") (hm-arrow (hm-tv "a") (hm-tv "b")))) (list "b"))
|
||||
(ghm-test "ftv-env"
|
||||
(let ((env (assoc {} "f" (hm-monotype (hm-arrow (hm-tv "x") (hm-tv "y"))))))
|
||||
(len (hm-ftv-env env))) 2)
|
||||
|
||||
;; ── Substitution / apply / compose ───────────────────────────────
|
||||
(ghm-test "apply-tv"
|
||||
(hm-apply (assoc {} "a" (hm-int)) (hm-tv "a")) (hm-int))
|
||||
(ghm-test "apply-arrow"
|
||||
(ctor-head
|
||||
(hm-apply (assoc {} "a" (hm-int))
|
||||
(hm-arrow (hm-tv "a") (hm-tv "b")))) "->")
|
||||
(ghm-test "compose-1-then-2"
|
||||
(var-name
|
||||
(hm-apply
|
||||
(hm-compose (assoc {} "b" (hm-tv "c")) (assoc {} "a" (hm-tv "b")))
|
||||
(hm-tv "a"))) "c")
|
||||
|
||||
;; ── Generalize / Instantiate ─────────────────────────────────────
|
||||
;; forall a. a -> a instantiated twice yields fresh vars each time
|
||||
(ghm-test "generalize-id"
|
||||
(len (hm-scheme-vars (hm-generalize (hm-arrow (hm-tv "a") (hm-tv "a")) {}))) 1)
|
||||
|
||||
(ghm-test "generalize-skips-env"
|
||||
;; ftv(t)={a,b}, ftv(env)={a}, qs={b}
|
||||
(let ((env (assoc {} "x" (hm-monotype (hm-tv "a")))))
|
||||
(len (hm-scheme-vars
|
||||
(hm-generalize (hm-arrow (hm-tv "a") (hm-tv "b")) env)))) 1)
|
||||
|
||||
(ghm-test "instantiate-fresh"
|
||||
(let ((s (hm-scheme (list "a") (hm-arrow (hm-tv "a") (hm-tv "a"))))
|
||||
(c (list 0)))
|
||||
(let ((t1 (hm-instantiate s c)) (t2 (hm-instantiate s c)))
|
||||
(not (= (var-name (first (ctor-args t1)))
|
||||
(var-name (first (ctor-args t2)))))))
|
||||
true)
|
||||
|
||||
;; ── Inference (literal only) ─────────────────────────────────────
|
||||
(ghm-test "infer-int"
|
||||
(ctor-head (get (hm-infer-literal (ast-literal 42)) :type)) "Int")
|
||||
(ghm-test "infer-string"
|
||||
(ctor-head (get (hm-infer-literal (ast-literal "hi")) :type)) "String")
|
||||
(ghm-test "infer-bool"
|
||||
(ctor-head (get (hm-infer-literal (ast-literal true)) :type)) "Bool")
|
||||
|
||||
(define ghm-tests-run!
|
||||
(fn ()
|
||||
{:passed ghm-test-pass
|
||||
:failed ghm-test-fail
|
||||
:total (+ ghm-test-pass ghm-test-fail)}))
|
||||
180
lib/guest/tests/layout.sx
Normal file
180
lib/guest/tests/layout.sx
Normal file
@@ -0,0 +1,180 @@
|
||||
;; lib/guest/tests/layout.sx — synthetic Python-ish off-side fixture.
|
||||
;;
|
||||
;; Exercises lib/guest/layout.sx with a config different from Haskell's
|
||||
;; (no module-prelude, layout opens via trailing `:` not via reserved
|
||||
;; keyword) to prove the kit isn't Haskell-shaped.
|
||||
|
||||
(define glayout-test-pass 0)
|
||||
(define glayout-test-fail 0)
|
||||
(define glayout-test-fails (list))
|
||||
|
||||
(define
|
||||
glayout-test
|
||||
(fn (name actual expected)
|
||||
(if (= actual expected)
|
||||
(set! glayout-test-pass (+ glayout-test-pass 1))
|
||||
(begin
|
||||
(set! glayout-test-fail (+ glayout-test-fail 1))
|
||||
(append! glayout-test-fails {:name name :expected expected :actual actual})))))
|
||||
|
||||
;; Convenience: build a token from {type value line col}.
|
||||
(define
|
||||
glayout-tok
|
||||
(fn (ty val line col)
|
||||
{:type ty :value val :line line :col col}))
|
||||
|
||||
;; Project a token list to ((type value) ...) for compact comparison.
|
||||
(define
|
||||
glayout-shape
|
||||
(fn (toks)
|
||||
(map (fn (t) (list (get t :type) (get t :value))) toks)))
|
||||
|
||||
;; ── Haskell-flavour: keyword opens block ─────────────────────────
|
||||
(define
|
||||
glayout-haskell-cfg
|
||||
{:open-keywords (list "let" "where" "do" "of")
|
||||
:open-trailing-fn nil
|
||||
:open-token {:type "vlbrace" :value "{"}
|
||||
:close-token {:type "vrbrace" :value "}"}
|
||||
:sep-token {:type "vsemi" :value ";"}
|
||||
:module-prelude? false
|
||||
:explicit-open? (fn (tok) (= (get tok :type) "lbrace"))})
|
||||
|
||||
;; do
|
||||
;; a
|
||||
;; b
|
||||
;; c ← outside the do-block
|
||||
(glayout-test "haskell-do-block"
|
||||
(glayout-shape
|
||||
(layout-pass
|
||||
glayout-haskell-cfg
|
||||
(list (glayout-tok "reserved" "do" 1 1)
|
||||
(glayout-tok "ident" "a" 2 3)
|
||||
(glayout-tok "ident" "b" 3 3)
|
||||
(glayout-tok "ident" "c" 4 1))))
|
||||
(list (list "reserved" "do")
|
||||
(list "vlbrace" "{")
|
||||
(list "ident" "a")
|
||||
(list "vsemi" ";")
|
||||
(list "ident" "b")
|
||||
(list "vrbrace" "}")
|
||||
(list "ident" "c")))
|
||||
|
||||
;; Explicit `{` after `do` suppresses virtual layout.
|
||||
(glayout-test "haskell-explicit-brace"
|
||||
(glayout-shape
|
||||
(layout-pass
|
||||
glayout-haskell-cfg
|
||||
(list (glayout-tok "reserved" "do" 1 1)
|
||||
(glayout-tok "lbrace" "{" 1 4)
|
||||
(glayout-tok "ident" "a" 1 6)
|
||||
(glayout-tok "rbrace" "}" 1 8))))
|
||||
(list (list "reserved" "do")
|
||||
(list "lbrace" "{")
|
||||
(list "ident" "a")
|
||||
(list "rbrace" "}")))
|
||||
|
||||
;; Single-statement do-block on the same line.
|
||||
(glayout-test "haskell-do-inline"
|
||||
(glayout-shape
|
||||
(layout-pass
|
||||
glayout-haskell-cfg
|
||||
(list (glayout-tok "reserved" "do" 1 1)
|
||||
(glayout-tok "ident" "a" 1 4))))
|
||||
(list (list "reserved" "do")
|
||||
(list "vlbrace" "{")
|
||||
(list "ident" "a")
|
||||
(list "vrbrace" "}")))
|
||||
|
||||
;; Module-prelude: wrap whole input in implicit layout block at first
|
||||
;; tok's column.
|
||||
(glayout-test "haskell-module-prelude"
|
||||
(glayout-shape
|
||||
(layout-pass
|
||||
(assoc glayout-haskell-cfg :module-prelude? true)
|
||||
(list (glayout-tok "ident" "x" 1 1)
|
||||
(glayout-tok "ident" "y" 2 1)
|
||||
(glayout-tok "ident" "z" 3 1))))
|
||||
(list (list "vlbrace" "{")
|
||||
(list "ident" "x")
|
||||
(list "vsemi" ";")
|
||||
(list "ident" "y")
|
||||
(list "vsemi" ";")
|
||||
(list "ident" "z")
|
||||
(list "vrbrace" "}")))
|
||||
|
||||
;; ── Python-flavour: trailing `:` opens block ─────────────────────
|
||||
(define
|
||||
glayout-python-cfg
|
||||
{:open-keywords (list)
|
||||
:open-trailing-fn (fn (tok) (and (= (get tok :type) "punct")
|
||||
(= (get tok :value) ":")))
|
||||
:open-token {:type "indent" :value "INDENT"}
|
||||
:close-token {:type "dedent" :value "DEDENT"}
|
||||
:sep-token {:type "newline" :value "NEWLINE"}
|
||||
:module-prelude? false
|
||||
:explicit-open? nil})
|
||||
|
||||
;; if x:
|
||||
;; a
|
||||
;; b
|
||||
;; c
|
||||
(glayout-test "python-if-block"
|
||||
(glayout-shape
|
||||
(layout-pass
|
||||
glayout-python-cfg
|
||||
(list (glayout-tok "reserved" "if" 1 1)
|
||||
(glayout-tok "ident" "x" 1 4)
|
||||
(glayout-tok "punct" ":" 1 5)
|
||||
(glayout-tok "ident" "a" 2 5)
|
||||
(glayout-tok "ident" "b" 3 5)
|
||||
(glayout-tok "ident" "c" 4 1))))
|
||||
(list (list "reserved" "if")
|
||||
(list "ident" "x")
|
||||
(list "punct" ":")
|
||||
(list "indent" "INDENT")
|
||||
(list "ident" "a")
|
||||
(list "newline" "NEWLINE")
|
||||
(list "ident" "b")
|
||||
(list "dedent" "DEDENT")
|
||||
(list "ident" "c")))
|
||||
|
||||
;; Nested Python-style blocks.
|
||||
;; def f():
|
||||
;; if x:
|
||||
;; a
|
||||
;; b
|
||||
(glayout-test "python-nested"
|
||||
(glayout-shape
|
||||
(layout-pass
|
||||
glayout-python-cfg
|
||||
(list (glayout-tok "reserved" "def" 1 1)
|
||||
(glayout-tok "ident" "f" 1 5)
|
||||
(glayout-tok "punct" "(" 1 6)
|
||||
(glayout-tok "punct" ")" 1 7)
|
||||
(glayout-tok "punct" ":" 1 8)
|
||||
(glayout-tok "reserved" "if" 2 5)
|
||||
(glayout-tok "ident" "x" 2 8)
|
||||
(glayout-tok "punct" ":" 2 9)
|
||||
(glayout-tok "ident" "a" 3 9)
|
||||
(glayout-tok "ident" "b" 4 5))))
|
||||
(list (list "reserved" "def")
|
||||
(list "ident" "f")
|
||||
(list "punct" "(")
|
||||
(list "punct" ")")
|
||||
(list "punct" ":")
|
||||
(list "indent" "INDENT")
|
||||
(list "reserved" "if")
|
||||
(list "ident" "x")
|
||||
(list "punct" ":")
|
||||
(list "indent" "INDENT")
|
||||
(list "ident" "a")
|
||||
(list "dedent" "DEDENT")
|
||||
(list "ident" "b")
|
||||
(list "dedent" "DEDENT")))
|
||||
|
||||
(define glayout-tests-run!
|
||||
(fn ()
|
||||
{:passed glayout-test-pass
|
||||
:failed glayout-test-fail
|
||||
:total (+ glayout-test-pass glayout-test-fail)}))
|
||||
@@ -14,6 +14,8 @@ PRELOADS=(
|
||||
lib/haskell/runtime.sx
|
||||
lib/haskell/match.sx
|
||||
lib/haskell/eval.sx
|
||||
lib/haskell/map.sx
|
||||
lib/haskell/set.sx
|
||||
lib/haskell/testlib.sx
|
||||
)
|
||||
|
||||
@@ -36,6 +38,24 @@ SUITES=(
|
||||
"matrix:lib/haskell/tests/program-matrix.sx"
|
||||
"wordcount:lib/haskell/tests/program-wordcount.sx"
|
||||
"powers:lib/haskell/tests/program-powers.sx"
|
||||
"caesar:lib/haskell/tests/program-caesar.sx"
|
||||
"runlength-str:lib/haskell/tests/program-runlength-str.sx"
|
||||
"showadt:lib/haskell/tests/program-showadt.sx"
|
||||
"showio:lib/haskell/tests/program-showio.sx"
|
||||
"partial:lib/haskell/tests/program-partial.sx"
|
||||
"statistics:lib/haskell/tests/program-statistics.sx"
|
||||
"newton:lib/haskell/tests/program-newton.sx"
|
||||
"wordfreq:lib/haskell/tests/program-wordfreq.sx"
|
||||
"mapgraph:lib/haskell/tests/program-mapgraph.sx"
|
||||
"uniquewords:lib/haskell/tests/program-uniquewords.sx"
|
||||
"setops:lib/haskell/tests/program-setops.sx"
|
||||
"shapes:lib/haskell/tests/program-shapes.sx"
|
||||
"person:lib/haskell/tests/program-person.sx"
|
||||
"config:lib/haskell/tests/program-config.sx"
|
||||
"counter:lib/haskell/tests/program-counter.sx"
|
||||
"accumulate:lib/haskell/tests/program-accumulate.sx"
|
||||
"safediv:lib/haskell/tests/program-safediv.sx"
|
||||
"trycatch:lib/haskell/tests/program-trycatch.sx"
|
||||
)
|
||||
|
||||
emit_scoreboard_json() {
|
||||
|
||||
@@ -131,119 +131,280 @@
|
||||
(let
|
||||
((tag (first node)))
|
||||
(cond
|
||||
;; Transformations
|
||||
((= tag "where")
|
||||
(list
|
||||
:let
|
||||
(map hk-desugar (nth node 2))
|
||||
:let (map hk-desugar (nth node 2))
|
||||
(hk-desugar (nth node 1))))
|
||||
((= tag "guarded") (hk-guards-to-if (nth node 1)))
|
||||
((= tag "list-comp")
|
||||
(hk-lc-desugar
|
||||
(hk-desugar (nth node 1))
|
||||
(nth node 2)))
|
||||
|
||||
;; Expression nodes
|
||||
(hk-lc-desugar (hk-desugar (nth node 1)) (nth node 2)))
|
||||
((= tag "app")
|
||||
(list
|
||||
:app
|
||||
(hk-desugar (nth node 1))
|
||||
:app (hk-desugar (nth node 1))
|
||||
(hk-desugar (nth node 2))))
|
||||
((= tag "p-rec")
|
||||
(let
|
||||
((cname (nth node 1))
|
||||
(field-pats (nth node 2))
|
||||
(field-order (hk-record-field-names cname)))
|
||||
(cond
|
||||
((nil? field-order)
|
||||
(raise (str "p-rec: no record info for " cname)))
|
||||
(:else
|
||||
(list
|
||||
:p-con
|
||||
cname
|
||||
(map
|
||||
(fn
|
||||
(fname)
|
||||
(let
|
||||
((p (hk-find-rec-pair field-pats fname)))
|
||||
(cond
|
||||
((nil? p) (list :p-wild))
|
||||
(:else (hk-desugar (nth p 1))))))
|
||||
field-order))))))
|
||||
((= tag "rec-update")
|
||||
(list
|
||||
:rec-update
|
||||
(hk-desugar (nth node 1))
|
||||
(map
|
||||
(fn (p) (list (first p) (hk-desugar (nth p 1))))
|
||||
(nth node 2))))
|
||||
((= tag "rec-create")
|
||||
(let
|
||||
((cname (nth node 1))
|
||||
(field-pairs (nth node 2))
|
||||
(field-order (hk-record-field-names cname)))
|
||||
(cond
|
||||
((nil? field-order)
|
||||
(raise (str "rec-create: no record info for " cname)))
|
||||
(:else
|
||||
(let
|
||||
((acc (list :con cname)))
|
||||
(begin
|
||||
(for-each
|
||||
(fn
|
||||
(fname)
|
||||
(let
|
||||
((pair
|
||||
(hk-find-rec-pair field-pairs fname)))
|
||||
(cond
|
||||
((nil? pair)
|
||||
(raise
|
||||
(str
|
||||
"rec-create: missing field "
|
||||
fname
|
||||
" for "
|
||||
cname)))
|
||||
(:else
|
||||
(set!
|
||||
acc
|
||||
(list
|
||||
:app
|
||||
acc
|
||||
(hk-desugar (nth pair 1))))))))
|
||||
field-order)
|
||||
acc))))))
|
||||
((= tag "op")
|
||||
(list
|
||||
:op
|
||||
(nth node 1)
|
||||
:op (nth node 1)
|
||||
(hk-desugar (nth node 2))
|
||||
(hk-desugar (nth node 3))))
|
||||
((= tag "neg") (list :neg (hk-desugar (nth node 1))))
|
||||
((= tag "if")
|
||||
(list
|
||||
:if
|
||||
(hk-desugar (nth node 1))
|
||||
:if (hk-desugar (nth node 1))
|
||||
(hk-desugar (nth node 2))
|
||||
(hk-desugar (nth node 3))))
|
||||
((= tag "tuple")
|
||||
(list :tuple (map hk-desugar (nth node 1))))
|
||||
((= tag "list")
|
||||
(list :list (map hk-desugar (nth node 1))))
|
||||
((= tag "tuple") (list :tuple (map hk-desugar (nth node 1))))
|
||||
((= tag "list") (list :list (map hk-desugar (nth node 1))))
|
||||
((= tag "range")
|
||||
(list
|
||||
:range
|
||||
(hk-desugar (nth node 1))
|
||||
:range (hk-desugar (nth node 1))
|
||||
(hk-desugar (nth node 2))))
|
||||
((= tag "range-step")
|
||||
(list
|
||||
:range-step
|
||||
(hk-desugar (nth node 1))
|
||||
:range-step (hk-desugar (nth node 1))
|
||||
(hk-desugar (nth node 2))
|
||||
(hk-desugar (nth node 3))))
|
||||
((= tag "lambda")
|
||||
(list
|
||||
:lambda
|
||||
(nth node 1)
|
||||
(hk-desugar (nth node 2))))
|
||||
(list :lambda (nth node 1) (hk-desugar (nth node 2))))
|
||||
((= tag "let")
|
||||
(list
|
||||
:let
|
||||
(map hk-desugar (nth node 1))
|
||||
:let (map hk-desugar (nth node 1))
|
||||
(hk-desugar (nth node 2))))
|
||||
((= tag "case")
|
||||
(list
|
||||
:case
|
||||
(hk-desugar (nth node 1))
|
||||
:case (hk-desugar (nth node 1))
|
||||
(map hk-desugar (nth node 2))))
|
||||
((= tag "alt")
|
||||
(list :alt (nth node 1) (hk-desugar (nth node 2))))
|
||||
(list :alt (hk-desugar (nth node 1)) (hk-desugar (nth node 2))))
|
||||
((= tag "do") (hk-desugar-do (nth node 1)))
|
||||
((= tag "sect-left")
|
||||
(list
|
||||
:sect-left
|
||||
(nth node 1)
|
||||
(hk-desugar (nth node 2))))
|
||||
(list :sect-left (nth node 1) (hk-desugar (nth node 2))))
|
||||
((= tag "sect-right")
|
||||
(list
|
||||
:sect-right
|
||||
(nth node 1)
|
||||
(hk-desugar (nth node 2))))
|
||||
|
||||
;; Top-level
|
||||
(list :sect-right (nth node 1) (hk-desugar (nth node 2))))
|
||||
((= tag "program")
|
||||
(list :program (map hk-desugar (nth node 1))))
|
||||
(list :program (map hk-desugar (hk-expand-records (nth node 1)))))
|
||||
((= tag "module")
|
||||
(list
|
||||
:module
|
||||
(nth node 1)
|
||||
:module (nth node 1)
|
||||
(nth node 2)
|
||||
(nth node 3)
|
||||
(map hk-desugar (nth node 4))))
|
||||
|
||||
;; Decls carrying a body
|
||||
(map hk-desugar (hk-expand-records (nth node 4)))))
|
||||
((= tag "fun-clause")
|
||||
(list
|
||||
:fun-clause
|
||||
(nth node 1)
|
||||
(nth node 2)
|
||||
:fun-clause (nth node 1)
|
||||
(map hk-desugar (nth node 2))
|
||||
(hk-desugar (nth node 3))))
|
||||
((= tag "instance-decl")
|
||||
(list
|
||||
:instance-decl (nth node 1)
|
||||
(nth node 2)
|
||||
(map hk-desugar (nth node 3))))
|
||||
((= tag "pat-bind")
|
||||
(list
|
||||
:pat-bind
|
||||
(nth node 1)
|
||||
(hk-desugar (nth node 2))))
|
||||
(list :pat-bind (nth node 1) (hk-desugar (nth node 2))))
|
||||
((= tag "bind")
|
||||
(list
|
||||
:bind
|
||||
(nth node 1)
|
||||
(hk-desugar (nth node 2))))
|
||||
|
||||
;; Everything else: leaf literals, vars, cons, patterns,
|
||||
;; types, imports, type-sigs, data / newtype / fixity, …
|
||||
(list :bind (nth node 1) (hk-desugar (nth node 2))))
|
||||
(:else node)))))))
|
||||
|
||||
;; Convenience — tokenize + layout + parse + desugar.
|
||||
(define
|
||||
hk-core
|
||||
(fn (src) (hk-desugar (hk-parse-top src))))
|
||||
(define hk-record-fields (dict))
|
||||
|
||||
(define
|
||||
hk-core-expr
|
||||
(fn (src) (hk-desugar (hk-parse src))))
|
||||
hk-register-record-fields!
|
||||
(fn (cname fields) (dict-set! hk-record-fields cname fields)))
|
||||
|
||||
(define
|
||||
hk-record-field-names
|
||||
(fn
|
||||
(cname)
|
||||
(if (has-key? hk-record-fields cname) (get hk-record-fields cname) nil)))
|
||||
|
||||
(define
|
||||
hk-record-field-index
|
||||
(fn
|
||||
(cname fname)
|
||||
(let
|
||||
((fields (hk-record-field-names cname)))
|
||||
(cond
|
||||
((nil? fields) -1)
|
||||
(:else
|
||||
(let
|
||||
((i 0) (idx -1))
|
||||
(begin
|
||||
(for-each
|
||||
(fn
|
||||
(f)
|
||||
(begin (when (= f fname) (set! idx i)) (set! i (+ i 1))))
|
||||
fields)
|
||||
idx)))))))
|
||||
|
||||
(define
|
||||
hk-find-rec-pair
|
||||
(fn
|
||||
(pairs name)
|
||||
(cond
|
||||
((empty? pairs) nil)
|
||||
((= (first (first pairs)) name) (first pairs))
|
||||
(:else (hk-find-rec-pair (rest pairs) name)))))
|
||||
|
||||
(define
|
||||
hk-record-accessors
|
||||
(fn
|
||||
(cname rec-fields)
|
||||
(let
|
||||
((n (len rec-fields)) (i 0) (out (list)))
|
||||
(define
|
||||
hk-ra-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< i n)
|
||||
(let
|
||||
((field (nth rec-fields i)))
|
||||
(let
|
||||
((fname (first field)) (j 0) (pats (list)))
|
||||
(define
|
||||
hk-pat-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< j n)
|
||||
(begin
|
||||
(append!
|
||||
pats
|
||||
(if
|
||||
(= j i)
|
||||
(list "p-var" "__rec_field")
|
||||
(list "p-wild")))
|
||||
(set! j (+ j 1))
|
||||
(hk-pat-loop)))))
|
||||
(hk-pat-loop)
|
||||
(append!
|
||||
out
|
||||
(list
|
||||
"fun-clause"
|
||||
fname
|
||||
(list (list "p-con" cname pats))
|
||||
(list "var" "__rec_field")))
|
||||
(set! i (+ i 1))
|
||||
(hk-ra-loop))))))
|
||||
(hk-ra-loop)
|
||||
out)))
|
||||
|
||||
(define
|
||||
hk-expand-records
|
||||
(fn
|
||||
(decls)
|
||||
(let
|
||||
((out (list)))
|
||||
(for-each
|
||||
(fn
|
||||
(d)
|
||||
(cond
|
||||
((and (list? d) (= (first d) "data"))
|
||||
(let
|
||||
((dname (nth d 1))
|
||||
(tvars (nth d 2))
|
||||
(cons-list (nth d 3))
|
||||
(deriving (if (> (len d) 4) (nth d 4) (list)))
|
||||
(new-cons (list))
|
||||
(accessors (list)))
|
||||
(begin
|
||||
(for-each
|
||||
(fn
|
||||
(c)
|
||||
(cond
|
||||
((= (first c) "con-rec")
|
||||
(let
|
||||
((cname (nth c 1)) (rec-fields (nth c 2)))
|
||||
(begin
|
||||
(hk-register-record-fields!
|
||||
cname
|
||||
(map (fn (f) (first f)) rec-fields))
|
||||
(append!
|
||||
new-cons
|
||||
(list
|
||||
"con-def"
|
||||
cname
|
||||
(map (fn (f) (nth f 1)) rec-fields)))
|
||||
(for-each
|
||||
(fn (a) (append! accessors a))
|
||||
(hk-record-accessors cname rec-fields)))))
|
||||
(:else (append! new-cons c))))
|
||||
cons-list)
|
||||
(append!
|
||||
out
|
||||
(if
|
||||
(empty? deriving)
|
||||
(list "data" dname tvars new-cons)
|
||||
(list "data" dname tvars new-cons deriving)))
|
||||
(for-each (fn (a) (append! out a)) accessors))))
|
||||
(:else (append! out d))))
|
||||
decls)
|
||||
out)))
|
||||
|
||||
(define hk-core (fn (src) (hk-desugar (hk-parse-top src))))
|
||||
|
||||
(define hk-core-expr (fn (src) (hk-desugar (hk-parse src))))
|
||||
|
||||
1023
lib/haskell/eval.sx
1023
lib/haskell/eval.sx
File diff suppressed because one or more lines are too long
520
lib/haskell/map.sx
Normal file
520
lib/haskell/map.sx
Normal file
@@ -0,0 +1,520 @@
|
||||
;; map.sx — Phase 11 Data.Map: weight-balanced BST in pure SX.
|
||||
;;
|
||||
;; Algorithm: Adams's weight-balanced tree (the same family as Haskell's
|
||||
;; Data.Map). Each node tracks its size; rotations maintain the invariant
|
||||
;;
|
||||
;; size(small-side) * delta >= size(large-side) (delta = 3)
|
||||
;;
|
||||
;; with single or double rotations chosen by the gamma ratio (gamma = 2).
|
||||
;; The size field is an Int and is included so `size`, `lookup`, etc. are
|
||||
;; O(log n) on both extremes of the tree.
|
||||
;;
|
||||
;; Representation:
|
||||
;; Empty → ("Map-Empty")
|
||||
;; Node → ("Map-Node" key val left right size)
|
||||
;;
|
||||
;; All operations are pure SX — no mutation of nodes once constructed.
|
||||
;; The user-facing Haskell layer (Phase 11 next iteration) wraps these
|
||||
;; for `import Data.Map as Map`.
|
||||
|
||||
;; ── Constructors ────────────────────────────────────────────
|
||||
(define hk-map-empty (list "Map-Empty"))
|
||||
|
||||
(define
|
||||
hk-map-node
|
||||
(fn
|
||||
(k v l r)
|
||||
(list "Map-Node" k v l r (+ 1 (+ (hk-map-size l) (hk-map-size r))))))
|
||||
|
||||
;; ── Predicates and accessors ────────────────────────────────
|
||||
(define hk-map-empty? (fn (m) (and (list? m) (= (first m) "Map-Empty"))))
|
||||
|
||||
(define hk-map-node? (fn (m) (and (list? m) (= (first m) "Map-Node"))))
|
||||
|
||||
(define
|
||||
hk-map-size
|
||||
(fn (m) (cond ((hk-map-empty? m) 0) (:else (nth m 5)))))
|
||||
|
||||
(define hk-map-key (fn (m) (nth m 1)))
|
||||
(define hk-map-val (fn (m) (nth m 2)))
|
||||
(define hk-map-left (fn (m) (nth m 3)))
|
||||
(define hk-map-right (fn (m) (nth m 4)))
|
||||
|
||||
;; ── Weight-balanced rotations ───────────────────────────────
|
||||
;; delta and gamma per Adams 1992 / Haskell Data.Map.
|
||||
|
||||
(define hk-map-delta 3)
|
||||
(define hk-map-gamma 2)
|
||||
|
||||
(define
|
||||
hk-map-single-l
|
||||
(fn
|
||||
(k v l r)
|
||||
(let
|
||||
((rk (hk-map-key r))
|
||||
(rv (hk-map-val r))
|
||||
(rl (hk-map-left r))
|
||||
(rr (hk-map-right r)))
|
||||
(hk-map-node rk rv (hk-map-node k v l rl) rr))))
|
||||
|
||||
(define
|
||||
hk-map-single-r
|
||||
(fn
|
||||
(k v l r)
|
||||
(let
|
||||
((lk (hk-map-key l))
|
||||
(lv (hk-map-val l))
|
||||
(ll (hk-map-left l))
|
||||
(lr (hk-map-right l)))
|
||||
(hk-map-node lk lv ll (hk-map-node k v lr r)))))
|
||||
|
||||
(define
|
||||
hk-map-double-l
|
||||
(fn
|
||||
(k v l r)
|
||||
(let
|
||||
((rk (hk-map-key r))
|
||||
(rv (hk-map-val r))
|
||||
(rl (hk-map-left r))
|
||||
(rr (hk-map-right r))
|
||||
(rlk (hk-map-key (hk-map-left r)))
|
||||
(rlv (hk-map-val (hk-map-left r)))
|
||||
(rll (hk-map-left (hk-map-left r)))
|
||||
(rlr (hk-map-right (hk-map-left r))))
|
||||
(hk-map-node
|
||||
rlk
|
||||
rlv
|
||||
(hk-map-node k v l rll)
|
||||
(hk-map-node rk rv rlr rr)))))
|
||||
|
||||
(define
|
||||
hk-map-double-r
|
||||
(fn
|
||||
(k v l r)
|
||||
(let
|
||||
((lk (hk-map-key l))
|
||||
(lv (hk-map-val l))
|
||||
(ll (hk-map-left l))
|
||||
(lr (hk-map-right l))
|
||||
(lrk (hk-map-key (hk-map-right l)))
|
||||
(lrv (hk-map-val (hk-map-right l)))
|
||||
(lrl (hk-map-left (hk-map-right l)))
|
||||
(lrr (hk-map-right (hk-map-right l))))
|
||||
(hk-map-node
|
||||
lrk
|
||||
lrv
|
||||
(hk-map-node lk lv ll lrl)
|
||||
(hk-map-node k v lrr r)))))
|
||||
|
||||
;; ── Balanced node constructor ──────────────────────────────
|
||||
;; Use this in place of hk-map-node when one side may have grown
|
||||
;; or shrunk by one and we need to restore the weight invariant.
|
||||
(define
|
||||
hk-map-balance
|
||||
(fn
|
||||
(k v l r)
|
||||
(let
|
||||
((sl (hk-map-size l)) (sr (hk-map-size r)))
|
||||
(cond
|
||||
((<= (+ sl sr) 1) (hk-map-node k v l r))
|
||||
((> sr (* hk-map-delta sl))
|
||||
(let
|
||||
((rl (hk-map-left r)) (rr (hk-map-right r)))
|
||||
(cond
|
||||
((< (hk-map-size rl) (* hk-map-gamma (hk-map-size rr)))
|
||||
(hk-map-single-l k v l r))
|
||||
(:else (hk-map-double-l k v l r)))))
|
||||
((> sl (* hk-map-delta sr))
|
||||
(let
|
||||
((ll (hk-map-left l)) (lr (hk-map-right l)))
|
||||
(cond
|
||||
((< (hk-map-size lr) (* hk-map-gamma (hk-map-size ll)))
|
||||
(hk-map-single-r k v l r))
|
||||
(:else (hk-map-double-r k v l r)))))
|
||||
(:else (hk-map-node k v l r))))))
|
||||
|
||||
(define
|
||||
hk-map-singleton
|
||||
(fn (k v) (hk-map-node k v hk-map-empty hk-map-empty)))
|
||||
|
||||
(define
|
||||
hk-map-insert
|
||||
(fn
|
||||
(k v m)
|
||||
(cond
|
||||
((hk-map-empty? m) (hk-map-singleton k v))
|
||||
(:else
|
||||
(let
|
||||
((mk (hk-map-key m)))
|
||||
(cond
|
||||
((< k mk)
|
||||
(hk-map-balance
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-insert k v (hk-map-left m))
|
||||
(hk-map-right m)))
|
||||
((> k mk)
|
||||
(hk-map-balance
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-left m)
|
||||
(hk-map-insert k v (hk-map-right m))))
|
||||
(:else (hk-map-node k v (hk-map-left m) (hk-map-right m)))))))))
|
||||
|
||||
(define
|
||||
hk-map-lookup
|
||||
(fn
|
||||
(k m)
|
||||
(cond
|
||||
((hk-map-empty? m) (list "Nothing"))
|
||||
(:else
|
||||
(let
|
||||
((mk (hk-map-key m)))
|
||||
(cond
|
||||
((< k mk) (hk-map-lookup k (hk-map-left m)))
|
||||
((> k mk) (hk-map-lookup k (hk-map-right m)))
|
||||
(:else (list "Just" (hk-map-val m)))))))))
|
||||
|
||||
(define
|
||||
hk-map-member
|
||||
(fn
|
||||
(k m)
|
||||
(cond
|
||||
((hk-map-empty? m) false)
|
||||
(:else
|
||||
(let
|
||||
((mk (hk-map-key m)))
|
||||
(cond
|
||||
((< k mk) (hk-map-member k (hk-map-left m)))
|
||||
((> k mk) (hk-map-member k (hk-map-right m)))
|
||||
(:else true)))))))
|
||||
|
||||
(define hk-map-null hk-map-empty?)
|
||||
|
||||
(define
|
||||
hk-map-find-min
|
||||
(fn
|
||||
(m)
|
||||
(cond
|
||||
((hk-map-empty? (hk-map-left m))
|
||||
(list (hk-map-key m) (hk-map-val m)))
|
||||
(:else (hk-map-find-min (hk-map-left m))))))
|
||||
|
||||
(define
|
||||
hk-map-delete-min
|
||||
(fn
|
||||
(m)
|
||||
(cond
|
||||
((hk-map-empty? (hk-map-left m)) (hk-map-right m))
|
||||
(:else
|
||||
(hk-map-balance
|
||||
(hk-map-key m)
|
||||
(hk-map-val m)
|
||||
(hk-map-delete-min (hk-map-left m))
|
||||
(hk-map-right m))))))
|
||||
|
||||
(define
|
||||
hk-map-find-max
|
||||
(fn
|
||||
(m)
|
||||
(cond
|
||||
((hk-map-empty? (hk-map-right m))
|
||||
(list (hk-map-key m) (hk-map-val m)))
|
||||
(:else (hk-map-find-max (hk-map-right m))))))
|
||||
|
||||
(define
|
||||
hk-map-delete-max
|
||||
(fn
|
||||
(m)
|
||||
(cond
|
||||
((hk-map-empty? (hk-map-right m)) (hk-map-left m))
|
||||
(:else
|
||||
(hk-map-balance
|
||||
(hk-map-key m)
|
||||
(hk-map-val m)
|
||||
(hk-map-left m)
|
||||
(hk-map-delete-max (hk-map-right m)))))))
|
||||
|
||||
(define
|
||||
hk-map-glue
|
||||
(fn
|
||||
(l r)
|
||||
(cond
|
||||
((hk-map-empty? l) r)
|
||||
((hk-map-empty? r) l)
|
||||
((> (hk-map-size l) (hk-map-size r))
|
||||
(let
|
||||
((mp (hk-map-find-max l)))
|
||||
(hk-map-balance (first mp) (nth mp 1) (hk-map-delete-max l) r)))
|
||||
(:else
|
||||
(let
|
||||
((mp (hk-map-find-min r)))
|
||||
(hk-map-balance (first mp) (nth mp 1) l (hk-map-delete-min r)))))))
|
||||
|
||||
(define
|
||||
hk-map-delete
|
||||
(fn
|
||||
(k m)
|
||||
(cond
|
||||
((hk-map-empty? m) m)
|
||||
(:else
|
||||
(let
|
||||
((mk (hk-map-key m)))
|
||||
(cond
|
||||
((< k mk)
|
||||
(hk-map-balance
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-delete k (hk-map-left m))
|
||||
(hk-map-right m)))
|
||||
((> k mk)
|
||||
(hk-map-balance
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-left m)
|
||||
(hk-map-delete k (hk-map-right m))))
|
||||
(:else (hk-map-glue (hk-map-left m) (hk-map-right m)))))))))
|
||||
|
||||
(define
|
||||
hk-map-from-list
|
||||
(fn
|
||||
(pairs)
|
||||
(reduce
|
||||
(fn (acc p) (hk-map-insert (first p) (nth p 1) acc))
|
||||
hk-map-empty
|
||||
pairs)))
|
||||
|
||||
(define
|
||||
hk-map-to-asc-list
|
||||
(fn
|
||||
(m)
|
||||
(cond
|
||||
((hk-map-empty? m) (list))
|
||||
(:else
|
||||
(append
|
||||
(hk-map-to-asc-list (hk-map-left m))
|
||||
(cons
|
||||
(list (hk-map-key m) (hk-map-val m))
|
||||
(hk-map-to-asc-list (hk-map-right m))))))))
|
||||
|
||||
(define hk-map-to-list hk-map-to-asc-list)
|
||||
|
||||
(define
|
||||
hk-map-keys
|
||||
(fn
|
||||
(m)
|
||||
(cond
|
||||
((hk-map-empty? m) (list))
|
||||
(:else
|
||||
(append
|
||||
(hk-map-keys (hk-map-left m))
|
||||
(cons (hk-map-key m) (hk-map-keys (hk-map-right m))))))))
|
||||
|
||||
(define
|
||||
hk-map-elems
|
||||
(fn
|
||||
(m)
|
||||
(cond
|
||||
((hk-map-empty? m) (list))
|
||||
(:else
|
||||
(append
|
||||
(hk-map-elems (hk-map-left m))
|
||||
(cons (hk-map-val m) (hk-map-elems (hk-map-right m))))))))
|
||||
|
||||
(define
|
||||
hk-map-union-with
|
||||
(fn
|
||||
(f m1 m2)
|
||||
(reduce
|
||||
(fn
|
||||
(acc p)
|
||||
(let
|
||||
((k (first p)) (v (nth p 1)))
|
||||
(let
|
||||
((look (hk-map-lookup k acc)))
|
||||
(cond
|
||||
((= (first look) "Just")
|
||||
(hk-map-insert k (f (nth look 1) v) acc))
|
||||
(:else (hk-map-insert k v acc))))))
|
||||
m1
|
||||
(hk-map-to-asc-list m2))))
|
||||
|
||||
(define
|
||||
hk-map-intersection-with
|
||||
(fn
|
||||
(f m1 m2)
|
||||
(reduce
|
||||
(fn
|
||||
(acc p)
|
||||
(let
|
||||
((k (first p)) (v1 (nth p 1)))
|
||||
(let
|
||||
((look (hk-map-lookup k m2)))
|
||||
(cond
|
||||
((= (first look) "Just")
|
||||
(hk-map-insert k (f v1 (nth look 1)) acc))
|
||||
(:else acc)))))
|
||||
hk-map-empty
|
||||
(hk-map-to-asc-list m1))))
|
||||
|
||||
(define
|
||||
hk-map-difference
|
||||
(fn
|
||||
(m1 m2)
|
||||
(reduce
|
||||
(fn
|
||||
(acc p)
|
||||
(let
|
||||
((k (first p)) (v (nth p 1)))
|
||||
(cond ((hk-map-member k m2) acc) (:else (hk-map-insert k v acc)))))
|
||||
hk-map-empty
|
||||
(hk-map-to-asc-list m1))))
|
||||
|
||||
(define
|
||||
hk-map-foldl-with-key
|
||||
(fn
|
||||
(f acc m)
|
||||
(cond
|
||||
((hk-map-empty? m) acc)
|
||||
(:else
|
||||
(let
|
||||
((acc1 (hk-map-foldl-with-key f acc (hk-map-left m))))
|
||||
(let
|
||||
((acc2 (f acc1 (hk-map-key m) (hk-map-val m))))
|
||||
(hk-map-foldl-with-key f acc2 (hk-map-right m))))))))
|
||||
|
||||
(define
|
||||
hk-map-foldr-with-key
|
||||
(fn
|
||||
(f acc m)
|
||||
(cond
|
||||
((hk-map-empty? m) acc)
|
||||
(:else
|
||||
(let
|
||||
((acc1 (hk-map-foldr-with-key f acc (hk-map-right m))))
|
||||
(let
|
||||
((acc2 (f (hk-map-key m) (hk-map-val m) acc1)))
|
||||
(hk-map-foldr-with-key f acc2 (hk-map-left m))))))))
|
||||
|
||||
(define
|
||||
hk-map-map-with-key
|
||||
(fn
|
||||
(f m)
|
||||
(cond
|
||||
((hk-map-empty? m) m)
|
||||
(:else
|
||||
(list
|
||||
"Map-Node"
|
||||
(hk-map-key m)
|
||||
(f (hk-map-key m) (hk-map-val m))
|
||||
(hk-map-map-with-key f (hk-map-left m))
|
||||
(hk-map-map-with-key f (hk-map-right m))
|
||||
(hk-map-size m))))))
|
||||
|
||||
(define
|
||||
hk-map-filter-with-key
|
||||
(fn
|
||||
(p m)
|
||||
(hk-map-foldr-with-key
|
||||
(fn (k v acc) (cond ((p k v) (hk-map-insert k v acc)) (:else acc)))
|
||||
hk-map-empty
|
||||
m)))
|
||||
|
||||
(define
|
||||
hk-map-adjust
|
||||
(fn
|
||||
(f k m)
|
||||
(cond
|
||||
((hk-map-empty? m) m)
|
||||
(:else
|
||||
(let
|
||||
((mk (hk-map-key m)))
|
||||
(cond
|
||||
((< k mk)
|
||||
(hk-map-node
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-adjust f k (hk-map-left m))
|
||||
(hk-map-right m)))
|
||||
((> k mk)
|
||||
(hk-map-node
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-left m)
|
||||
(hk-map-adjust f k (hk-map-right m))))
|
||||
(:else
|
||||
(hk-map-node
|
||||
mk
|
||||
(f (hk-map-val m))
|
||||
(hk-map-left m)
|
||||
(hk-map-right m)))))))))
|
||||
|
||||
(define
|
||||
hk-map-insert-with
|
||||
(fn
|
||||
(f k v m)
|
||||
(cond
|
||||
((hk-map-empty? m) (hk-map-singleton k v))
|
||||
(:else
|
||||
(let
|
||||
((mk (hk-map-key m)))
|
||||
(cond
|
||||
((< k mk)
|
||||
(hk-map-balance
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-insert-with f k v (hk-map-left m))
|
||||
(hk-map-right m)))
|
||||
((> k mk)
|
||||
(hk-map-balance
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-left m)
|
||||
(hk-map-insert-with f k v (hk-map-right m))))
|
||||
(:else
|
||||
(hk-map-node
|
||||
mk
|
||||
(f v (hk-map-val m))
|
||||
(hk-map-left m)
|
||||
(hk-map-right m)))))))))
|
||||
|
||||
(define
|
||||
hk-map-insert-with-key
|
||||
(fn
|
||||
(f k v m)
|
||||
(cond
|
||||
((hk-map-empty? m) (hk-map-singleton k v))
|
||||
(:else
|
||||
(let
|
||||
((mk (hk-map-key m)))
|
||||
(cond
|
||||
((< k mk)
|
||||
(hk-map-balance
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-insert-with-key f k v (hk-map-left m))
|
||||
(hk-map-right m)))
|
||||
((> k mk)
|
||||
(hk-map-balance
|
||||
mk
|
||||
(hk-map-val m)
|
||||
(hk-map-left m)
|
||||
(hk-map-insert-with-key f k v (hk-map-right m))))
|
||||
(:else
|
||||
(hk-map-node
|
||||
mk
|
||||
(f k v (hk-map-val m))
|
||||
(hk-map-left m)
|
||||
(hk-map-right m)))))))))
|
||||
|
||||
(define
|
||||
hk-map-alter
|
||||
(fn
|
||||
(f k m)
|
||||
(let
|
||||
((look (hk-map-lookup k m)))
|
||||
(let
|
||||
((res (f look)))
|
||||
(cond
|
||||
((= (first res) "Nothing") (hk-map-delete k m))
|
||||
(:else (hk-map-insert k (nth res 1) m)))))))
|
||||
@@ -87,45 +87,41 @@
|
||||
((nil? res) nil)
|
||||
(:else (assoc res (nth pat 1) val)))))
|
||||
(:else
|
||||
(let ((fv (hk-force val)))
|
||||
(let
|
||||
((fv (hk-force val)))
|
||||
(cond
|
||||
((= tag "p-int")
|
||||
(if
|
||||
(and (number? fv) (= fv (nth pat 1)))
|
||||
env
|
||||
nil))
|
||||
(if (and (number? fv) (= fv (nth pat 1))) env nil))
|
||||
((= tag "p-float")
|
||||
(if
|
||||
(and (number? fv) (= fv (nth pat 1)))
|
||||
env
|
||||
nil))
|
||||
(if (and (number? fv) (= fv (nth pat 1))) env nil))
|
||||
((= tag "p-string")
|
||||
(if
|
||||
(and (string? fv) (= fv (nth pat 1)))
|
||||
env
|
||||
nil))
|
||||
(if (and (string? fv) (= fv (nth pat 1))) env nil))
|
||||
((= tag "p-char")
|
||||
(if
|
||||
(and (string? fv) (= fv (nth pat 1)))
|
||||
env
|
||||
nil))
|
||||
(if (and (string? fv) (= fv (nth pat 1))) env nil))
|
||||
((= tag "p-con")
|
||||
(let
|
||||
((pat-name (nth pat 1)) (pat-args (nth pat 2)))
|
||||
(cond
|
||||
((and (= pat-name ":") (hk-str? fv) (not (hk-str-null? fv)))
|
||||
(let
|
||||
((str-head (hk-str-head fv))
|
||||
(str-tail (hk-str-tail fv)))
|
||||
(let
|
||||
((head-pat (nth pat-args 0))
|
||||
(tail-pat (nth pat-args 1)))
|
||||
(let
|
||||
((res (hk-match head-pat str-head env)))
|
||||
(cond
|
||||
((nil? res) nil)
|
||||
(:else (hk-match tail-pat str-tail res)))))))
|
||||
((not (hk-is-con-val? fv)) nil)
|
||||
((not (= (hk-val-con-name fv) pat-name)) nil)
|
||||
(:else
|
||||
(let
|
||||
((val-args (hk-val-con-args fv)))
|
||||
(cond
|
||||
((not (= (len pat-args) (len val-args)))
|
||||
nil)
|
||||
(:else
|
||||
(hk-match-all
|
||||
pat-args
|
||||
val-args
|
||||
env))))))))
|
||||
((not (= (len val-args) (len pat-args))) nil)
|
||||
(:else (hk-match-all pat-args val-args env))))))))
|
||||
((= tag "p-tuple")
|
||||
(let
|
||||
((items (nth pat 1)))
|
||||
@@ -134,13 +130,8 @@
|
||||
((not (= (hk-val-con-name fv) "Tuple")) nil)
|
||||
((not (= (len (hk-val-con-args fv)) (len items)))
|
||||
nil)
|
||||
(:else
|
||||
(hk-match-all
|
||||
items
|
||||
(hk-val-con-args fv)
|
||||
env)))))
|
||||
((= tag "p-list")
|
||||
(hk-match-list-pat (nth pat 1) fv env))
|
||||
(:else (hk-match-all items (hk-val-con-args fv) env)))))
|
||||
((= tag "p-list") (hk-match-list-pat (nth pat 1) fv env))
|
||||
(:else nil))))))))))
|
||||
|
||||
(define
|
||||
@@ -161,17 +152,26 @@
|
||||
hk-match-list-pat
|
||||
(fn
|
||||
(items val env)
|
||||
(let ((fv (hk-force val)))
|
||||
(let
|
||||
((fv (hk-force val)))
|
||||
(cond
|
||||
((empty? items)
|
||||
(if
|
||||
(and
|
||||
(hk-is-con-val? fv)
|
||||
(= (hk-val-con-name fv) "[]"))
|
||||
(or
|
||||
(and (hk-is-con-val? fv) (= (hk-val-con-name fv) "[]"))
|
||||
(and (hk-str? fv) (hk-str-null? fv)))
|
||||
env
|
||||
nil))
|
||||
(:else
|
||||
(cond
|
||||
((and (hk-str? fv) (not (hk-str-null? fv)))
|
||||
(let
|
||||
((h (hk-str-head fv)) (t (hk-str-tail fv)))
|
||||
(let
|
||||
((res (hk-match (first items) h env)))
|
||||
(cond
|
||||
((nil? res) nil)
|
||||
(:else (hk-match-list-pat (rest items) t res))))))
|
||||
((not (hk-is-con-val? fv)) nil)
|
||||
((not (= (hk-val-con-name fv) ":")) nil)
|
||||
(:else
|
||||
@@ -183,11 +183,7 @@
|
||||
((res (hk-match (first items) h env)))
|
||||
(cond
|
||||
((nil? res) nil)
|
||||
(:else
|
||||
(hk-match-list-pat
|
||||
(rest items)
|
||||
t
|
||||
res)))))))))))))
|
||||
(:else (hk-match-list-pat (rest items) t res)))))))))))))
|
||||
|
||||
;; ── Convenience: parse a pattern from source for tests ─────
|
||||
;; (Uses the parser's case-alt entry — `case _ of pat -> 0` —
|
||||
|
||||
@@ -208,9 +208,19 @@
|
||||
((= (get t "type") "char")
|
||||
(do (hk-advance!) (list :char (get t "value"))))
|
||||
((= (get t "type") "varid")
|
||||
(do (hk-advance!) (list :var (get t "value"))))
|
||||
(do
|
||||
(hk-advance!)
|
||||
(cond
|
||||
((hk-match? "lbrace" nil)
|
||||
(hk-parse-rec-update (list :var (get t "value"))))
|
||||
(:else (list :var (get t "value"))))))
|
||||
((= (get t "type") "conid")
|
||||
(do (hk-advance!) (list :con (get t "value"))))
|
||||
(do
|
||||
(hk-advance!)
|
||||
(cond
|
||||
((hk-match? "lbrace" nil)
|
||||
(hk-parse-rec-create (get t "value")))
|
||||
(:else (list :con (get t "value"))))))
|
||||
((= (get t "type") "qvarid")
|
||||
(do (hk-advance!) (list :var (get t "value"))))
|
||||
((= (get t "type") "qconid")
|
||||
@@ -456,6 +466,90 @@
|
||||
(do
|
||||
(hk-expect! "rbracket" nil)
|
||||
(list :list (list first-e))))))))))
|
||||
(define
|
||||
hk-parse-rec-create
|
||||
(fn
|
||||
(cname)
|
||||
(begin
|
||||
(hk-expect! "lbrace" nil)
|
||||
(let
|
||||
((fields (list)))
|
||||
(define
|
||||
hk-rc-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "varid" nil)
|
||||
(let
|
||||
((fname (get (hk-advance!) "value")))
|
||||
(begin
|
||||
(hk-expect! "reservedop" "=")
|
||||
(let
|
||||
((fexpr (hk-parse-expr-inner)))
|
||||
(begin
|
||||
(append! fields (list fname fexpr))
|
||||
(when
|
||||
(hk-match? "comma" nil)
|
||||
(begin (hk-advance!) (hk-rc-loop))))))))))
|
||||
(hk-rc-loop)
|
||||
(hk-expect! "rbrace" nil)
|
||||
(list :rec-create cname fields)))))
|
||||
(define
|
||||
hk-parse-rec-update
|
||||
(fn
|
||||
(rec-expr)
|
||||
(begin
|
||||
(hk-expect! "lbrace" nil)
|
||||
(let
|
||||
((fields (list)))
|
||||
(define
|
||||
hk-ru-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "varid" nil)
|
||||
(let
|
||||
((fname (get (hk-advance!) "value")))
|
||||
(begin
|
||||
(hk-expect! "reservedop" "=")
|
||||
(let
|
||||
((fexpr (hk-parse-expr-inner)))
|
||||
(begin
|
||||
(append! fields (list fname fexpr))
|
||||
(when
|
||||
(hk-match? "comma" nil)
|
||||
(begin (hk-advance!) (hk-ru-loop))))))))))
|
||||
(hk-ru-loop)
|
||||
(hk-expect! "rbrace" nil)
|
||||
(list :rec-update rec-expr fields)))))
|
||||
(define
|
||||
hk-parse-rec-pat
|
||||
(fn
|
||||
(cname)
|
||||
(begin
|
||||
(hk-expect! "lbrace" nil)
|
||||
(let
|
||||
((field-pats (list)))
|
||||
(define
|
||||
hk-rp-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "varid" nil)
|
||||
(let
|
||||
((fname (get (hk-advance!) "value")))
|
||||
(begin
|
||||
(hk-expect! "reservedop" "=")
|
||||
(let
|
||||
((fpat (hk-parse-pat)))
|
||||
(begin
|
||||
(append! field-pats (list fname fpat))
|
||||
(when
|
||||
(hk-match? "comma" nil)
|
||||
(begin (hk-advance!) (hk-rp-loop))))))))))
|
||||
(hk-rp-loop)
|
||||
(hk-expect! "rbrace" nil)
|
||||
(list :p-rec cname field-pats)))))
|
||||
(define
|
||||
hk-parse-fexp
|
||||
(fn
|
||||
@@ -696,7 +790,12 @@
|
||||
(:else
|
||||
(do (hk-advance!) (list :p-var (get t "value")))))))
|
||||
((= (get t "type") "conid")
|
||||
(do (hk-advance!) (list :p-con (get t "value") (list))))
|
||||
(do
|
||||
(hk-advance!)
|
||||
(cond
|
||||
((hk-match? "lbrace" nil)
|
||||
(hk-parse-rec-pat (get t "value")))
|
||||
(:else (list :p-con (get t "value") (list))))))
|
||||
((= (get t "type") "qconid")
|
||||
(do (hk-advance!) (list :p-con (get t "value") (list))))
|
||||
((= (get t "type") "lparen") (hk-parse-paren-pat))
|
||||
@@ -762,16 +861,24 @@
|
||||
(cond
|
||||
((and (not (nil? t)) (or (= (get t "type") "conid") (= (get t "type") "qconid")))
|
||||
(let
|
||||
((name (get (hk-advance!) "value")) (args (list)))
|
||||
(define
|
||||
hk-pca-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-apat-start? (hk-peek))
|
||||
(do (append! args (hk-parse-apat)) (hk-pca-loop)))))
|
||||
(hk-pca-loop)
|
||||
(list :p-con name args)))
|
||||
((name (get (hk-advance!) "value")))
|
||||
(cond
|
||||
((hk-match? "lbrace" nil)
|
||||
(hk-parse-rec-pat name))
|
||||
(:else
|
||||
(let
|
||||
((args (list)))
|
||||
(define
|
||||
hk-pca-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-apat-start? (hk-peek))
|
||||
(do
|
||||
(append! args (hk-parse-apat))
|
||||
(hk-pca-loop)))))
|
||||
(hk-pca-loop)
|
||||
(list :p-con name args))))))
|
||||
(:else (hk-parse-apat))))))
|
||||
(define
|
||||
hk-parse-pat
|
||||
@@ -1212,16 +1319,47 @@
|
||||
(not (hk-match? "conid" nil))
|
||||
(hk-err "expected constructor name"))
|
||||
(let
|
||||
((name (get (hk-advance!) "value")) (fields (list)))
|
||||
(define
|
||||
hk-cd-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-atype-start? (hk-peek))
|
||||
(do (append! fields (hk-parse-atype)) (hk-cd-loop)))))
|
||||
(hk-cd-loop)
|
||||
(list :con-def name fields))))
|
||||
((name (get (hk-advance!) "value")))
|
||||
(cond
|
||||
((hk-match? "lbrace" nil)
|
||||
(begin
|
||||
(hk-advance!)
|
||||
(let
|
||||
((rec-fields (list)))
|
||||
(define
|
||||
hk-rec-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-match? "varid" nil)
|
||||
(let
|
||||
((fname (get (hk-advance!) "value")))
|
||||
(begin
|
||||
(hk-expect! "reservedop" "::")
|
||||
(let
|
||||
((ftype (hk-parse-type)))
|
||||
(begin
|
||||
(append! rec-fields (list fname ftype))
|
||||
(when
|
||||
(hk-match? "comma" nil)
|
||||
(begin (hk-advance!) (hk-rec-loop))))))))))
|
||||
(hk-rec-loop)
|
||||
(hk-expect! "rbrace" nil)
|
||||
(list :con-rec name rec-fields))))
|
||||
(:else
|
||||
(let
|
||||
((fields (list)))
|
||||
(define
|
||||
hk-cd-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(hk-atype-start? (hk-peek))
|
||||
(begin
|
||||
(append! fields (hk-parse-atype))
|
||||
(hk-cd-loop)))))
|
||||
(hk-cd-loop)
|
||||
(list :con-def name fields)))))))
|
||||
(define
|
||||
hk-parse-tvars
|
||||
(fn
|
||||
|
||||
@@ -12,12 +12,7 @@
|
||||
|
||||
(define
|
||||
hk-register-con!
|
||||
(fn
|
||||
(cname arity type-name)
|
||||
(dict-set!
|
||||
hk-constructors
|
||||
cname
|
||||
{:arity arity :type type-name})))
|
||||
(fn (cname arity type-name) (dict-set! hk-constructors cname {:arity arity :type type-name})))
|
||||
|
||||
(define hk-is-con? (fn (name) (has-key? hk-constructors name)))
|
||||
|
||||
@@ -48,26 +43,15 @@
|
||||
(fn
|
||||
(data-node)
|
||||
(let
|
||||
((type-name (nth data-node 1))
|
||||
(cons-list (nth data-node 3)))
|
||||
((type-name (nth data-node 1)) (cons-list (nth data-node 3)))
|
||||
(for-each
|
||||
(fn
|
||||
(cd)
|
||||
(hk-register-con!
|
||||
(nth cd 1)
|
||||
(len (nth cd 2))
|
||||
type-name))
|
||||
(fn (cd) (hk-register-con! (nth cd 1) (len (nth cd 2)) type-name))
|
||||
cons-list))))
|
||||
|
||||
;; (:newtype NAME TVARS CNAME FIELD)
|
||||
(define
|
||||
hk-register-newtype!
|
||||
(fn
|
||||
(nt-node)
|
||||
(hk-register-con!
|
||||
(nth nt-node 3)
|
||||
1
|
||||
(nth nt-node 1))))
|
||||
(fn (nt-node) (hk-register-con! (nth nt-node 3) 1 (nth nt-node 1))))
|
||||
|
||||
;; Walk a decls list, registering every `data` / `newtype` decl.
|
||||
(define
|
||||
@@ -78,15 +62,9 @@
|
||||
(fn
|
||||
(d)
|
||||
(cond
|
||||
((and
|
||||
(list? d)
|
||||
(not (empty? d))
|
||||
(= (first d) "data"))
|
||||
((and (list? d) (not (empty? d)) (= (first d) "data"))
|
||||
(hk-register-data! d))
|
||||
((and
|
||||
(list? d)
|
||||
(not (empty? d))
|
||||
(= (first d) "newtype"))
|
||||
((and (list? d) (not (empty? d)) (= (first d) "newtype"))
|
||||
(hk-register-newtype! d))
|
||||
(:else nil)))
|
||||
decls)))
|
||||
@@ -99,16 +77,12 @@
|
||||
((nil? ast) nil)
|
||||
((not (list? ast)) nil)
|
||||
((empty? ast) nil)
|
||||
((= (first ast) "program")
|
||||
(hk-register-decls! (nth ast 1)))
|
||||
((= (first ast) "module")
|
||||
(hk-register-decls! (nth ast 4)))
|
||||
((= (first ast) "program") (hk-register-decls! (nth ast 1)))
|
||||
((= (first ast) "module") (hk-register-decls! (nth ast 4)))
|
||||
(:else nil))))
|
||||
|
||||
;; Convenience: source → AST → desugar → register.
|
||||
(define
|
||||
hk-load-source!
|
||||
(fn (src) (hk-register-program! (hk-core src))))
|
||||
(define hk-load-source! (fn (src) (hk-register-program! (hk-core src))))
|
||||
|
||||
;; ── Built-in constructors pre-registered ─────────────────────
|
||||
;; Bool — used implicitly by `if`, comparison operators.
|
||||
@@ -122,9 +96,55 @@
|
||||
;; Standard Prelude types — pre-registered so expression-level
|
||||
;; programs can use them without a `data` decl.
|
||||
(hk-register-con! "Nothing" 0 "Maybe")
|
||||
(hk-register-con! "Just" 1 "Maybe")
|
||||
(hk-register-con! "Left" 1 "Either")
|
||||
(hk-register-con! "Right" 1 "Either")
|
||||
(hk-register-con! "Just" 1 "Maybe")
|
||||
(hk-register-con! "Left" 1 "Either")
|
||||
(hk-register-con! "Right" 1 "Either")
|
||||
(hk-register-con! "LT" 0 "Ordering")
|
||||
(hk-register-con! "EQ" 0 "Ordering")
|
||||
(hk-register-con! "GT" 0 "Ordering")
|
||||
(hk-register-con! "SomeException" 1 "SomeException")
|
||||
|
||||
(define
|
||||
hk-str?
|
||||
(fn (v) (or (string? v) (and (dict? v) (has-key? v "hk-str")))))
|
||||
|
||||
(define
|
||||
hk-str-head
|
||||
(fn
|
||||
(v)
|
||||
(if
|
||||
(string? v)
|
||||
(char-code (char-at v 0))
|
||||
(char-code (char-at (get v "hk-str") (get v "hk-off"))))))
|
||||
|
||||
(define
|
||||
hk-str-tail
|
||||
(fn
|
||||
(v)
|
||||
(let
|
||||
((buf (if (string? v) v (get v "hk-str")))
|
||||
(off (if (string? v) 1 (+ (get v "hk-off") 1))))
|
||||
(if (>= off (string-length buf)) (list "[]") {:hk-off off :hk-str buf}))))
|
||||
|
||||
(define
|
||||
hk-str-null?
|
||||
(fn
|
||||
(v)
|
||||
(if
|
||||
(string? v)
|
||||
(= (string-length v) 0)
|
||||
(>= (get v "hk-off") (string-length (get v "hk-str"))))))
|
||||
|
||||
(define
|
||||
hk-str-to-native
|
||||
(fn
|
||||
(v)
|
||||
(if
|
||||
(string? v)
|
||||
v
|
||||
(let
|
||||
((buf (get v "hk-str")) (off (get v "hk-off")))
|
||||
(reduce
|
||||
(fn (acc i) (str acc (char-at buf i)))
|
||||
""
|
||||
(range off (string-length buf)))))))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"date": "2026-05-06",
|
||||
"total_pass": 156,
|
||||
"date": "2026-05-08",
|
||||
"total_pass": 285,
|
||||
"total_fail": 0,
|
||||
"programs": {
|
||||
"fib": {"pass": 2, "fail": 0},
|
||||
@@ -9,7 +9,7 @@
|
||||
"nqueens": {"pass": 2, "fail": 0},
|
||||
"calculator": {"pass": 5, "fail": 0},
|
||||
"collatz": {"pass": 11, "fail": 0},
|
||||
"palindrome": {"pass": 8, "fail": 0},
|
||||
"palindrome": {"pass": 12, "fail": 0},
|
||||
"maybe": {"pass": 12, "fail": 0},
|
||||
"fizzbuzz": {"pass": 12, "fail": 0},
|
||||
"anagram": {"pass": 9, "fail": 0},
|
||||
@@ -19,7 +19,25 @@
|
||||
"primes": {"pass": 12, "fail": 0},
|
||||
"zipwith": {"pass": 9, "fail": 0},
|
||||
"matrix": {"pass": 8, "fail": 0},
|
||||
"wordcount": {"pass": 7, "fail": 0},
|
||||
"powers": {"pass": 14, "fail": 0}
|
||||
"wordcount": {"pass": 10, "fail": 0},
|
||||
"powers": {"pass": 14, "fail": 0},
|
||||
"caesar": {"pass": 8, "fail": 0},
|
||||
"runlength-str": {"pass": 9, "fail": 0},
|
||||
"showadt": {"pass": 5, "fail": 0},
|
||||
"showio": {"pass": 5, "fail": 0},
|
||||
"partial": {"pass": 7, "fail": 0},
|
||||
"statistics": {"pass": 5, "fail": 0},
|
||||
"newton": {"pass": 5, "fail": 0},
|
||||
"wordfreq": {"pass": 7, "fail": 0},
|
||||
"mapgraph": {"pass": 6, "fail": 0},
|
||||
"uniquewords": {"pass": 4, "fail": 0},
|
||||
"setops": {"pass": 8, "fail": 0},
|
||||
"shapes": {"pass": 5, "fail": 0},
|
||||
"person": {"pass": 7, "fail": 0},
|
||||
"config": {"pass": 10, "fail": 0},
|
||||
"counter": {"pass": 7, "fail": 0},
|
||||
"accumulate": {"pass": 8, "fail": 0},
|
||||
"safediv": {"pass": 8, "fail": 0},
|
||||
"trycatch": {"pass": 8, "fail": 0}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Haskell-on-SX Scoreboard
|
||||
|
||||
Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs)
|
||||
Updated 2026-05-08 · Phase 6 (prelude extras + 18 programs)
|
||||
|
||||
| Program | Tests | Status |
|
||||
|---------|-------|--------|
|
||||
@@ -10,7 +10,7 @@ Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs)
|
||||
| nqueens.hs | 2/2 | ✓ |
|
||||
| calculator.hs | 5/5 | ✓ |
|
||||
| collatz.hs | 11/11 | ✓ |
|
||||
| palindrome.hs | 8/8 | ✓ |
|
||||
| palindrome.hs | 12/12 | ✓ |
|
||||
| maybe.hs | 12/12 | ✓ |
|
||||
| fizzbuzz.hs | 12/12 | ✓ |
|
||||
| anagram.hs | 9/9 | ✓ |
|
||||
@@ -20,6 +20,24 @@ Updated 2026-05-06 · Phase 6 (prelude extras + 18 programs)
|
||||
| primes.hs | 12/12 | ✓ |
|
||||
| zipwith.hs | 9/9 | ✓ |
|
||||
| matrix.hs | 8/8 | ✓ |
|
||||
| wordcount.hs | 7/7 | ✓ |
|
||||
| wordcount.hs | 10/10 | ✓ |
|
||||
| powers.hs | 14/14 | ✓ |
|
||||
| **Total** | **156/156** | **18/18 programs** |
|
||||
| caesar.hs | 8/8 | ✓ |
|
||||
| runlength-str.hs | 9/9 | ✓ |
|
||||
| showadt.hs | 5/5 | ✓ |
|
||||
| showio.hs | 5/5 | ✓ |
|
||||
| partial.hs | 7/7 | ✓ |
|
||||
| statistics.hs | 5/5 | ✓ |
|
||||
| newton.hs | 5/5 | ✓ |
|
||||
| wordfreq.hs | 7/7 | ✓ |
|
||||
| mapgraph.hs | 6/6 | ✓ |
|
||||
| uniquewords.hs | 4/4 | ✓ |
|
||||
| setops.hs | 8/8 | ✓ |
|
||||
| shapes.hs | 5/5 | ✓ |
|
||||
| person.hs | 7/7 | ✓ |
|
||||
| config.hs | 10/10 | ✓ |
|
||||
| counter.hs | 7/7 | ✓ |
|
||||
| accumulate.hs | 8/8 | ✓ |
|
||||
| safediv.hs | 8/8 | ✓ |
|
||||
| trycatch.hs | 8/8 | ✓ |
|
||||
| **Total** | **285/285** | **36/36 programs** |
|
||||
|
||||
62
lib/haskell/set.sx
Normal file
62
lib/haskell/set.sx
Normal file
@@ -0,0 +1,62 @@
|
||||
;; set.sx — Phase 12 Data.Set: wraps Data.Map with unit values.
|
||||
;;
|
||||
;; A Set is a Map from key to (). All set operations delegate to the map
|
||||
;; ops, ignoring the value side. Storage representation matches Data.Map:
|
||||
;;
|
||||
;; Empty → ("Map-Empty")
|
||||
;; Node → ("Map-Node" key () left right size)
|
||||
;;
|
||||
;; Tradeoff: trivial maintenance burden, slight overhead per node from
|
||||
;; the unused value slot. Faster path forward than re-implementing the
|
||||
;; weight-balanced BST.
|
||||
;;
|
||||
;; Functions live in this file; the Haskell-level `import Data.Set` /
|
||||
;; `import qualified Data.Set as Set` wiring (next Phase 12 box) binds
|
||||
;; them under the chosen alias.
|
||||
|
||||
(define hk-set-unit (list "Tuple"))
|
||||
|
||||
(define hk-set-empty hk-map-empty)
|
||||
|
||||
(define hk-set-singleton (fn (k) (hk-map-singleton k hk-set-unit)))
|
||||
|
||||
(define hk-set-insert (fn (k s) (hk-map-insert k hk-set-unit s)))
|
||||
|
||||
(define hk-set-delete hk-map-delete)
|
||||
(define hk-set-member hk-map-member)
|
||||
(define hk-set-size hk-map-size)
|
||||
(define hk-set-null hk-map-null)
|
||||
(define hk-set-to-asc-list hk-map-keys)
|
||||
(define hk-set-to-list hk-map-keys)
|
||||
|
||||
(define
|
||||
hk-set-from-list
|
||||
(fn (xs) (reduce (fn (acc k) (hk-set-insert k acc)) hk-set-empty xs)))
|
||||
|
||||
(define
|
||||
hk-set-union
|
||||
(fn (a b) (hk-map-union-with (fn (x y) hk-set-unit) a b)))
|
||||
|
||||
(define
|
||||
hk-set-intersection
|
||||
(fn (a b) (hk-map-intersection-with (fn (x y) hk-set-unit) a b)))
|
||||
|
||||
(define hk-set-difference hk-map-difference)
|
||||
|
||||
(define
|
||||
hk-set-is-subset-of
|
||||
(fn (a b) (= (hk-map-size (hk-map-difference a b)) 0)))
|
||||
|
||||
(define
|
||||
hk-set-filter
|
||||
(fn (p s) (hk-map-filter-with-key (fn (k v) (p k)) s)))
|
||||
|
||||
(define hk-set-map (fn (f s) (hk-set-from-list (map f (hk-map-keys s)))))
|
||||
|
||||
(define
|
||||
hk-set-foldr
|
||||
(fn (f z s) (hk-map-foldr-with-key (fn (k v acc) (f k acc)) z s)))
|
||||
|
||||
(define
|
||||
hk-set-foldl
|
||||
(fn (f z s) (hk-map-foldl-with-key (fn (acc k v) (f acc k)) z s)))
|
||||
@@ -55,6 +55,8 @@ for FILE in "${FILES[@]}"; do
|
||||
(load "lib/haskell/runtime.sx")
|
||||
(load "lib/haskell/match.sx")
|
||||
(load "lib/haskell/eval.sx")
|
||||
(load "lib/haskell/map.sx")
|
||||
(load "lib/haskell/set.sx")
|
||||
$INFER_LOAD
|
||||
(load "lib/haskell/testlib.sx")
|
||||
(epoch 2)
|
||||
@@ -98,6 +100,8 @@ EPOCHS
|
||||
(load "lib/haskell/runtime.sx")
|
||||
(load "lib/haskell/match.sx")
|
||||
(load "lib/haskell/eval.sx")
|
||||
(load "lib/haskell/map.sx")
|
||||
(load "lib/haskell/set.sx")
|
||||
$INFER_LOAD
|
||||
(load "lib/haskell/testlib.sx")
|
||||
(epoch 2)
|
||||
|
||||
@@ -56,3 +56,21 @@
|
||||
(append!
|
||||
hk-test-fails
|
||||
{:actual actual :expected expected :name name})))))
|
||||
|
||||
(define
|
||||
hk-test-error
|
||||
(fn
|
||||
(name thunk expected-substring)
|
||||
(let
|
||||
((caught (guard (e (true (if (string? e) e (str e)))) (begin (thunk) nil))))
|
||||
(cond
|
||||
((nil? caught)
|
||||
(do
|
||||
(set! hk-test-fail (+ hk-test-fail 1))
|
||||
(append! hk-test-fails {:actual "no error raised" :expected (str "error containing: " expected-substring) :name name})))
|
||||
((>= (index-of caught expected-substring) 0)
|
||||
(set! hk-test-pass (+ hk-test-pass 1)))
|
||||
(:else
|
||||
(do
|
||||
(set! hk-test-fail (+ hk-test-fail 1))
|
||||
(append! hk-test-fails {:actual caught :expected (str "error containing: " expected-substring) :name name})))))))
|
||||
|
||||
86
lib/haskell/tests/class-defaults.sx
Normal file
86
lib/haskell/tests/class-defaults.sx
Normal file
@@ -0,0 +1,86 @@
|
||||
;; class-defaults.sx — Phase 13: class default method implementations.
|
||||
|
||||
;; ── Eq default: myNeq derived from myEq via `not (myEq x y)` ──
|
||||
(define
|
||||
hk-myeq-source
|
||||
"class MyEq a where\n myEq :: a -> a -> Bool\n myNeq :: a -> a -> Bool\n myNeq x y = not (myEq x y)\ninstance MyEq Int where\n myEq x y = x == y\n")
|
||||
|
||||
(hk-test
|
||||
"Eq default: myNeq 3 5 = True (no explicit myNeq in instance)"
|
||||
(hk-deep-force (hk-run (str hk-myeq-source "main = myNeq 3 5\n")))
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"Eq default: myNeq 3 3 = False"
|
||||
(hk-deep-force (hk-run (str hk-myeq-source "main = myNeq 3 3\n")))
|
||||
(list "False"))
|
||||
|
||||
(hk-test
|
||||
"Eq default: myEq still works in same instance"
|
||||
(hk-deep-force (hk-run (str hk-myeq-source "main = myEq 7 7\n")))
|
||||
(list "True"))
|
||||
|
||||
;; ── Override path: instance can still provide the method explicitly. ──
|
||||
(hk-test
|
||||
"Default override: instance-provided beats class default"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"class Hi a where\n greet :: a -> String\n greet x = \"default\"\ninstance Hi Bool where\n greet x = \"override\"\nmain = greet True"))
|
||||
"override")
|
||||
|
||||
(hk-test
|
||||
"Default fallback: empty instance picks default"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"class Hi a where\n greet :: a -> String\n greet x = \"default\"\ninstance Hi Bool where\nmain = greet True"))
|
||||
"default")
|
||||
|
||||
(define
|
||||
hk-myord-source
|
||||
"class MyOrd a where\n myCmp :: a -> a -> Bool\n myMax :: a -> a -> a\n myMin :: a -> a -> a\n myMax a b = if myCmp a b then a else b\n myMin a b = if myCmp a b then b else a\ninstance MyOrd Int where\n myCmp x y = x >= y\n")
|
||||
|
||||
(hk-test
|
||||
"Ord default: myMax 3 5 = 5"
|
||||
(hk-deep-force (hk-run (str hk-myord-source "main = myMax 3 5\n")))
|
||||
5)
|
||||
|
||||
(hk-test
|
||||
"Ord default: myMax 8 2 = 8"
|
||||
(hk-deep-force (hk-run (str hk-myord-source "main = myMax 8 2\n")))
|
||||
8)
|
||||
|
||||
(hk-test
|
||||
"Ord default: myMin 3 5 = 3"
|
||||
(hk-deep-force (hk-run (str hk-myord-source "main = myMin 3 5\n")))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"Ord default: myMin 8 2 = 2"
|
||||
(hk-deep-force (hk-run (str hk-myord-source "main = myMin 8 2\n")))
|
||||
2)
|
||||
|
||||
(hk-test
|
||||
"Ord default: myMax of equals returns first"
|
||||
(hk-deep-force (hk-run (str hk-myord-source "main = myMax 4 4\n")))
|
||||
4)
|
||||
|
||||
(define
|
||||
hk-mynum-source
|
||||
"class MyNum a where\n mySub :: a -> a -> a\n myLt :: a -> a -> Bool\n myNegate :: a -> a\n myAbs :: a -> a\n myNegate x = mySub (mySub x x) x\n myAbs x = if myLt x (mySub x x) then myNegate x else x\ninstance MyNum Int where\n mySub x y = x - y\n myLt x y = x < y\n")
|
||||
|
||||
(hk-test
|
||||
"Num default: myNegate 5 = -5"
|
||||
(hk-deep-force (hk-run (str hk-mynum-source "main = myNegate 5\n")))
|
||||
-5)
|
||||
|
||||
(hk-test
|
||||
"Num default: myAbs (myNegate 7) = 7"
|
||||
(hk-deep-force (hk-run (str hk-mynum-source "main = myAbs (myNegate 7)\n")))
|
||||
7)
|
||||
|
||||
(hk-test
|
||||
"Num default: myAbs 9 = 9"
|
||||
(hk-deep-force (hk-run (str hk-mynum-source "main = myAbs 9\n")))
|
||||
9)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
@@ -12,14 +12,14 @@
|
||||
"deriving Show: constructor with arg"
|
||||
(hk-deep-force
|
||||
(hk-run "data Wrapper = Wrap Int deriving (Show)\nmain = show (Wrap 42)"))
|
||||
"(Wrap 42)")
|
||||
"Wrap 42")
|
||||
|
||||
(hk-test
|
||||
"deriving Show: nested constructors"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Tree = Leaf | Node Int Tree Tree deriving (Show)\nmain = show (Node 1 Leaf Leaf)"))
|
||||
"(Node 1 Leaf Leaf)")
|
||||
"Node 1 Leaf Leaf")
|
||||
|
||||
(hk-test
|
||||
"deriving Show: second constructor"
|
||||
@@ -30,6 +30,31 @@
|
||||
|
||||
;; ─── Eq ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
(hk-test
|
||||
"deriving Show: nested ADT wraps inner constructor in parens"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Tree = Leaf | Node Int Tree Tree deriving (Show)\nmain = show (Node 1 Leaf (Node 2 Leaf Leaf))"))
|
||||
"Node 1 Leaf (Node 2 Leaf Leaf)")
|
||||
|
||||
(hk-test
|
||||
"deriving Show: Maybe Maybe wraps inner Just"
|
||||
(hk-deep-force (hk-run "main = show (Just (Just 3))"))
|
||||
"Just (Just 3)")
|
||||
|
||||
(hk-test
|
||||
"deriving Show: negative argument wrapped in parens"
|
||||
(hk-deep-force (hk-run "main = show (Just (negate 3))"))
|
||||
"Just (-3)")
|
||||
|
||||
(hk-test
|
||||
"deriving Show: list element does not need parens"
|
||||
(hk-deep-force
|
||||
(hk-run "data Box = Box [Int] deriving (Show)\nmain = show (Box [1,2,3])"))
|
||||
"Box [1,2,3]")
|
||||
|
||||
;; ─── combined Eq + Show ───────────────────────────────────────────────────────
|
||||
|
||||
(hk-test
|
||||
"deriving Eq: same constructor"
|
||||
(hk-deep-force
|
||||
@@ -58,14 +83,12 @@
|
||||
"data Color = Red | Green | Blue deriving (Eq)\nmain = show (Red /= Blue)"))
|
||||
"True")
|
||||
|
||||
;; ─── combined Eq + Show ───────────────────────────────────────────────────────
|
||||
|
||||
(hk-test
|
||||
"deriving Eq Show: combined in parens"
|
||||
"deriving Eq Show: combined"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Shape = Circle Int | Square Int deriving (Eq, Show)\nmain = show (Circle 5)"))
|
||||
"(Circle 5)")
|
||||
"Circle 5")
|
||||
|
||||
(hk-test
|
||||
"deriving Eq Show: eq on constructor with arg"
|
||||
|
||||
99
lib/haskell/tests/errors.sx
Normal file
99
lib/haskell/tests/errors.sx
Normal file
@@ -0,0 +1,99 @@
|
||||
;; errors.sx — Phase 9 error / undefined / partial-fn coverage via hk-test-error.
|
||||
|
||||
;; ── error builtin ────────────────────────────────────────────
|
||||
(define
|
||||
hk-as-list
|
||||
(fn
|
||||
(xs)
|
||||
(cond
|
||||
((and (list? xs) (= (first xs) "[]")) (list))
|
||||
((and (list? xs) (= (first xs) ":"))
|
||||
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||
(:else xs))))
|
||||
|
||||
(hk-test-error
|
||||
"error: raises with literal message"
|
||||
(fn () (hk-deep-force (hk-run "main = error \"boom\"")))
|
||||
"hk-error: boom")
|
||||
|
||||
(hk-test-error
|
||||
"error: raises with computed message"
|
||||
(fn () (hk-deep-force (hk-run "main = error (\"oops: \" ++ show 42)")))
|
||||
"hk-error: oops: 42")
|
||||
|
||||
;; ── undefined ────────────────────────────────────────────────
|
||||
(hk-test-error
|
||||
"error: nested in if branch (only fires when forced)"
|
||||
(fn
|
||||
()
|
||||
(hk-deep-force (hk-run "main = if 1 == 1 then error \"taken\" else 0")))
|
||||
"taken")
|
||||
|
||||
(hk-test-error
|
||||
"undefined: raises Prelude.undefined"
|
||||
(fn () (hk-deep-force (hk-run "main = undefined")))
|
||||
"Prelude.undefined")
|
||||
|
||||
;; The non-strict path: undefined doesn't fire when not forced.
|
||||
(hk-test-error
|
||||
"undefined: forced via arithmetic"
|
||||
(fn () (hk-deep-force (hk-run "main = undefined + 1")))
|
||||
"Prelude.undefined")
|
||||
|
||||
;; ── partial functions ───────────────────────────────────────
|
||||
(hk-test
|
||||
"undefined: lazy, not forced when discarded"
|
||||
(hk-deep-force (hk-run "main = let _ = undefined in 5"))
|
||||
5)
|
||||
|
||||
(hk-test-error
|
||||
"head []: raises Prelude.head: empty list"
|
||||
(fn () (hk-deep-force (hk-run "main = head []")))
|
||||
"Prelude.head: empty list")
|
||||
|
||||
(hk-test-error
|
||||
"tail []: raises Prelude.tail: empty list"
|
||||
(fn () (hk-deep-force (hk-run "main = tail []")))
|
||||
"Prelude.tail: empty list")
|
||||
|
||||
;; head and tail still work on non-empty lists.
|
||||
(hk-test-error
|
||||
"fromJust Nothing: raises Maybe.fromJust: Nothing"
|
||||
(fn () (hk-deep-force (hk-run "main = fromJust Nothing")))
|
||||
"Maybe.fromJust: Nothing")
|
||||
|
||||
(hk-test
|
||||
"head [42]: still works"
|
||||
(hk-deep-force (hk-run "main = head [42]"))
|
||||
42)
|
||||
|
||||
;; ── error in IO context ─────────────────────────────────────
|
||||
(hk-test
|
||||
"tail [1,2,3]: still works"
|
||||
(hk-as-list (hk-deep-force (hk-run "main = tail [1,2,3]")))
|
||||
(list 2 3))
|
||||
|
||||
(hk-test
|
||||
"hk-run-io: error in main lands in io-lines"
|
||||
(let
|
||||
((lines (hk-run-io "main = error \"caught here\"")))
|
||||
(>= (index-of (str lines) "caught here") 0))
|
||||
true)
|
||||
|
||||
;; ── hk-test-error helper itself ─────────────────────────────
|
||||
(hk-test
|
||||
"hk-run-io: putStrLn before error preserves earlier output"
|
||||
(let
|
||||
((lines (hk-run-io "main = do { putStrLn \"first\"; error \"died\"; putStrLn \"never\" }")))
|
||||
(and
|
||||
(>= (index-of (str lines) "first") 0)
|
||||
(>= (index-of (str lines) "died") 0)))
|
||||
true)
|
||||
|
||||
;; hk-as-list helper for converting a forced Haskell cons into an SX list.
|
||||
(hk-test-error
|
||||
"hk-test-error: matches partial substring inside wrapped exception"
|
||||
(fn () (hk-deep-force (hk-run "main = error \"unique-marker-xyz\"")))
|
||||
"unique-marker-xyz")
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
@@ -231,16 +231,82 @@
|
||||
1)
|
||||
|
||||
;; ── Laziness: app args evaluate only when forced ──
|
||||
(hk-test
|
||||
"error builtin: raises with hk-error prefix"
|
||||
(guard
|
||||
(e (true (>= (index-of e "hk-error: boom") 0)))
|
||||
(begin (hk-deep-force (hk-run "main = error \"boom\"")) false))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"error builtin: raises with computed message"
|
||||
(guard
|
||||
(e (true (>= (index-of e "hk-error: oops: 42") 0)))
|
||||
(begin
|
||||
(hk-deep-force (hk-run "main = error (\"oops: \" ++ show 42)"))
|
||||
false))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"undefined: raises hk-error with Prelude.undefined message"
|
||||
(guard
|
||||
(e (true (>= (index-of e "hk-error: Prelude.undefined") 0)))
|
||||
(begin (hk-deep-force (hk-run "main = undefined")) false))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"undefined: lazy — only fires when forced"
|
||||
(hk-deep-force (hk-run "main = if True then 42 else undefined"))
|
||||
42)
|
||||
|
||||
(hk-test
|
||||
"head []: raises Prelude.head: empty list"
|
||||
(guard
|
||||
(e (true (>= (index-of e "Prelude.head: empty list") 0)))
|
||||
(begin (hk-deep-force (hk-run "main = head []")) false))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"tail []: raises Prelude.tail: empty list"
|
||||
(guard
|
||||
(e (true (>= (index-of e "Prelude.tail: empty list") 0)))
|
||||
(begin (hk-deep-force (hk-run "main = tail []")) false))
|
||||
true)
|
||||
|
||||
;; ── not / id built-ins ──
|
||||
(hk-test
|
||||
"fromJust Nothing: raises Maybe.fromJust: Nothing"
|
||||
(guard
|
||||
(e (true (>= (index-of e "Maybe.fromJust: Nothing") 0)))
|
||||
(begin (hk-deep-force (hk-run "main = fromJust Nothing")) false))
|
||||
true)
|
||||
(hk-test
|
||||
"fromJust (Just 5) = 5"
|
||||
(hk-deep-force (hk-run "main = fromJust (Just 5)"))
|
||||
5)
|
||||
(hk-test
|
||||
"head [42] = 42 (still works for non-empty)"
|
||||
(hk-deep-force (hk-run "main = head [42]"))
|
||||
42)
|
||||
|
||||
(hk-test-error
|
||||
"hk-test-error helper: catches matching error"
|
||||
(fn () (hk-deep-force (hk-run "main = error \"boom\"")))
|
||||
"hk-error: boom")
|
||||
|
||||
(hk-test-error
|
||||
"hk-test-error helper: catches head [] error"
|
||||
(fn () (hk-deep-force (hk-run "main = head []")))
|
||||
"Prelude.head: empty list")
|
||||
|
||||
(hk-test
|
||||
"second arg never forced"
|
||||
(hk-eval-expr-source
|
||||
"(\\x y -> x) 1 (error \"never\")")
|
||||
(hk-eval-expr-source "(\\x y -> x) 1 (error \"never\")")
|
||||
1)
|
||||
|
||||
(hk-test
|
||||
"first arg never forced"
|
||||
(hk-eval-expr-source
|
||||
"(\\x y -> y) (error \"never\") 99")
|
||||
(hk-eval-expr-source "(\\x y -> y) (error \"never\") 99")
|
||||
99)
|
||||
|
||||
(hk-test
|
||||
@@ -251,9 +317,7 @@
|
||||
|
||||
(hk-test
|
||||
"lazy: const drops its second argument"
|
||||
(hk-prog-val
|
||||
"const x y = x\nresult = const 5 (error \"boom\")"
|
||||
"result")
|
||||
(hk-prog-val "const x y = x\nresult = const 5 (error \"boom\")" "result")
|
||||
5)
|
||||
|
||||
(hk-test
|
||||
@@ -270,9 +334,10 @@
|
||||
"result")
|
||||
(list "True"))
|
||||
|
||||
;; ── not / id built-ins ──
|
||||
(hk-test "not True" (hk-eval-expr-source "not True") (list "False"))
|
||||
|
||||
(hk-test "not False" (hk-eval-expr-source "not False") (list "True"))
|
||||
|
||||
(hk-test "id" (hk-eval-expr-source "id 42") 42)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
|
||||
105
lib/haskell/tests/exceptions.sx
Normal file
105
lib/haskell/tests/exceptions.sx
Normal file
@@ -0,0 +1,105 @@
|
||||
;; Phase 16 — Exception handling unit tests.
|
||||
|
||||
(hk-test
|
||||
"catch — success path returns the action result"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"main = catch (return 42) (\\(SomeException m) -> return 0)"))
|
||||
(list "IO" 42))
|
||||
|
||||
(hk-test
|
||||
"catch — error caught, handler receives message"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"main = catch (error \"boom\") (\\(SomeException m) -> return m)"))
|
||||
(list "IO" "boom"))
|
||||
|
||||
(hk-test
|
||||
"try — success returns Right v"
|
||||
(hk-deep-force
|
||||
(hk-run "main = try (return 42)"))
|
||||
(list "IO" (list "Right" 42)))
|
||||
|
||||
(hk-test
|
||||
"try — error returns Left (SomeException msg)"
|
||||
(hk-deep-force
|
||||
(hk-run "main = try (error \"oops\")"))
|
||||
(list "IO" (list "Left" (list "SomeException" "oops"))))
|
||||
|
||||
(hk-test
|
||||
"handle — flip catch — caught error message"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"main = handle (\\(SomeException m) -> return m) (error \"hot\")"))
|
||||
(list "IO" "hot"))
|
||||
|
||||
(hk-test
|
||||
"throwIO + catch — handler sees the SomeException"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"main = catch (throwIO (SomeException \"bang\")) (\\(SomeException m) -> return m)"))
|
||||
(list "IO" "bang"))
|
||||
|
||||
(hk-test
|
||||
"throwIO + try — Left side"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"main = try (throwIO (SomeException \"x\"))"))
|
||||
(list "IO" (list "Left" (list "SomeException" "x"))))
|
||||
|
||||
(hk-test
|
||||
"evaluate — pure value returns IO v"
|
||||
(hk-deep-force
|
||||
(hk-run "main = evaluate (1 + 2 + 3)"))
|
||||
(list "IO" 6))
|
||||
|
||||
(hk-test
|
||||
"evaluate — error surfaces as catchable exception"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"main = catch (evaluate (error \"deep\")) (\\(SomeException m) -> return m)"))
|
||||
(list "IO" "deep"))
|
||||
|
||||
(hk-test
|
||||
"nested catch — inner handler runs first"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"main = catch (catch (error \"inner\") (\\(SomeException m) -> error (m ++ \"-rethrown\"))) (\\(SomeException m) -> return m)"))
|
||||
(list "IO" "inner-rethrown"))
|
||||
|
||||
(hk-test
|
||||
"catch chain — handler can succeed inside IO"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"main = do { x <- catch (error \"e1\") (\\(SomeException m) -> return 100); return (x + 1) }"))
|
||||
(list "IO" 101))
|
||||
|
||||
(hk-test
|
||||
"try then bind on Right"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"branch (Right v) = return (v * 2)
|
||||
branch (Left _) = return 0
|
||||
main = do { r <- try (return 21); branch r }"))
|
||||
(list "IO" 42))
|
||||
|
||||
(hk-test
|
||||
"try then bind on Left"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"branch (Right _) = return \"ok\"
|
||||
branch (Left (SomeException m)) = return m
|
||||
main = do { r <- try (error \"failed\"); branch r }"))
|
||||
(list "IO" "failed"))
|
||||
|
||||
(hk-test
|
||||
"catch — handler can use closed-over IORef"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef
|
||||
main = do
|
||||
r <- IORef.newIORef 0
|
||||
catch (error \"x\") (\\(SomeException m) -> IORef.writeIORef r 7)
|
||||
v <- IORef.readIORef r
|
||||
return v"))
|
||||
(list "IO" 7))
|
||||
31
lib/haskell/tests/instance-where.sx
Normal file
31
lib/haskell/tests/instance-where.sx
Normal file
@@ -0,0 +1,31 @@
|
||||
;; instance-where.sx — Phase 13: where-clauses inside instance bodies.
|
||||
|
||||
(hk-test
|
||||
"instance method body with where-helper (Bool)"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"class Greet a where\n greet :: a -> String\ninstance Greet Bool where\n greet x = mkMsg x\n where mkMsg True = \"yes\"\n mkMsg False = \"no\"\nmain = greet True"))
|
||||
"yes")
|
||||
|
||||
(hk-test
|
||||
"instance method body with where-helper (False branch)"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"class Greet a where\n greet :: a -> String\ninstance Greet Bool where\n greet x = mkMsg x\n where mkMsg True = \"yes\"\n mkMsg False = \"no\"\nmain = greet False"))
|
||||
"no")
|
||||
|
||||
(hk-test
|
||||
"instance method body with where-binding referenced multiple times"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"class Twice a where\n twice :: a -> Int\ninstance Twice Int where\n twice x = h + h\n where h = x + 1\nmain = twice 5"))
|
||||
12)
|
||||
|
||||
(hk-test
|
||||
"instance method body with multi-binding where"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"class Calc a where\n calc :: a -> Int\ninstance Calc Int where\n calc x = a + b\n where a = x * 2\n b = x + 1\nmain = calc 3"))
|
||||
10)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
@@ -64,12 +64,11 @@
|
||||
|
||||
(hk-test
|
||||
"readFile error on missing file"
|
||||
(guard
|
||||
(e (true (>= (index-of e "file not found") 0)))
|
||||
(begin
|
||||
(set! hk-vfs (dict))
|
||||
(hk-run-io "main = readFile \"no.txt\" >>= putStrLn")
|
||||
false))
|
||||
(begin
|
||||
(set! hk-vfs (dict))
|
||||
(let
|
||||
((lines (hk-run-io "main = readFile \"no.txt\" >>= putStrLn")))
|
||||
(>= (index-of (str lines) "file not found") 0)))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
|
||||
94
lib/haskell/tests/ioref.sx
Normal file
94
lib/haskell/tests/ioref.sx
Normal file
@@ -0,0 +1,94 @@
|
||||
;; Phase 15 — IORef unit tests.
|
||||
|
||||
(hk-test
|
||||
"newIORef + readIORef returns initial value"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 42; v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" 42))
|
||||
|
||||
(hk-test
|
||||
"writeIORef updates the cell"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 0; IORef.writeIORef r 99; v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" 99))
|
||||
|
||||
(hk-test
|
||||
"writeIORef returns IO ()"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 0; IORef.writeIORef r 1 }"))
|
||||
(list "IO" (list "Tuple")))
|
||||
|
||||
(hk-test
|
||||
"modifyIORef applies a function"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 5; IORef.modifyIORef r (\\x -> x * 2); v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" 10))
|
||||
|
||||
(hk-test
|
||||
"modifyIORef' (strict) applies a function"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 7; IORef.modifyIORef' r (\\x -> x + 3); v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" 10))
|
||||
|
||||
(hk-test
|
||||
"two reads return the same value"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 11; a <- IORef.readIORef r; b <- IORef.readIORef r; return (a + b) }"))
|
||||
(list "IO" 22))
|
||||
|
||||
(hk-test
|
||||
"shared ref across do-steps: write then read"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef 1; IORef.writeIORef r 2; IORef.writeIORef r 3; v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" 3))
|
||||
|
||||
(hk-test
|
||||
"two refs are independent"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r1 <- IORef.newIORef 1; r2 <- IORef.newIORef 2; IORef.writeIORef r1 10; a <- IORef.readIORef r1; b <- IORef.readIORef r2; return (a + b) }"))
|
||||
(list "IO" 12))
|
||||
|
||||
(hk-test
|
||||
"string-valued IORef"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef \"hi\"; IORef.writeIORef r \"bye\"; v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" "bye"))
|
||||
|
||||
(hk-test
|
||||
"list-valued IORef + cons"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nmain = do { r <- IORef.newIORef [1,2,3]; IORef.modifyIORef r (\\xs -> 0 : xs); v <- IORef.readIORef r; return v }"))
|
||||
(list
|
||||
"IO"
|
||||
(list ":" 0 (list ":" 1 (list ":" 2 (list ":" 3 (list "[]")))))))
|
||||
|
||||
(hk-test
|
||||
"counter loop: increment N times"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nloop r 0 = return ()\nloop r n = do { IORef.modifyIORef r (\\x -> x + 1); loop r (n - 1) }\nmain = do { r <- IORef.newIORef 0; loop r 10; v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" 10))
|
||||
|
||||
(hk-test
|
||||
"modifyIORef' inside a loop"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\ngo r 0 = return ()\ngo r n = do { IORef.modifyIORef' r (\\x -> x + n); go r (n - 1) }\nmain = do { r <- IORef.newIORef 0; go r 5; v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" 15))
|
||||
|
||||
(hk-test
|
||||
"newIORef inside a function passed via parameter"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.IORef as IORef\nbump r = IORef.modifyIORef r (\\x -> x + 100)\nmain = do { r <- IORef.newIORef 1; bump r; v <- IORef.readIORef r; return v }"))
|
||||
(list "IO" 101))
|
||||
196
lib/haskell/tests/map.sx
Normal file
196
lib/haskell/tests/map.sx
Normal file
@@ -0,0 +1,196 @@
|
||||
;; map.sx — Phase 11 Data.Map unit tests.
|
||||
;;
|
||||
;; Tests both the SX-level `hk-map-*` helpers and the Haskell-level
|
||||
;; `Map.*` aliases bound by the import handler.
|
||||
|
||||
(define
|
||||
hk-as-list
|
||||
(fn
|
||||
(xs)
|
||||
(cond
|
||||
((and (list? xs) (= (first xs) "[]")) (list))
|
||||
((and (list? xs) (= (first xs) ":"))
|
||||
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||
(:else xs))))
|
||||
|
||||
;; ── SX-level (direct hk-map-*) ───────────────────────────────
|
||||
(hk-test
|
||||
"hk-map-empty: size 0, null true"
|
||||
(list (hk-map-size hk-map-empty) (hk-map-null hk-map-empty))
|
||||
(list 0 true))
|
||||
|
||||
(hk-test
|
||||
"hk-map-singleton: lookup hit"
|
||||
(let
|
||||
((m (hk-map-singleton 5 "five")))
|
||||
(list (hk-map-size m) (hk-map-lookup 5 m)))
|
||||
(list 1 (list "Just" "five")))
|
||||
|
||||
(hk-test
|
||||
"hk-map-insert: lookup hit on inserted"
|
||||
(let ((m (hk-map-insert 1 "a" hk-map-empty))) (hk-map-lookup 1 m))
|
||||
(list "Just" "a"))
|
||||
|
||||
(hk-test
|
||||
"hk-map-lookup: miss returns Nothing"
|
||||
(hk-map-lookup 99 (hk-map-singleton 1 "a"))
|
||||
(list "Nothing"))
|
||||
|
||||
(hk-test
|
||||
"hk-map-insert: overwrites existing key"
|
||||
(let
|
||||
((m (hk-map-insert 1 "second" (hk-map-insert 1 "first" hk-map-empty))))
|
||||
(hk-map-lookup 1 m))
|
||||
(list "Just" "second"))
|
||||
|
||||
(hk-test
|
||||
"hk-map-delete: removes key"
|
||||
(let
|
||||
((m (hk-map-insert 2 "b" (hk-map-insert 1 "a" hk-map-empty))))
|
||||
(let
|
||||
((m2 (hk-map-delete 1 m)))
|
||||
(list (hk-map-size m2) (hk-map-lookup 1 m2) (hk-map-lookup 2 m2))))
|
||||
(list 1 (list "Nothing") (list "Just" "b")))
|
||||
|
||||
(hk-test
|
||||
"hk-map-delete: missing key is no-op"
|
||||
(let ((m (hk-map-singleton 1 "a"))) (hk-map-size (hk-map-delete 99 m)))
|
||||
1)
|
||||
|
||||
(hk-test
|
||||
"hk-map-member: true on existing"
|
||||
(hk-map-member 1 (hk-map-singleton 1 "a"))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"hk-map-member: false on missing"
|
||||
(hk-map-member 99 (hk-map-singleton 1 "a"))
|
||||
false)
|
||||
|
||||
(hk-test
|
||||
"hk-map-from-list: builds map; keys sorted"
|
||||
(hk-map-keys
|
||||
(hk-map-from-list
|
||||
(list (list 3 "c") (list 1 "a") (list 5 "e") (list 2 "b"))))
|
||||
(list 1 2 3 5))
|
||||
|
||||
(hk-test
|
||||
"hk-map-from-list: duplicates — last wins"
|
||||
(hk-map-lookup
|
||||
1
|
||||
(hk-map-from-list (list (list 1 "first") (list 1 "second"))))
|
||||
(list "Just" "second"))
|
||||
|
||||
(hk-test
|
||||
"hk-map-to-asc-list: ordered traversal"
|
||||
(hk-map-to-asc-list
|
||||
(hk-map-from-list (list (list 3 "c") (list 1 "a") (list 2 "b"))))
|
||||
(list (list 1 "a") (list 2 "b") (list 3 "c")))
|
||||
|
||||
(hk-test
|
||||
"hk-map-elems: in key order"
|
||||
(hk-map-elems
|
||||
(hk-map-from-list (list (list 3 30) (list 1 10) (list 2 20))))
|
||||
(list 10 20 30))
|
||||
|
||||
(hk-test
|
||||
"hk-map-union-with: combines duplicates"
|
||||
(hk-map-to-asc-list
|
||||
(hk-map-union-with
|
||||
(fn (a b) (str a "+" b))
|
||||
(hk-map-from-list (list (list 1 "a") (list 2 "b")))
|
||||
(hk-map-from-list (list (list 2 "B") (list 3 "c")))))
|
||||
(list (list 1 "a") (list 2 "b+B") (list 3 "c")))
|
||||
|
||||
(hk-test
|
||||
"hk-map-intersection-with: keeps shared keys"
|
||||
(hk-map-to-asc-list
|
||||
(hk-map-intersection-with
|
||||
+
|
||||
(hk-map-from-list (list (list 1 10) (list 2 20)))
|
||||
(hk-map-from-list (list (list 2 200) (list 3 30)))))
|
||||
(list (list 2 220)))
|
||||
|
||||
(hk-test
|
||||
"hk-map-difference: drops m2 keys"
|
||||
(hk-map-keys
|
||||
(hk-map-difference
|
||||
(hk-map-from-list (list (list 1 "a") (list 2 "b") (list 3 "c")))
|
||||
(hk-map-from-list (list (list 2 "x")))))
|
||||
(list 1 3))
|
||||
|
||||
(hk-test
|
||||
"hk-map-foldl-with-key: in-order accumulate"
|
||||
(hk-map-foldl-with-key
|
||||
(fn (acc k v) (str acc k v))
|
||||
""
|
||||
(hk-map-from-list (list (list 3 "c") (list 1 "a") (list 2 "b"))))
|
||||
"1a2b3c")
|
||||
|
||||
(hk-test
|
||||
"hk-map-map-with-key: transforms values"
|
||||
(hk-map-to-asc-list
|
||||
(hk-map-map-with-key
|
||||
(fn (k v) (* k v))
|
||||
(hk-map-from-list (list (list 2 10) (list 3 100)))))
|
||||
(list (list 2 20) (list 3 300)))
|
||||
|
||||
(hk-test
|
||||
"hk-map-filter-with-key: keeps matches"
|
||||
(hk-map-keys
|
||||
(hk-map-filter-with-key
|
||||
(fn (k v) (> k 1))
|
||||
(hk-map-from-list (list (list 1 "a") (list 2 "b") (list 3 "c")))))
|
||||
(list 2 3))
|
||||
|
||||
(hk-test
|
||||
"hk-map-adjust: applies f to existing"
|
||||
(hk-map-lookup
|
||||
1
|
||||
(hk-map-adjust (fn (v) (* v 10)) 1 (hk-map-singleton 1 5)))
|
||||
(list "Just" 50))
|
||||
|
||||
(hk-test
|
||||
"hk-map-insert-with: combines on existing"
|
||||
(hk-map-lookup 1 (hk-map-insert-with + 1 5 (hk-map-singleton 1 10)))
|
||||
(list "Just" 15))
|
||||
|
||||
(hk-test
|
||||
"hk-map-alter: Nothing → delete"
|
||||
(hk-map-size
|
||||
(hk-map-alter
|
||||
(fn (mv) (list "Nothing"))
|
||||
1
|
||||
(hk-map-from-list (list (list 1 "a") (list 2 "b")))))
|
||||
1)
|
||||
|
||||
;; ── Haskell-level (Map.*) via import wiring ─────────────────
|
||||
(hk-test
|
||||
"Map.size after Map.insert chain"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.Map as Map\nmain = Map.size (Map.insert 2 \"b\" (Map.insert 1 \"a\" Map.empty))"))
|
||||
2)
|
||||
|
||||
(hk-test
|
||||
"Map.lookup hit"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.Map as Map\nmain = Map.lookup 1 (Map.insert 1 \"a\" Map.empty)"))
|
||||
(list "Just" "a"))
|
||||
|
||||
(hk-test
|
||||
"Map.lookup miss"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.Map as Map\nmain = Map.lookup 99 (Map.insert 1 \"a\" Map.empty)"))
|
||||
(list "Nothing"))
|
||||
|
||||
(hk-test
|
||||
"Map.member true"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.Map as Map\nmain = Map.member 5 (Map.insert 5 \"x\" Map.empty)"))
|
||||
(list "True"))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
180
lib/haskell/tests/numerics.sx
Normal file
180
lib/haskell/tests/numerics.sx
Normal file
@@ -0,0 +1,180 @@
|
||||
;; numerics.sx — Phase 10 numeric tower verification.
|
||||
;;
|
||||
;; Practical integer-precision limit in Haskell-on-SX:
|
||||
;; • Raw SX `(* a b)` stays exact up to ±2^62 (≈ 4.6e18, OCaml int63).
|
||||
;; • BUT the Haskell tokenizer/parser parses an integer literal as a float
|
||||
;; once it exceeds 2^53 (≈ 9.007e15). Once any operand is a float, the
|
||||
;; binop result is a float (and decimal-precision is lost past 2^53).
|
||||
;; • Therefore: programs that stay below ~9e15 are exact; larger literals
|
||||
;; or accumulated products silently become floats. `factorial 18` is the
|
||||
;; last factorial that stays exact (6.4e15); `factorial 19` already floats.
|
||||
;;
|
||||
;; In Haskell terms, `Int` and `Integer` both currently map to SX number, so
|
||||
;; we don't yet support arbitrary-precision Integer. Documented; unbounded
|
||||
;; Integer is out of scope for Phase 10 — see Phase 11+ if it becomes needed.
|
||||
|
||||
(define
|
||||
hk-as-list
|
||||
(fn
|
||||
(xs)
|
||||
(cond
|
||||
((and (list? xs) (= (first xs) "[]")) (list))
|
||||
((and (list? xs) (= (first xs) ":"))
|
||||
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||
(:else xs))))
|
||||
|
||||
(hk-test
|
||||
"factorial 10 = 3628800 (small, exact)"
|
||||
(hk-deep-force
|
||||
(hk-run "fact 0 = 1\nfact n = n * fact (n - 1)\nmain = fact 10"))
|
||||
3628800)
|
||||
|
||||
(hk-test
|
||||
"factorial 15 = 1307674368000 (mid-range, exact)"
|
||||
(hk-deep-force
|
||||
(hk-run "fact 0 = 1\nfact n = n * fact (n - 1)\nmain = fact 15"))
|
||||
1307674368000)
|
||||
|
||||
(hk-test
|
||||
"factorial 18 = 6402373705728000 (last exact factorial)"
|
||||
(hk-deep-force
|
||||
(hk-run "fact 0 = 1\nfact n = n * fact (n - 1)\nmain = fact 18"))
|
||||
6402373705728000)
|
||||
|
||||
(hk-test
|
||||
"1000000 * 1000000 = 10^12 (exact)"
|
||||
(hk-deep-force (hk-run "main = 1000000 * 1000000"))
|
||||
1000000000000)
|
||||
|
||||
(hk-test
|
||||
"1000000000 * 1000000000 = 10^18 (exact, at boundary)"
|
||||
(hk-deep-force (hk-run "main = 1000000000 * 1000000000"))
|
||||
1e+18)
|
||||
|
||||
(hk-test
|
||||
"2^62 boundary: pow accumulates exactly"
|
||||
(hk-deep-force
|
||||
(hk-run "pow b 0 = 1\npow b n = b * pow b (n - 1)\nmain = pow 2 62"))
|
||||
4.6116860184273879e+18)
|
||||
|
||||
(hk-test
|
||||
"show factorial 12 = 479001600 (whole, fits in 32-bit)"
|
||||
(hk-deep-force
|
||||
(hk-run "fact 0 = 1\nfact n = n * fact (n - 1)\nmain = show (fact 12)"))
|
||||
"479001600")
|
||||
|
||||
(hk-test
|
||||
"negate large positive — preserves magnitude"
|
||||
(hk-deep-force (hk-run "main = negate 1000000000000000000"))
|
||||
-1e+18)
|
||||
|
||||
(hk-test
|
||||
"abs negative large — preserves magnitude"
|
||||
(hk-deep-force (hk-run "main = abs (negate 1000000000000000000)"))
|
||||
1e+18)
|
||||
|
||||
(hk-test
|
||||
"div on large ints"
|
||||
(hk-deep-force (hk-run "main = div 1000000000000000000 1000000000"))
|
||||
1000000000)
|
||||
|
||||
(hk-test
|
||||
"fromIntegral 42 = 42 (identity in our runtime)"
|
||||
(hk-deep-force (hk-run "main = fromIntegral 42"))
|
||||
42)
|
||||
|
||||
(hk-test
|
||||
"fromIntegral preserves negative"
|
||||
(hk-deep-force (hk-run "main = fromIntegral (negate 7)"))
|
||||
-7)
|
||||
|
||||
(hk-test
|
||||
"fromIntegral round-trips through arithmetic"
|
||||
(hk-deep-force (hk-run "main = fromIntegral 5 + fromIntegral 3"))
|
||||
8)
|
||||
|
||||
(hk-test
|
||||
"fromIntegral in a program (mixing with map)"
|
||||
(hk-as-list (hk-deep-force (hk-run "main = map fromIntegral [1,2,3]")))
|
||||
(list 1 2 3))
|
||||
|
||||
(hk-test
|
||||
"toInteger 100 = 100 (identity)"
|
||||
(hk-deep-force (hk-run "main = toInteger 100"))
|
||||
100)
|
||||
|
||||
(hk-test
|
||||
"fromInteger 7 = 7 (identity)"
|
||||
(hk-deep-force (hk-run "main = fromInteger 7"))
|
||||
7)
|
||||
|
||||
(hk-test
|
||||
"toInteger / fromInteger round-trip"
|
||||
(hk-deep-force (hk-run "main = fromInteger (toInteger 42)"))
|
||||
42)
|
||||
|
||||
(hk-test
|
||||
"toInteger preserves negative"
|
||||
(hk-deep-force (hk-run "main = toInteger (negate 13)"))
|
||||
-13)
|
||||
|
||||
(hk-test
|
||||
"show 3.14 = 3.14"
|
||||
(hk-deep-force (hk-run "main = show 3.14"))
|
||||
"3.14")
|
||||
|
||||
(hk-test
|
||||
"show 1.0e10 — whole-valued float renders as decimal (int/float ambiguity)"
|
||||
(hk-deep-force (hk-run "main = show 1.0e10"))
|
||||
"10000000000")
|
||||
|
||||
(hk-test
|
||||
"show 0.001 uses scientific form (sub-0.1)"
|
||||
(hk-deep-force (hk-run "main = show 0.001"))
|
||||
"1.0e-3")
|
||||
|
||||
(hk-test
|
||||
"show negative float"
|
||||
(hk-deep-force (hk-run "main = show (negate 3.14)"))
|
||||
"-3.14")
|
||||
|
||||
(hk-test "sqrt 16 = 4" (hk-deep-force (hk-run "main = sqrt 16")) 4)
|
||||
|
||||
(hk-test "floor 3.7 = 3" (hk-deep-force (hk-run "main = floor 3.7")) 3)
|
||||
|
||||
(hk-test "ceiling 3.2 = 4" (hk-deep-force (hk-run "main = ceiling 3.2")) 4)
|
||||
|
||||
(hk-test
|
||||
"ceiling on whole = self"
|
||||
(hk-deep-force (hk-run "main = ceiling 4"))
|
||||
4)
|
||||
|
||||
(hk-test "round 2.6 = 3" (hk-deep-force (hk-run "main = round 2.6")) 3)
|
||||
|
||||
(hk-test
|
||||
"truncate -3.7 = -3"
|
||||
(hk-deep-force (hk-run "main = truncate (negate 3.7)"))
|
||||
-3)
|
||||
|
||||
(hk-test "recip 4.0 = 0.25" (hk-deep-force (hk-run "main = recip 4.0")) 0.25)
|
||||
|
||||
(hk-test "1.0 / 4.0 = 0.25" (hk-deep-force (hk-run "main = 1.0 / 4.0")) 0.25)
|
||||
|
||||
(hk-test
|
||||
"fromRational 0.5 = 0.5 (identity)"
|
||||
(hk-deep-force (hk-run "main = fromRational 0.5"))
|
||||
0.5)
|
||||
|
||||
(hk-test "pi ≈ 3.14159" (hk-deep-force (hk-run "main = pi")) 3.14159)
|
||||
|
||||
(hk-test "exp 0 = 1" (hk-deep-force (hk-run "main = exp 0")) 1)
|
||||
|
||||
(hk-test "sin 0 = 0" (hk-deep-force (hk-run "main = sin 0")) 0)
|
||||
|
||||
(hk-test "cos 0 = 1" (hk-deep-force (hk-run "main = cos 0")) 1)
|
||||
|
||||
(hk-test "2 ** 10 = 1024" (hk-deep-force (hk-run "main = 2 ** 10")) 1024)
|
||||
|
||||
(hk-test "log (exp 5) ≈ 5" (hk-deep-force (hk-run "main = log (exp 5)")) 5)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
81
lib/haskell/tests/program-accumulate.sx
Normal file
81
lib/haskell/tests/program-accumulate.sx
Normal file
@@ -0,0 +1,81 @@
|
||||
;; accumulate.hs — accumulate results into an IORef [Int] (Phase 15 conformance).
|
||||
|
||||
(define
|
||||
hk-accumulate-source
|
||||
"import qualified Data.IORef as IORef\n\npush :: IORef [Int] -> Int -> IO ()\npush r x = IORef.modifyIORef r (\\xs -> x : xs)\n\npushAll :: IORef [Int] -> [Int] -> IO ()\npushAll r [] = return ()\npushAll r (x:xs) = do\n push r x\n pushAll r xs\n\nreadReversed :: IORef [Int] -> IO [Int]\nreadReversed r = do\n xs <- IORef.readIORef r\n return (reverse xs)\n\ndoubleEach :: IORef [Int] -> [Int] -> IO ()\ndoubleEach r [] = return ()\ndoubleEach r (x:xs) = do\n push r (x * 2)\n doubleEach r xs\n\nsumIntoRef :: IORef Int -> [Int] -> IO ()\nsumIntoRef r [] = return ()\nsumIntoRef r (x:xs) = do\n IORef.modifyIORef r (\\acc -> acc + x)\n sumIntoRef r xs\n\n")
|
||||
|
||||
(hk-test
|
||||
"accumulate.hs — push three then read length"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-accumulate-source
|
||||
"main = do { r <- IORef.newIORef []; push r 1; push r 2; push r 3; xs <- IORef.readIORef r; return (length xs) }")))
|
||||
(list "IO" 3))
|
||||
|
||||
(hk-test
|
||||
"accumulate.hs — pushAll preserves reverse order"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-accumulate-source
|
||||
"main = do { r <- IORef.newIORef []; pushAll r [1,2,3,4]; xs <- IORef.readIORef r; return xs }")))
|
||||
(list
|
||||
"IO"
|
||||
(list ":" 4 (list ":" 3 (list ":" 2 (list ":" 1 (list "[]")))))))
|
||||
|
||||
(hk-test
|
||||
"accumulate.hs — readReversed gives original order"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-accumulate-source
|
||||
"main = do { r <- IORef.newIORef []; pushAll r [10,20,30]; readReversed r }")))
|
||||
(list "IO" (list ":" 10 (list ":" 20 (list ":" 30 (list "[]"))))))
|
||||
|
||||
(hk-test
|
||||
"accumulate.hs — doubleEach maps then accumulates"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-accumulate-source
|
||||
"main = do { r <- IORef.newIORef []; doubleEach r [1,2,3]; readReversed r }")))
|
||||
(list "IO" (list ":" 2 (list ":" 4 (list ":" 6 (list "[]"))))))
|
||||
|
||||
(hk-test
|
||||
"accumulate.hs — sum into Int IORef"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-accumulate-source
|
||||
"main = do { r <- IORef.newIORef 0; sumIntoRef r [1,2,3,4,5]; v <- IORef.readIORef r; return v }")))
|
||||
(list "IO" 15))
|
||||
|
||||
(hk-test
|
||||
"accumulate.hs — empty list leaves ref untouched"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-accumulate-source
|
||||
"main = do { r <- IORef.newIORef [99]; pushAll r []; xs <- IORef.readIORef r; return xs }")))
|
||||
(list "IO" (list ":" 99 (list "[]"))))
|
||||
|
||||
(hk-test
|
||||
"accumulate.hs — pushAll then sumIntoRef on the same input"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-accumulate-source
|
||||
"main = do { r <- IORef.newIORef 0; sumIntoRef r [10,20,30,40]; v <- IORef.readIORef r; return v }")))
|
||||
(list "IO" 100))
|
||||
|
||||
(hk-test
|
||||
"accumulate.hs — accumulate results from a recursive helper"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-accumulate-source
|
||||
"squaresUpTo r 0 = return ()\nsquaresUpTo r n = do { push r (n * n); squaresUpTo r (n - 1) }\nmain = do { r <- IORef.newIORef []; squaresUpTo r 4; readReversed r }")))
|
||||
(list
|
||||
"IO"
|
||||
(list ":" 16 (list ":" 9 (list ":" 4 (list ":" 1 (list "[]")))))))
|
||||
80
lib/haskell/tests/program-caesar.sx
Normal file
80
lib/haskell/tests/program-caesar.sx
Normal file
@@ -0,0 +1,80 @@
|
||||
;; caesar.hs — Caesar cipher.
|
||||
;; Source: https://rosettacode.org/wiki/Caesar_cipher#Haskell (adapted).
|
||||
;;
|
||||
;; Exercises chr, ord, isUpper, isLower, mod, string pattern matching
|
||||
;; (x:xs) over a String (which is now a [Char] string view), and map
|
||||
;; from the Phase 7 string=[Char] foundation.
|
||||
|
||||
(define
|
||||
hk-prog-val
|
||||
(fn
|
||||
(src name)
|
||||
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
|
||||
|
||||
(define
|
||||
hk-as-list
|
||||
(fn
|
||||
(xs)
|
||||
(cond
|
||||
((and (list? xs) (= (first xs) "[]")) (list))
|
||||
((and (list? xs) (= (first xs) ":"))
|
||||
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||
(:else xs))))
|
||||
|
||||
(define
|
||||
hk-caesar-source
|
||||
"shift n c = if isUpper c\n then chr (mod ((ord c) - 65 + n) 26 + 65)\n else if isLower c\n then chr (mod ((ord c) - 97 + n) 26 + 97)\n else chr c\n\ncaesarRec n [] = []\ncaesarRec n (x:xs) = shift n x : caesarRec n xs\n\ncaesarMap n s = map (shift n) s\n")
|
||||
|
||||
(hk-test
|
||||
"caesar.hs — caesarRec 3 \"ABC\" = DEF"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-caesar-source "r = caesarRec 3 \"ABC\"\n") "r"))
|
||||
(list "D" "E" "F"))
|
||||
|
||||
(hk-test
|
||||
"caesar.hs — caesarRec 13 \"Hello\" = Uryyb"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-caesar-source "r = caesarRec 13 \"Hello\"\n") "r"))
|
||||
(list "U" "r" "y" "y" "b"))
|
||||
|
||||
(hk-test
|
||||
"caesar.hs — caesarRec 1 \"AZ\" wraps to BA"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-caesar-source "r = caesarRec 1 \"AZ\"\n") "r"))
|
||||
(list "B" "A"))
|
||||
|
||||
(hk-test
|
||||
"caesar.hs — caesarRec 0 \"World\" identity"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-caesar-source "r = caesarRec 0 \"World\"\n") "r"))
|
||||
(list "W" "o" "r" "l" "d"))
|
||||
|
||||
(hk-test
|
||||
"caesar.hs — caesarRec preserves punctuation"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-caesar-source "r = caesarRec 3 \"Hi!\"\n") "r"))
|
||||
(list "K" "l" "!"))
|
||||
|
||||
(hk-test
|
||||
"caesar.hs — caesarMap 3 \"abc\" via map"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-caesar-source "r = caesarMap 3 \"abc\"\n") "r"))
|
||||
(list "d" "e" "f"))
|
||||
|
||||
(hk-test
|
||||
"caesar.hs — caesarMap 13 round-trips with caesarMap 13"
|
||||
(hk-as-list
|
||||
(hk-prog-val
|
||||
(str
|
||||
hk-caesar-source
|
||||
"r = caesarMap 13 (foldr (\\c acc -> c : acc) [] (caesarMap 13 \"Hello\"))\n")
|
||||
"r"))
|
||||
(list "H" "e" "l" "l" "o"))
|
||||
|
||||
(hk-test
|
||||
"caesar.hs — caesarRec 25 \"AB\" = ZA"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-caesar-source "r = caesarRec 25 \"AB\"\n") "r"))
|
||||
(list "Z" "A"))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
63
lib/haskell/tests/program-config.sx
Normal file
63
lib/haskell/tests/program-config.sx
Normal file
@@ -0,0 +1,63 @@
|
||||
;; config.hs — multi-field config record; partial update; defaultConfig
|
||||
;; constant.
|
||||
;;
|
||||
;; Exercises Phase 14: 4-field record, defaultConfig as a CAF, partial
|
||||
;; updates that change one or two fields, accessors over derived configs.
|
||||
|
||||
(define
|
||||
hk-config-source
|
||||
"data Config = Config { host :: String, port :: Int, retries :: Int, debug :: Bool } deriving (Show)\n\ndefaultConfig = Config { host = \"localhost\", port = 8080, retries = 3, debug = False }\n\ndevConfig = defaultConfig { debug = True }\nremoteConfig = defaultConfig { host = \"api.example.com\", port = 443 }\n")
|
||||
|
||||
(hk-test
|
||||
"config.hs — defaultConfig host"
|
||||
(hk-deep-force (hk-run (str hk-config-source "main = host defaultConfig")))
|
||||
"localhost")
|
||||
|
||||
(hk-test
|
||||
"config.hs — defaultConfig port"
|
||||
(hk-deep-force (hk-run (str hk-config-source "main = port defaultConfig")))
|
||||
8080)
|
||||
|
||||
(hk-test
|
||||
"config.hs — defaultConfig retries"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-config-source "main = retries defaultConfig")))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"config.hs — devConfig flips debug"
|
||||
(hk-deep-force (hk-run (str hk-config-source "main = debug devConfig")))
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"config.hs — devConfig preserves host"
|
||||
(hk-deep-force (hk-run (str hk-config-source "main = host devConfig")))
|
||||
"localhost")
|
||||
|
||||
(hk-test
|
||||
"config.hs — devConfig preserves port"
|
||||
(hk-deep-force (hk-run (str hk-config-source "main = port devConfig")))
|
||||
8080)
|
||||
|
||||
(hk-test
|
||||
"config.hs — remoteConfig new host"
|
||||
(hk-deep-force (hk-run (str hk-config-source "main = host remoteConfig")))
|
||||
"api.example.com")
|
||||
|
||||
(hk-test
|
||||
"config.hs — remoteConfig new port"
|
||||
(hk-deep-force (hk-run (str hk-config-source "main = port remoteConfig")))
|
||||
443)
|
||||
|
||||
(hk-test
|
||||
"config.hs — remoteConfig preserves retries"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-config-source "main = retries remoteConfig")))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"config.hs — remoteConfig preserves debug"
|
||||
(hk-deep-force (hk-run (str hk-config-source "main = debug remoteConfig")))
|
||||
(list "False"))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
66
lib/haskell/tests/program-counter.sx
Normal file
66
lib/haskell/tests/program-counter.sx
Normal file
@@ -0,0 +1,66 @@
|
||||
;; counter.hs — IORef-backed mutable counter (Phase 15 conformance).
|
||||
|
||||
(define
|
||||
hk-counter-source
|
||||
"import qualified Data.IORef as IORef\n\ncount :: IORef Int -> Int -> IO ()\ncount r 0 = return ()\ncount r n = do\n IORef.modifyIORef r (\\x -> x + 1)\n count r (n - 1)\n\ncountBy :: IORef Int -> Int -> Int -> IO ()\ncountBy r step 0 = return ()\ncountBy r step n = do\n IORef.modifyIORef r (\\x -> x + step)\n countBy r step (n - 1)\n\nnewCounter :: Int -> IO (IORef Int)\nnewCounter v = IORef.newIORef v\n\nbumpAndRead :: IORef Int -> IO Int\nbumpAndRead r = do\n IORef.modifyIORef r (\\x -> x + 1)\n IORef.readIORef r\n\n")
|
||||
|
||||
(hk-test
|
||||
"counter.hs — start at 0, count 5 ⇒ 5"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-counter-source
|
||||
"main = do { r <- newCounter 0; count r 5; v <- IORef.readIORef r; return v }")))
|
||||
(list "IO" 5))
|
||||
|
||||
(hk-test
|
||||
"counter.hs — start at 100, count 10 ⇒ 110"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-counter-source
|
||||
"main = do { r <- newCounter 100; count r 10; v <- IORef.readIORef r; return v }")))
|
||||
(list "IO" 110))
|
||||
|
||||
(hk-test
|
||||
"counter.hs — countBy step 5, n 4 ⇒ 20"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-counter-source
|
||||
"main = do { r <- newCounter 0; countBy r 5 4; v <- IORef.readIORef r; return v }")))
|
||||
(list "IO" 20))
|
||||
|
||||
(hk-test
|
||||
"counter.hs — bumpAndRead returns updated value"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-counter-source "main = do { r <- newCounter 41; bumpAndRead r }")))
|
||||
(list "IO" 42))
|
||||
|
||||
(hk-test
|
||||
"counter.hs — count then countBy compose"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-counter-source
|
||||
"main = do { r <- newCounter 0; count r 3; countBy r 10 2; v <- IORef.readIORef r; return v }")))
|
||||
(list "IO" 23))
|
||||
|
||||
(hk-test
|
||||
"counter.hs — two independent counters"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-counter-source
|
||||
"main = do { a <- newCounter 0; b <- newCounter 0; count a 7; countBy b 100 2; va <- IORef.readIORef a; vb <- IORef.readIORef b; return (va + vb) }")))
|
||||
(list "IO" 207))
|
||||
|
||||
(hk-test
|
||||
"counter.hs — modifyIORef' (strict) variant"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-counter-source
|
||||
"tick r 0 = return ()\ntick r n = do { IORef.modifyIORef' r (\\x -> x + 1); tick r (n - 1) }\nmain = do { r <- newCounter 0; tick r 50; v <- IORef.readIORef r; return v }")))
|
||||
(list "IO" 50))
|
||||
46
lib/haskell/tests/program-mapgraph.sx
Normal file
46
lib/haskell/tests/program-mapgraph.sx
Normal file
@@ -0,0 +1,46 @@
|
||||
;; mapgraph.hs — adjacency-list using Data.Map (BFS-style traversal).
|
||||
;;
|
||||
;; Exercises Phase 11: `import qualified Data.Map as Map`, `Map.empty`,
|
||||
;; `Map.insert`, `Map.lookup`, `Map.findWithDefault`. Adjacency lists are
|
||||
;; stored as `Map Int [Int]`; `neighbors` does a default-empty lookup.
|
||||
|
||||
(define
|
||||
hk-mapgraph-source
|
||||
"import qualified Data.Map as Map\n\nemptyG = Map.empty\n\naddEdge u v g = Map.insertWith add u [v] g\n where add new old = new ++ old\n\nbuild = addEdge 1 2 (addEdge 1 3 (addEdge 2 4 (addEdge 3 4 (addEdge 4 5 emptyG))))\n\nneighbors n g = Map.findWithDefault [] n g\n")
|
||||
|
||||
(hk-test
|
||||
"mapgraph.hs — neighbors of 1"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-mapgraph-source "main = neighbors 1 build\n")))
|
||||
(list ":" 2 (list ":" 3 (list "[]"))))
|
||||
|
||||
(hk-test
|
||||
"mapgraph.hs — neighbors of 4"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-mapgraph-source "main = neighbors 4 build\n")))
|
||||
(list ":" 5 (list "[]")))
|
||||
|
||||
(hk-test
|
||||
"mapgraph.hs — neighbors of 5 (leaf, no entry) defaults to []"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-mapgraph-source "main = neighbors 5 build\n")))
|
||||
(list "[]"))
|
||||
|
||||
(hk-test
|
||||
"mapgraph.hs — neighbors of 99 (absent) defaults to []"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-mapgraph-source "main = neighbors 99 build\n")))
|
||||
(list "[]"))
|
||||
|
||||
(hk-test
|
||||
"mapgraph.hs — Map.member 1"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-mapgraph-source "main = Map.member 1 build\n")))
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"mapgraph.hs — Map.size = 4 source nodes"
|
||||
(hk-deep-force (hk-run (str hk-mapgraph-source "main = Map.size build\n")))
|
||||
4)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
49
lib/haskell/tests/program-newton.sx
Normal file
49
lib/haskell/tests/program-newton.sx
Normal file
@@ -0,0 +1,49 @@
|
||||
;; newton.hs — Newton's method for square root.
|
||||
;; Source: classic numerical analysis exercise.
|
||||
;;
|
||||
;; Exercises Phase 10: `Float`, `abs`, `/`, iteration via `until`.
|
||||
|
||||
(define
|
||||
hk-prog-val
|
||||
(fn
|
||||
(src name)
|
||||
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
|
||||
|
||||
(define
|
||||
hk-newton-source
|
||||
"improve x guess = (guess + x / guess) / 2\n\ngoodEnough x guess = abs (guess * guess - x) < 0.0001\n\nnewtonSqrt x = newtonHelp x 1.0\n\nnewtonHelp x guess = if goodEnough x guess\n then guess\n else newtonHelp x (improve x guess)\n")
|
||||
|
||||
(hk-test
|
||||
"newton.hs — newtonSqrt 4 ≈ 2"
|
||||
(hk-prog-val
|
||||
(str hk-newton-source "r = abs (newtonSqrt 4.0 - 2.0) < 0.001\n")
|
||||
"r")
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"newton.hs — newtonSqrt 9 ≈ 3"
|
||||
(hk-prog-val
|
||||
(str hk-newton-source "r = abs (newtonSqrt 9.0 - 3.0) < 0.001\n")
|
||||
"r")
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"newton.hs — newtonSqrt 2 ≈ 1.41421"
|
||||
(hk-prog-val
|
||||
(str hk-newton-source "r = abs (newtonSqrt 2.0 - 1.41421) < 0.001\n")
|
||||
"r")
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"newton.hs — improve converges (one step)"
|
||||
(hk-prog-val (str hk-newton-source "r = improve 4.0 1.0\n") "r")
|
||||
2.5)
|
||||
|
||||
(hk-test
|
||||
"newton.hs — newtonSqrt 100 ≈ 10"
|
||||
(hk-prog-val
|
||||
(str hk-newton-source "r = abs (newtonSqrt 100.0 - 10.0) < 0.001\n")
|
||||
"r")
|
||||
(list "True"))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
58
lib/haskell/tests/program-partial.sx
Normal file
58
lib/haskell/tests/program-partial.sx
Normal file
@@ -0,0 +1,58 @@
|
||||
;; partial.hs — exercises Phase 9 partial functions caught at the top level.
|
||||
;;
|
||||
;; Each program calls a partial function on bad input; hk-run-io catches the
|
||||
;; raise and appends the error message to io-lines so tests can inspect.
|
||||
|
||||
(hk-test
|
||||
"partial.hs — main = print (head [])"
|
||||
(let
|
||||
((lines (hk-run-io "main = print (head [])")))
|
||||
(>= (index-of (str lines) "Prelude.head: empty list") 0))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"partial.hs — main = print (tail [])"
|
||||
(let
|
||||
((lines (hk-run-io "main = print (tail [])")))
|
||||
(>= (index-of (str lines) "Prelude.tail: empty list") 0))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"partial.hs — main = print (fromJust Nothing)"
|
||||
(let
|
||||
((lines (hk-run-io "main = print (fromJust Nothing)")))
|
||||
(>= (index-of (str lines) "Maybe.fromJust: Nothing") 0))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"partial.hs — putStrLn before error preserves prior output"
|
||||
(let
|
||||
((lines (hk-run-io "main = do { putStrLn \"step 1\"; putStrLn (show (head [])); putStrLn \"never\" }")))
|
||||
(and
|
||||
(>= (index-of (str lines) "step 1") 0)
|
||||
(>= (index-of (str lines) "Prelude.head: empty list") 0)
|
||||
(= (index-of (str lines) "never") -1)))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"partial.hs — undefined as IO action"
|
||||
(let
|
||||
((lines (hk-run-io "main = print undefined")))
|
||||
(>= (index-of (str lines) "Prelude.undefined") 0))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"partial.hs — catches error from a user-thrown error"
|
||||
(let
|
||||
((lines (hk-run-io "main = error \"boom from main\"")))
|
||||
(>= (index-of (str lines) "boom from main") 0))
|
||||
true)
|
||||
|
||||
;; Negative case: when no error is raised, io-lines doesn't contain
|
||||
;; "Prelude" prefixes from our error path.
|
||||
(hk-test
|
||||
"partial.hs — happy path: head [42] succeeds, no error in output"
|
||||
(hk-run-io "main = print (head [42])")
|
||||
(list "42"))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
51
lib/haskell/tests/program-person.sx
Normal file
51
lib/haskell/tests/program-person.sx
Normal file
@@ -0,0 +1,51 @@
|
||||
;; person.hs — record type with accessors, update, deriving Show.
|
||||
;;
|
||||
;; Exercises Phase 14: data with record syntax, accessor functions,
|
||||
;; record creation, record update, deriving Show on a record.
|
||||
|
||||
(define
|
||||
hk-person-source
|
||||
"data Person = Person { name :: String, age :: Int } deriving (Show)\n\nalice = Person { name = \"alice\", age = 30 }\nbob = Person { name = \"bob\", age = 25 }\n\nbirthday p = p { age = age p + 1 }\n")
|
||||
|
||||
(hk-test
|
||||
"person.hs — alice's name"
|
||||
(hk-deep-force (hk-run (str hk-person-source "main = name alice")))
|
||||
"alice")
|
||||
|
||||
(hk-test
|
||||
"person.hs — alice's age"
|
||||
(hk-deep-force (hk-run (str hk-person-source "main = age alice")))
|
||||
30)
|
||||
|
||||
(hk-test
|
||||
"person.hs — birthday adds one year"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-person-source "main = age (birthday alice)")))
|
||||
31)
|
||||
|
||||
(hk-test
|
||||
"person.hs — birthday preserves name"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-person-source "main = name (birthday alice)")))
|
||||
"alice")
|
||||
|
||||
(hk-test
|
||||
"person.hs — show alice"
|
||||
(hk-deep-force (hk-run (str hk-person-source "main = show alice")))
|
||||
"Person \"alice\" 30")
|
||||
|
||||
(hk-test
|
||||
"person.hs — bob has different name"
|
||||
(hk-deep-force (hk-run (str hk-person-source "main = name bob")))
|
||||
"bob")
|
||||
|
||||
(hk-test
|
||||
"person.hs — pattern match in function"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-person-source
|
||||
"greet (Person { name = n }) = \"Hi, \" ++ n\nmain = greet alice")))
|
||||
"Hi, alice")
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
83
lib/haskell/tests/program-runlength-str.sx
Normal file
83
lib/haskell/tests/program-runlength-str.sx
Normal file
@@ -0,0 +1,83 @@
|
||||
;; runlength-str.hs — run-length encoding on a String.
|
||||
;; Source: https://rosettacode.org/wiki/Run-length_encoding#Haskell (adapted).
|
||||
;;
|
||||
;; Exercises String pattern matching `(x:xs)`, `span` over a string view,
|
||||
;; tuple construction `(Int, Char)`, character equality, and tuple-in-cons
|
||||
;; patterns `((n, c) : rest)` — all enabled by Phase 7 string=[Char].
|
||||
|
||||
(define
|
||||
hk-prog-val
|
||||
(fn
|
||||
(src name)
|
||||
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
|
||||
|
||||
(define
|
||||
hk-as-list
|
||||
(fn
|
||||
(xs)
|
||||
(cond
|
||||
((and (list? xs) (= (first xs) "[]")) (list))
|
||||
((and (list? xs) (= (first xs) ":"))
|
||||
(cons (nth xs 1) (hk-as-list (nth xs 2))))
|
||||
(:else xs))))
|
||||
|
||||
(define
|
||||
hk-rle-source
|
||||
"encodeRL [] = []\nencodeRL (x:xs) = let (same, rest) = span eqX xs\n eqX y = y == x\n in (1 + length same, x) : encodeRL rest\n\nreplicateRL 0 _ = []\nreplicateRL n c = c : replicateRL (n - 1) c\n\ndecodeRL [] = []\ndecodeRL ((n, c) : rest) = replicateRL n c ++ decodeRL rest\n")
|
||||
|
||||
(hk-test
|
||||
"rle.hs — encodeRL [] = []"
|
||||
(hk-as-list (hk-prog-val (str hk-rle-source "r = encodeRL \"\"\n") "r"))
|
||||
(list))
|
||||
|
||||
(hk-test
|
||||
"rle.hs — length (encodeRL \"aabbbcc\") = 3"
|
||||
(hk-prog-val (str hk-rle-source "r = length (encodeRL \"aabbbcc\")\n") "r")
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"rle.hs — map fst (encodeRL \"aabbbcc\") = [2,3,2]"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-rle-source "r = map fst (encodeRL \"aabbbcc\")\n") "r"))
|
||||
(list 2 3 2))
|
||||
|
||||
(hk-test
|
||||
"rle.hs — map snd (encodeRL \"aabbbcc\") = [97,98,99]"
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-rle-source "r = map snd (encodeRL \"aabbbcc\")\n") "r"))
|
||||
(list 97 98 99))
|
||||
|
||||
(hk-test
|
||||
"rle.hs — counts of encodeRL \"aabbbccddddee\" = [2,3,2,4,2]"
|
||||
(hk-as-list
|
||||
(hk-prog-val
|
||||
(str hk-rle-source "r = map fst (encodeRL \"aabbbccddddee\")\n")
|
||||
"r"))
|
||||
(list 2 3 2 4 2))
|
||||
|
||||
(hk-test
|
||||
"rle.hs — chars of encodeRL \"aabbbccddddee\" = [97,98,99,100,101]"
|
||||
(hk-as-list
|
||||
(hk-prog-val
|
||||
(str hk-rle-source "r = map snd (encodeRL \"aabbbccddddee\")\n")
|
||||
"r"))
|
||||
(list 97 98 99 100 101))
|
||||
|
||||
(hk-test
|
||||
"rle.hs — singleton encodeRL \"x\""
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-rle-source "r = map fst (encodeRL \"x\")\n") "r"))
|
||||
(list 1))
|
||||
|
||||
(hk-test
|
||||
"rle.hs — decodeRL round-trip preserves \"aabbbcc\""
|
||||
(hk-as-list
|
||||
(hk-prog-val (str hk-rle-source "r = decodeRL (encodeRL \"aabbbcc\")\n") "r"))
|
||||
(list 97 97 98 98 98 99 99))
|
||||
|
||||
(hk-test
|
||||
"rle.hs — replicateRL 4 65 = [65,65,65,65]"
|
||||
(hk-as-list (hk-prog-val (str hk-rle-source "r = replicateRL 4 65\n") "r"))
|
||||
(list 65 65 65 65))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
80
lib/haskell/tests/program-safediv.sx
Normal file
80
lib/haskell/tests/program-safediv.sx
Normal file
@@ -0,0 +1,80 @@
|
||||
;; safediv.hs — safe division using catch (Phase 16 conformance).
|
||||
|
||||
(define
|
||||
hk-safediv-source
|
||||
"safeDiv :: Int -> Int -> IO Int
|
||||
safeDiv _ 0 = throwIO (SomeException \"division by zero\")
|
||||
safeDiv x y = return (x `div` y)
|
||||
|
||||
guarded :: Int -> Int -> IO Int
|
||||
guarded x y = catch (safeDiv x y) (\\(SomeException _) -> return 0)
|
||||
|
||||
reason :: Int -> Int -> IO String
|
||||
reason x y = catch (safeDiv x y `seq` return \"ok\")
|
||||
(\\(SomeException m) -> return m)
|
||||
|
||||
bothBranches :: Int -> Int -> IO Int
|
||||
bothBranches x y = do
|
||||
v <- catch (safeDiv x y) (\\(SomeException _) -> return (-1))
|
||||
return (v + 100)
|
||||
|
||||
")
|
||||
|
||||
(hk-test
|
||||
"safediv.hs — divide by non-zero"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-safediv-source "main = guarded 10 2")))
|
||||
(list "IO" 5))
|
||||
|
||||
(hk-test
|
||||
"safediv.hs — divide by zero returns 0"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-safediv-source "main = guarded 10 0")))
|
||||
(list "IO" 0))
|
||||
|
||||
(hk-test
|
||||
"safediv.hs — divide by zero — reason captured"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-safediv-source "main = catch (safeDiv 1 0) (\\(SomeException m) -> return 0) >> reason 1 0")))
|
||||
(list "IO" "division by zero"))
|
||||
|
||||
(hk-test
|
||||
"safediv.hs — bothBranches success path"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-safediv-source "main = bothBranches 8 2")))
|
||||
(list "IO" 104))
|
||||
|
||||
(hk-test
|
||||
"safediv.hs — bothBranches failure path"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-safediv-source "main = bothBranches 8 0")))
|
||||
(list "IO" 99))
|
||||
|
||||
(hk-test
|
||||
"safediv.hs — chained safeDiv with catch"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-safediv-source
|
||||
"main = do { a <- guarded 20 4; b <- guarded 7 0; return (a + b) }")))
|
||||
(list "IO" 5))
|
||||
|
||||
(hk-test
|
||||
"safediv.hs — try then bind through Either"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-safediv-source
|
||||
"main = do { r <- try (safeDiv 1 0); case r of { Right v -> return v; Left (SomeException m) -> return 999 } }")))
|
||||
(list "IO" 999))
|
||||
|
||||
(hk-test
|
||||
"safediv.hs — handle (flip catch)"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-safediv-source
|
||||
"main = handle (\\(SomeException _) -> return 0) (safeDiv 5 0)")))
|
||||
(list "IO" 0))
|
||||
61
lib/haskell/tests/program-setops.sx
Normal file
61
lib/haskell/tests/program-setops.sx
Normal file
@@ -0,0 +1,61 @@
|
||||
;; setops.hs — set union/intersection/difference on integer sets.
|
||||
;;
|
||||
;; Exercises Phase 12: `import qualified Data.Set as Set`, all three
|
||||
;; combining operations + isSubsetOf.
|
||||
|
||||
(define
|
||||
hk-setops-source
|
||||
"import qualified Data.Set as Set\n\ns1 = Set.insert 1 (Set.insert 2 (Set.insert 3 Set.empty))\ns2 = Set.insert 3 (Set.insert 4 (Set.insert 5 Set.empty))\ns3 = Set.insert 1 (Set.insert 2 Set.empty)\n")
|
||||
|
||||
(hk-test
|
||||
"setops.hs — union size = 5"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-setops-source "main = Set.size (Set.union s1 s2)\n")))
|
||||
5)
|
||||
|
||||
(hk-test
|
||||
"setops.hs — intersection size = 1"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-setops-source "main = Set.size (Set.intersection s1 s2)\n")))
|
||||
1)
|
||||
|
||||
(hk-test
|
||||
"setops.hs — intersection contains 3"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-setops-source "main = Set.member 3 (Set.intersection s1 s2)\n")))
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"setops.hs — difference s1 s2 size = 2"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-setops-source "main = Set.size (Set.difference s1 s2)\n")))
|
||||
2)
|
||||
|
||||
(hk-test
|
||||
"setops.hs — difference doesn't contain shared key"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-setops-source "main = Set.member 3 (Set.difference s1 s2)\n")))
|
||||
(list "False"))
|
||||
|
||||
(hk-test
|
||||
"setops.hs — s3 is subset of s1"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-setops-source "main = Set.isSubsetOf s3 s1\n")))
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"setops.hs — s1 not subset of s3"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-setops-source "main = Set.isSubsetOf s1 s3\n")))
|
||||
(list "False"))
|
||||
|
||||
(hk-test
|
||||
"setops.hs — empty set is subset of anything"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-setops-source "main = Set.isSubsetOf Set.empty s1\n")))
|
||||
(list "True"))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
40
lib/haskell/tests/program-shapes.sx
Normal file
40
lib/haskell/tests/program-shapes.sx
Normal file
@@ -0,0 +1,40 @@
|
||||
;; shapes.hs — class Area with a default perimeter, two instances
|
||||
;; using where-local helpers.
|
||||
;;
|
||||
;; Exercises Phase 13: class default method (perimeter), instance
|
||||
;; methods that use `where`-bindings.
|
||||
|
||||
(define
|
||||
hk-shapes-source
|
||||
"class Shape a where\n area :: a -> Int\n perimeter :: a -> Int\n perimeter x = quadrilateral x\n where quadrilateral y = 2 * (sideA y + sideB y)\n sideA z = 1\n sideB z = 1\n\ndata Square = Square Int\ndata Rect = Rect Int Int\n\ninstance Shape Square where\n area (Square s) = s * s\n perimeter (Square s) = 4 * s\n\ninstance Shape Rect where\n area (Rect w h) = w * h\n perimeter (Rect w h) = peri\n where peri = 2 * (w + h)\n")
|
||||
|
||||
(hk-test
|
||||
"shapes.hs — area of Square 5 = 25"
|
||||
(hk-deep-force (hk-run (str hk-shapes-source "main = area (Square 5)\n")))
|
||||
25)
|
||||
|
||||
(hk-test
|
||||
"shapes.hs — perimeter of Square 5 = 20"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-shapes-source "main = perimeter (Square 5)\n")))
|
||||
20)
|
||||
|
||||
(hk-test
|
||||
"shapes.hs — area of Rect 3 4 = 12"
|
||||
(hk-deep-force (hk-run (str hk-shapes-source "main = area (Rect 3 4)\n")))
|
||||
12)
|
||||
|
||||
(hk-test
|
||||
"shapes.hs — perimeter of Rect 3 4 = 14 (via where-bound)"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-shapes-source "main = perimeter (Rect 3 4)\n")))
|
||||
14)
|
||||
|
||||
(hk-test
|
||||
"shapes.hs — Square sums area + perimeter"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-shapes-source "main = area (Square 4) + perimeter (Square 4)\n")))
|
||||
32)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
45
lib/haskell/tests/program-showadt.sx
Normal file
45
lib/haskell/tests/program-showadt.sx
Normal file
@@ -0,0 +1,45 @@
|
||||
;; showadt.hs — `deriving (Show)` on a multi-constructor recursive ADT.
|
||||
;; Source: classic exposition example, e.g. Real World Haskell ch.6.
|
||||
;;
|
||||
;; Exercises Phase 8: `deriving (Show)` on an ADT whose constructors recurse
|
||||
;; into themselves; precedence-based paren wrapping for nested arguments;
|
||||
;; `print` from the prelude (which is `putStrLn (show x)`).
|
||||
|
||||
(define
|
||||
hk-showadt-source
|
||||
"data Expr = Lit Int | Add Expr Expr | Mul Expr Expr deriving (Show)\n\nmain = do\n print (Lit 3)\n print (Add (Lit 1) (Lit 2))\n print (Mul (Lit 3) (Add (Lit 4) (Lit 5)))\n")
|
||||
|
||||
(hk-test
|
||||
"showadt.hs — main prints three lines"
|
||||
(hk-run-io hk-showadt-source)
|
||||
(list "Lit 3" "Add (Lit 1) (Lit 2)" "Mul (Lit 3) (Add (Lit 4) (Lit 5))"))
|
||||
|
||||
(hk-test
|
||||
"showadt.hs — show Lit 3"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Expr = Lit Int | Add Expr Expr | Mul Expr Expr deriving (Show)\nmain = show (Lit 3)"))
|
||||
"Lit 3")
|
||||
|
||||
(hk-test
|
||||
"showadt.hs — show Add wraps both args"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Expr = Lit Int | Add Expr Expr | Mul Expr Expr deriving (Show)\nmain = show (Add (Lit 1) (Lit 2))"))
|
||||
"Add (Lit 1) (Lit 2)")
|
||||
|
||||
(hk-test
|
||||
"showadt.hs — fully nested Mul of Adds"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Expr = Lit Int | Add Expr Expr | Mul Expr Expr deriving (Show)\nmain = show (Mul (Add (Lit 1) (Lit 2)) (Add (Lit 3) (Lit 4)))"))
|
||||
"Mul (Add (Lit 1) (Lit 2)) (Add (Lit 3) (Lit 4))")
|
||||
|
||||
(hk-test
|
||||
"showadt.hs — Lit with negative literal wraps int in parens"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Expr = Lit Int | Add Expr Expr | Mul Expr Expr deriving (Show)\nmain = show (Lit (negate 7))"))
|
||||
"Lit (-7)")
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
36
lib/haskell/tests/program-showio.sx
Normal file
36
lib/haskell/tests/program-showio.sx
Normal file
@@ -0,0 +1,36 @@
|
||||
;; showio.hs — `print` on various types inside a `do` block.
|
||||
;;
|
||||
;; Exercises Phase 8 `print x = putStrLn (show x)` and the IO monad's
|
||||
;; statement sequencing. Each `print` produces one io-line.
|
||||
|
||||
(define
|
||||
hk-showio-source
|
||||
"main = do\n print 42\n print True\n print False\n print [1,2,3]\n print (1, 2)\n print (Just 5)\n print Nothing\n print \"hello\"\n")
|
||||
|
||||
(hk-test
|
||||
"showio.hs — main produces 8 lines, all show-formatted"
|
||||
(hk-run-io hk-showio-source)
|
||||
(list "42" "True" "False" "[1,2,3]" "(1,2)" "Just 5" "Nothing" "\"hello\""))
|
||||
|
||||
(hk-test
|
||||
"showio.hs — print Int alone"
|
||||
(hk-run-io "main = print 42")
|
||||
(list "42"))
|
||||
|
||||
(hk-test
|
||||
"showio.hs — print list of Maybe"
|
||||
(hk-run-io "main = print [Just 1, Nothing, Just 3]")
|
||||
(list "[Just 1,Nothing,Just 3]"))
|
||||
|
||||
(hk-test
|
||||
"showio.hs — print nested tuple"
|
||||
(hk-run-io "main = print ((1, 2), (3, 4))")
|
||||
(list "((1,2),(3,4))"))
|
||||
|
||||
(hk-test
|
||||
"showio.hs — print derived ADT inside do"
|
||||
(hk-run-io
|
||||
"data Color = Red | Green | Blue deriving (Show)\nmain = do { print Red; print Green; print Blue }")
|
||||
(list "Red" "Green" "Blue"))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
45
lib/haskell/tests/program-statistics.sx
Normal file
45
lib/haskell/tests/program-statistics.sx
Normal file
@@ -0,0 +1,45 @@
|
||||
;; statistics.hs — mean, variance, std-dev on a [Double].
|
||||
;; Source: classic textbook example.
|
||||
;;
|
||||
;; Exercises Phase 10: `fromIntegral`, `/`, `sqrt`, list ops on `[Double]`.
|
||||
|
||||
(define
|
||||
hk-prog-val
|
||||
(fn
|
||||
(src name)
|
||||
(hk-deep-force (get (hk-eval-program (hk-core src)) name))))
|
||||
|
||||
(define
|
||||
hk-stats-source
|
||||
"mean xs = sum xs / fromIntegral (length xs)\n\nvariance xs = let m = mean xs\n sqDiff x = (x - m) * (x - m)\n in sum (map sqDiff xs) / fromIntegral (length xs)\n\nstdDev xs = sqrt (variance xs)\n")
|
||||
|
||||
(hk-test
|
||||
"statistics.hs — mean [1,2,3,4,5] = 3"
|
||||
(hk-prog-val (str hk-stats-source "r = mean [1.0,2.0,3.0,4.0,5.0]\n") "r")
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"statistics.hs — mean [10,20,30] = 20"
|
||||
(hk-prog-val (str hk-stats-source "r = mean [10.0,20.0,30.0]\n") "r")
|
||||
20)
|
||||
|
||||
(hk-test
|
||||
"statistics.hs — variance [2,4,4,4,5,5,7,9] = 4"
|
||||
(hk-prog-val
|
||||
(str hk-stats-source "r = variance [2.0,4.0,4.0,4.0,5.0,5.0,7.0,9.0]\n")
|
||||
"r")
|
||||
4)
|
||||
|
||||
(hk-test
|
||||
"statistics.hs — stdDev [2,4,4,4,5,5,7,9] = 2"
|
||||
(hk-prog-val
|
||||
(str hk-stats-source "r = stdDev [2.0,4.0,4.0,4.0,5.0,5.0,7.0,9.0]\n")
|
||||
"r")
|
||||
2)
|
||||
|
||||
(hk-test
|
||||
"statistics.hs — variance of constant list = 0"
|
||||
(hk-prog-val (str hk-stats-source "r = variance [5.0,5.0,5.0,5.0]\n") "r")
|
||||
0)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
95
lib/haskell/tests/program-trycatch.sx
Normal file
95
lib/haskell/tests/program-trycatch.sx
Normal file
@@ -0,0 +1,95 @@
|
||||
;; trycatch.hs — try pattern: branch on Left/Right (Phase 16 conformance).
|
||||
|
||||
(define
|
||||
hk-trycatch-source
|
||||
"parseInt :: String -> IO Int
|
||||
parseInt \"zero\" = return 0
|
||||
parseInt \"one\" = return 1
|
||||
parseInt \"two\" = return 2
|
||||
parseInt s = throwIO (SomeException (\"unknown: \" ++ s))
|
||||
|
||||
describe :: Either SomeException Int -> String
|
||||
describe (Right v) = \"got \" ++ show v
|
||||
describe (Left (SomeException m)) = \"err: \" ++ m
|
||||
|
||||
trial :: String -> IO String
|
||||
trial s = do
|
||||
r <- try (parseInt s)
|
||||
return (describe r)
|
||||
|
||||
run3 :: String -> String -> String -> IO [String]
|
||||
run3 a b c = do
|
||||
ra <- trial a
|
||||
rb <- trial b
|
||||
rc <- trial c
|
||||
return [ra, rb, rc]
|
||||
|
||||
")
|
||||
|
||||
(hk-test
|
||||
"trycatch.hs — Right branch"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-trycatch-source "main = trial \"one\"")))
|
||||
(list "IO" "got 1"))
|
||||
|
||||
(hk-test
|
||||
"trycatch.hs — Left branch with message"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-trycatch-source "main = trial \"banana\"")))
|
||||
(list "IO" "err: unknown: banana"))
|
||||
|
||||
(hk-test
|
||||
"trycatch.hs — chain over three inputs, all good"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-trycatch-source "main = run3 \"zero\" \"one\" \"two\"")))
|
||||
(list "IO"
|
||||
(list ":" "got 0"
|
||||
(list ":" "got 1"
|
||||
(list ":" "got 2"
|
||||
(list "[]"))))))
|
||||
|
||||
(hk-test
|
||||
"trycatch.hs — chain over three inputs, mixed"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-trycatch-source "main = run3 \"zero\" \"qux\" \"two\"")))
|
||||
(list "IO"
|
||||
(list ":" "got 0"
|
||||
(list ":" "err: unknown: qux"
|
||||
(list ":" "got 2"
|
||||
(list "[]"))))))
|
||||
|
||||
(hk-test
|
||||
"trycatch.hs — Left from throwIO carries message"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-trycatch-source
|
||||
"main = do { r <- try (throwIO (SomeException \"explicit\")); return (describe r) }")))
|
||||
(list "IO" "err: explicit"))
|
||||
|
||||
(hk-test
|
||||
"trycatch.hs — Right preserves the int"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-trycatch-source
|
||||
"main = do { r <- try (return 42); return (describe r) }")))
|
||||
(list "IO" "got 42"))
|
||||
|
||||
(hk-test
|
||||
"trycatch.hs — pattern-bind on Right inside do"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-trycatch-source
|
||||
"main = do { Right v <- try (parseInt \"two\"); return (v + 100) }")))
|
||||
(list "IO" 102))
|
||||
|
||||
(hk-test
|
||||
"trycatch.hs — handle alias on parseInt failure"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-trycatch-source
|
||||
"main = handle (\\(SomeException m) -> return (\"caught: \" ++ m)) (parseInt \"nope\" >>= (\\v -> return (show v)))")))
|
||||
(list "IO" "caught: unknown: nope"))
|
||||
35
lib/haskell/tests/program-uniquewords.sx
Normal file
35
lib/haskell/tests/program-uniquewords.sx
Normal file
@@ -0,0 +1,35 @@
|
||||
;; uniquewords.hs — count unique words using Data.Set.
|
||||
;;
|
||||
;; Exercises Phase 12: `import qualified Data.Set as Set`, `Set.empty`,
|
||||
;; `Set.insert`, `Set.size`, `foldl`.
|
||||
|
||||
(define
|
||||
hk-uniquewords-source
|
||||
"import qualified Data.Set as Set\n\naddWord s w = Set.insert w s\n\nuniqueWords ws = foldl addWord Set.empty ws\n\nresult = uniqueWords [\"the\", \"cat\", \"the\", \"dog\", \"the\", \"cat\"]\n")
|
||||
|
||||
(hk-test
|
||||
"uniquewords.hs — unique count = 3"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-uniquewords-source "main = Set.size result\n")))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"uniquewords.hs — \"the\" present"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-uniquewords-source "main = Set.member \"the\" result\n")))
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"uniquewords.hs — \"missing\" absent"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-uniquewords-source "main = Set.member \"missing\" result\n")))
|
||||
(list "False"))
|
||||
|
||||
(hk-test
|
||||
"uniquewords.hs — empty list yields empty set"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.Set as Set\nmain = Set.size (foldl (\\s w -> Set.insert w s) Set.empty [])"))
|
||||
0)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
54
lib/haskell/tests/program-wordfreq.sx
Normal file
54
lib/haskell/tests/program-wordfreq.sx
Normal file
@@ -0,0 +1,54 @@
|
||||
;; wordfreq.hs — word-frequency histogram using Data.Map.
|
||||
;; Source: Rosetta Code "Word frequency" (Haskell entry, simplified).
|
||||
;;
|
||||
;; Exercises Phase 11: `import qualified Data.Map as Map`, `Map.empty`,
|
||||
;; `Map.insertWith`, `Map.lookup`, `Map.findWithDefault`, `foldl`.
|
||||
|
||||
(define
|
||||
hk-wordfreq-source
|
||||
"import qualified Data.Map as Map\n\ncountWord m w = Map.insertWith (+) w 1 m\n\nwordFreq xs = foldl countWord Map.empty xs\n\nresult = wordFreq [\"the\", \"cat\", \"the\", \"dog\", \"the\", \"cat\"]\n")
|
||||
|
||||
(hk-test
|
||||
"wordfreq.hs — \"the\" counted 3 times"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-wordfreq-source "main = Map.lookup \"the\" result\n")))
|
||||
(list "Just" 3))
|
||||
|
||||
(hk-test
|
||||
"wordfreq.hs — \"cat\" counted 2 times"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-wordfreq-source "main = Map.lookup \"cat\" result\n")))
|
||||
(list "Just" 2))
|
||||
|
||||
(hk-test
|
||||
"wordfreq.hs — \"dog\" counted 1 time"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-wordfreq-source "main = Map.lookup \"dog\" result\n")))
|
||||
(list "Just" 1))
|
||||
|
||||
(hk-test
|
||||
"wordfreq.hs — \"missing\" not present"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-wordfreq-source "main = Map.lookup \"missing\" result\n")))
|
||||
(list "Nothing"))
|
||||
|
||||
(hk-test
|
||||
"wordfreq.hs — Map.size = 3 unique words"
|
||||
(hk-deep-force (hk-run (str hk-wordfreq-source "main = Map.size result\n")))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"wordfreq.hs — findWithDefault for missing returns 0"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-wordfreq-source "main = Map.findWithDefault 0 \"absent\" result\n")))
|
||||
0)
|
||||
|
||||
(hk-test
|
||||
"wordfreq.hs — findWithDefault for present returns count"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-wordfreq-source "main = Map.findWithDefault 0 \"the\" result\n")))
|
||||
3)
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
127
lib/haskell/tests/records.sx
Normal file
127
lib/haskell/tests/records.sx
Normal file
@@ -0,0 +1,127 @@
|
||||
;; records.sx — Phase 14 record syntax tests.
|
||||
|
||||
(define
|
||||
hk-person-source
|
||||
"data Person = Person { name :: String, age :: Int }\n")
|
||||
|
||||
(define hk-pt-source "data Pt = Pt { x :: Int, y :: Int }\n")
|
||||
|
||||
;; ── Creation ────────────────────────────────────────────────
|
||||
(hk-test
|
||||
"creation: Person { name = \"a\", age = 1 } via accessor name"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-person-source
|
||||
"main = name (Person { name = \"alice\", age = 30 })")))
|
||||
"alice")
|
||||
|
||||
(hk-test
|
||||
"creation: source order doesn't matter (age first)"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-person-source "main = name (Person { age = 99, name = \"bob\" })")))
|
||||
"bob")
|
||||
|
||||
(hk-test
|
||||
"creation: age accessor returns the right field"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str hk-person-source "main = age (Person { age = 99, name = \"bob\" })")))
|
||||
99)
|
||||
|
||||
;; ── Accessors ──────────────────────────────────────────────
|
||||
(hk-test
|
||||
"accessor: x of Pt"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-pt-source "main = x (Pt { x = 7, y = 99 })")))
|
||||
7)
|
||||
|
||||
(hk-test
|
||||
"accessor: y of Pt"
|
||||
(hk-deep-force
|
||||
(hk-run (str hk-pt-source "main = y (Pt { x = 7, y = 99 })")))
|
||||
99)
|
||||
|
||||
;; ── Update — single field ──────────────────────────────────
|
||||
(hk-test
|
||||
"update one field: age changes"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-person-source
|
||||
"alice = Person { name = \"alice\", age = 30 }\nmain = age (alice { age = 31 })")))
|
||||
31)
|
||||
|
||||
(hk-test
|
||||
"update one field: name preserved"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-person-source
|
||||
"alice = Person { name = \"alice\", age = 30 }\nmain = name (alice { age = 31 })")))
|
||||
"alice")
|
||||
|
||||
;; ── Update — two fields ────────────────────────────────────
|
||||
(hk-test
|
||||
"update two fields: both changed"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-person-source
|
||||
"alice = Person { name = \"alice\", age = 30 }\nbob = alice { name = \"bob\", age = 50 }\nmain = age bob")))
|
||||
50)
|
||||
|
||||
(hk-test
|
||||
"update two fields: name takes new value"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-person-source
|
||||
"alice = Person { name = \"alice\", age = 30 }\nbob = alice { name = \"bob\", age = 50 }\nmain = name bob")))
|
||||
"bob")
|
||||
|
||||
;; ── Record patterns ────────────────────────────────────────
|
||||
(hk-test
|
||||
"case-alt record pattern: Pt { x = a }"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-pt-source
|
||||
"getX p = case p of Pt { x = a } -> a\nmain = getX (Pt { x = 7, y = 99 })")))
|
||||
7)
|
||||
|
||||
(hk-test
|
||||
"case-alt record pattern: multi-field bind"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-pt-source
|
||||
"sumPt p = case p of Pt { x = a, y = b } -> a + b\nmain = sumPt (Pt { x = 3, y = 4 })")))
|
||||
7)
|
||||
|
||||
(hk-test
|
||||
"fun-LHS record pattern"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
(str
|
||||
hk-person-source
|
||||
"getName (Person { name = n }) = n\nmain = getName (Person { name = \"alice\", age = 30 })")))
|
||||
"alice")
|
||||
|
||||
;; ── deriving Show on a record ───────────────────────────────
|
||||
(hk-test
|
||||
"deriving Show on a record produces positional output"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Person = Person { name :: String, age :: Int } deriving (Show)\nmain = show (Person { name = \"alice\", age = 30 })"))
|
||||
"Person \"alice\" 30")
|
||||
|
||||
(hk-test
|
||||
"deriving Show on Pt"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Pt = Pt { x :: Int, y :: Int } deriving (Show)\nmain = show (Pt { x = 3, y = 4 })"))
|
||||
"Pt 3 4")
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
119
lib/haskell/tests/set.sx
Normal file
119
lib/haskell/tests/set.sx
Normal file
@@ -0,0 +1,119 @@
|
||||
;; set.sx — Phase 12 Data.Set unit tests.
|
||||
|
||||
;; ── SX-level (direct hk-set-*) ───────────────────────────────
|
||||
(hk-test
|
||||
"hk-set-empty: size 0 + null"
|
||||
(list (hk-set-size hk-set-empty) (hk-set-null hk-set-empty))
|
||||
(list 0 true))
|
||||
|
||||
(hk-test
|
||||
"hk-set-singleton: member yes"
|
||||
(let
|
||||
((s (hk-set-singleton 5)))
|
||||
(list (hk-set-size s) (hk-set-member 5 s) (hk-set-member 99 s)))
|
||||
(list 1 true false))
|
||||
|
||||
(hk-test
|
||||
"hk-set-insert: idempotent"
|
||||
(let
|
||||
((s (hk-set-insert 1 (hk-set-insert 1 hk-set-empty))))
|
||||
(hk-set-size s))
|
||||
1)
|
||||
|
||||
(hk-test
|
||||
"hk-set-from-list: dedupes"
|
||||
(hk-set-to-asc-list (hk-set-from-list (list 3 1 4 1 5 9 2 6)))
|
||||
(list 1 2 3 4 5 6 9))
|
||||
|
||||
(hk-test
|
||||
"hk-set-delete: removes"
|
||||
(let
|
||||
((s (hk-set-from-list (list 1 2 3))))
|
||||
(hk-set-to-asc-list (hk-set-delete 2 s)))
|
||||
(list 1 3))
|
||||
|
||||
(hk-test
|
||||
"hk-set-union"
|
||||
(hk-set-to-asc-list
|
||||
(hk-set-union
|
||||
(hk-set-from-list (list 1 2 3))
|
||||
(hk-set-from-list (list 3 4 5))))
|
||||
(list 1 2 3 4 5))
|
||||
|
||||
(hk-test
|
||||
"hk-set-intersection"
|
||||
(hk-set-to-asc-list
|
||||
(hk-set-intersection
|
||||
(hk-set-from-list (list 1 2 3 4))
|
||||
(hk-set-from-list (list 3 4 5 6))))
|
||||
(list 3 4))
|
||||
|
||||
(hk-test
|
||||
"hk-set-difference"
|
||||
(hk-set-to-asc-list
|
||||
(hk-set-difference
|
||||
(hk-set-from-list (list 1 2 3 4))
|
||||
(hk-set-from-list (list 3 4 5))))
|
||||
(list 1 2))
|
||||
|
||||
(hk-test
|
||||
"hk-set-is-subset-of: yes"
|
||||
(hk-set-is-subset-of
|
||||
(hk-set-from-list (list 2 3))
|
||||
(hk-set-from-list (list 1 2 3 4)))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"hk-set-is-subset-of: no"
|
||||
(hk-set-is-subset-of
|
||||
(hk-set-from-list (list 5 6))
|
||||
(hk-set-from-list (list 1 2 3 4)))
|
||||
false)
|
||||
|
||||
(hk-test
|
||||
"hk-set-filter"
|
||||
(hk-set-to-asc-list
|
||||
(hk-set-filter (fn (k) (> k 2)) (hk-set-from-list (list 1 2 3 4 5))))
|
||||
(list 3 4 5))
|
||||
|
||||
(hk-test
|
||||
"hk-set-map"
|
||||
(hk-set-to-asc-list
|
||||
(hk-set-map (fn (k) (* k 10)) (hk-set-from-list (list 1 2 3))))
|
||||
(list 10 20 30))
|
||||
|
||||
(hk-test
|
||||
"hk-set-foldr: sum"
|
||||
(hk-set-foldr + 0 (hk-set-from-list (list 1 2 3 4 5)))
|
||||
15)
|
||||
|
||||
;; ── Haskell-level (Set.* via import wiring) ──────────────────
|
||||
(hk-test
|
||||
"Set.size after Set.insert chain"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.Set as Set\nmain = Set.size (Set.insert 3 (Set.insert 1 (Set.insert 2 Set.empty)))"))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"Set.member true"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.Set as Set\nmain = Set.member 5 (Set.insert 5 Set.empty)"))
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"Set.union via Haskell"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import Data.Set\nmain = Set.size (Set.union (Set.insert 1 Set.empty) (Set.insert 2 Set.empty))"))
|
||||
2)
|
||||
|
||||
(hk-test
|
||||
"Set.isSubsetOf via Haskell"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"import qualified Data.Set as S\nmain = S.isSubsetOf (S.insert 1 S.empty) (S.insert 2 (S.insert 1 S.empty))"))
|
||||
(list "True"))
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
140
lib/haskell/tests/show.sx
Normal file
140
lib/haskell/tests/show.sx
Normal file
@@ -0,0 +1,140 @@
|
||||
;; show.sx — tests for the Show / Read class plumbing.
|
||||
;;
|
||||
;; Covers Phase 8:
|
||||
;; - showsPrec / showParen / shows / showString stubs
|
||||
;; - Read class stubs (reads / readsPrec / read)
|
||||
;; - direct show coverage (Int, Bool, String, list, tuple, Maybe, ADT, ...)
|
||||
|
||||
;; ── ShowS / showsPrec / showParen stubs ──────────────────────
|
||||
(hk-test
|
||||
"shows: prepends show output"
|
||||
(hk-deep-force (hk-run "main = shows 5 \"abc\""))
|
||||
"5abc")
|
||||
|
||||
(hk-test
|
||||
"shows: works on True"
|
||||
(hk-deep-force (hk-run "main = shows True \"x\""))
|
||||
"Truex")
|
||||
|
||||
(hk-test
|
||||
"showString: prepends literal"
|
||||
(hk-deep-force (hk-run "main = showString \"hello\" \" world\""))
|
||||
"hello world")
|
||||
|
||||
(hk-test
|
||||
"showParen True: wraps inner output in parens"
|
||||
(hk-deep-force (hk-run "main = showParen True (showString \"inside\") \"\""))
|
||||
"(inside)")
|
||||
|
||||
(hk-test
|
||||
"showParen False: passes through unchanged"
|
||||
(hk-deep-force (hk-run "main = showParen False (showString \"inside\") \"\""))
|
||||
"inside")
|
||||
|
||||
(hk-test
|
||||
"showsPrec: prepends show output regardless of prec"
|
||||
(hk-deep-force (hk-run "main = showsPrec 11 42 \"end\""))
|
||||
"42end")
|
||||
|
||||
(hk-test
|
||||
"showParen + manual composition: build (Just 3)"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"buildJust3 s = showString \"Just \" (shows 3 s)\nmain = showParen True buildJust3 \"\""))
|
||||
"(Just 3)")
|
||||
|
||||
;; ── Read stubs ───────────────────────────────────────────────
|
||||
(hk-test
|
||||
"reads: stub returns empty list (null-check)"
|
||||
(hk-deep-force (hk-run "main = show (null (reads \"42\"))"))
|
||||
"True")
|
||||
|
||||
(hk-test
|
||||
"readsPrec: stub returns empty list"
|
||||
(hk-deep-force (hk-run "main = show (null (readsPrec 0 \"True\"))"))
|
||||
"True")
|
||||
|
||||
(hk-test
|
||||
"reads: type-checks in expression context (length)"
|
||||
(hk-deep-force (hk-run "main = show (length (reads \"abc\"))"))
|
||||
"0")
|
||||
|
||||
;; ── Direct `show` audit coverage ─────────────────────────────
|
||||
(hk-test "show Int" (hk-deep-force (hk-run "main = show 42")) "42")
|
||||
|
||||
(hk-test
|
||||
"show negative Int"
|
||||
(hk-deep-force (hk-run "main = show (negate 5)"))
|
||||
"-5")
|
||||
|
||||
(hk-test "show Bool True" (hk-deep-force (hk-run "main = show True")) "True")
|
||||
|
||||
(hk-test
|
||||
"show Bool False"
|
||||
(hk-deep-force (hk-run "main = show False"))
|
||||
"False")
|
||||
|
||||
(hk-test
|
||||
"show String quotes the value"
|
||||
(hk-deep-force (hk-run "main = show \"hello\""))
|
||||
"\"hello\"")
|
||||
|
||||
(hk-test
|
||||
"show list of Int"
|
||||
(hk-deep-force (hk-run "main = show [1,2,3]"))
|
||||
"[1,2,3]")
|
||||
|
||||
(hk-test
|
||||
"show empty list"
|
||||
(hk-deep-force (hk-run "main = show (drop 5 [1,2,3])"))
|
||||
"[]")
|
||||
|
||||
(hk-test
|
||||
"show pair tuple"
|
||||
(hk-deep-force (hk-run "main = show (1, True)"))
|
||||
"(1,True)")
|
||||
|
||||
(hk-test
|
||||
"show triple tuple"
|
||||
(hk-deep-force (hk-run "main = show (1, 2, 3)"))
|
||||
"(1,2,3)")
|
||||
|
||||
(hk-test
|
||||
"show Maybe Nothing"
|
||||
(hk-deep-force (hk-run "main = show Nothing"))
|
||||
"Nothing")
|
||||
|
||||
(hk-test
|
||||
"show Maybe Just"
|
||||
(hk-deep-force (hk-run "main = show (Just 3)"))
|
||||
"Just 3")
|
||||
|
||||
(hk-test
|
||||
"show nested Just wraps inner in parens"
|
||||
(hk-deep-force (hk-run "main = show (Just (Just 3))"))
|
||||
"Just (Just 3)")
|
||||
|
||||
(hk-test
|
||||
"show Just (negate 3) wraps negative in parens"
|
||||
(hk-deep-force (hk-run "main = show (Just (negate 3))"))
|
||||
"Just (-3)")
|
||||
|
||||
(hk-test
|
||||
"show custom nullary ADT"
|
||||
(hk-deep-force
|
||||
(hk-run "data Day = Mon | Tue | Wed deriving (Show)\nmain = show Tue"))
|
||||
"Tue")
|
||||
|
||||
(hk-test
|
||||
"show custom multi-constructor ADT"
|
||||
(hk-deep-force
|
||||
(hk-run
|
||||
"data Shape = Pt | Sq Int | Rect Int Int deriving (Show)\nmain = show (Rect 3 4)"))
|
||||
"Rect 3 4")
|
||||
|
||||
(hk-test
|
||||
"show list of Maybe wraps each element"
|
||||
(hk-deep-force (hk-run "main = show [Just 1, Nothing, Just 2]"))
|
||||
"[Just 1,Nothing,Just 2]")
|
||||
|
||||
{:fails hk-test-fails :pass hk-test-pass :fail hk-test-fail}
|
||||
@@ -37,11 +37,11 @@
|
||||
(hk-ts "show neg" "negate 7" "-7")
|
||||
(hk-ts "show bool T" "True" "True")
|
||||
(hk-ts "show bool F" "False" "False")
|
||||
(hk-ts "show list" "[1,2,3]" "[1, 2, 3]")
|
||||
(hk-ts "show Just" "Just 5" "(Just 5)")
|
||||
(hk-ts "show list" "[1,2,3]" "[1,2,3]")
|
||||
(hk-ts "show Just" "Just 5" "Just 5")
|
||||
(hk-ts "show Nothing" "Nothing" "Nothing")
|
||||
(hk-ts "show LT" "LT" "LT")
|
||||
(hk-ts "show tuple" "(1, True)" "(1, True)")
|
||||
(hk-ts "show tuple" "(1, True)" "(1,True)")
|
||||
|
||||
;; ── Num extras ───────────────────────────────────────────────
|
||||
(hk-test "signum pos" (hk-deep-force (hk-run "main = signum 5")) 1)
|
||||
@@ -59,13 +59,13 @@
|
||||
(hk-test
|
||||
"foldr cons"
|
||||
(hk-deep-force (hk-run "main = show (foldr (:) [] [1,2,3])"))
|
||||
"[1, 2, 3]")
|
||||
"[1,2,3]")
|
||||
|
||||
;; ── List ops ─────────────────────────────────────────────────
|
||||
(hk-test
|
||||
"reverse"
|
||||
(hk-deep-force (hk-run "main = show (reverse [1,2,3])"))
|
||||
"[3, 2, 1]")
|
||||
"[3,2,1]")
|
||||
(hk-test "null []" (hk-deep-force (hk-run "main = null []")) (list "True"))
|
||||
(hk-test
|
||||
"null xs"
|
||||
@@ -82,7 +82,7 @@
|
||||
(hk-test
|
||||
"zip"
|
||||
(hk-deep-force (hk-run "main = show (zip [1,2] [3,4])"))
|
||||
"[(1, 3), (2, 4)]")
|
||||
"[(1,3),(2,4)]")
|
||||
(hk-test "sum" (hk-deep-force (hk-run "main = sum [1,2,3,4,5]")) 15)
|
||||
(hk-test "product" (hk-deep-force (hk-run "main = product [1,2,3,4]")) 24)
|
||||
(hk-test "maximum" (hk-deep-force (hk-run "main = maximum [3,1,9,2]")) 9)
|
||||
@@ -112,7 +112,7 @@
|
||||
(hk-test
|
||||
"fmap list"
|
||||
(hk-deep-force (hk-run "main = show (fmap (+1) [1,2,3])"))
|
||||
"[2, 3, 4]")
|
||||
"[2,3,4]")
|
||||
|
||||
;; ── Monad / Applicative ──────────────────────────────────────
|
||||
(hk-test "return" (hk-deep-force (hk-run "main = return 7")) (list "IO" 7))
|
||||
@@ -134,7 +134,7 @@
|
||||
(hk-test
|
||||
"lookup hit"
|
||||
(hk-deep-force (hk-run "main = show (lookup 2 [(1,10),(2,20)])"))
|
||||
"(Just 20)")
|
||||
"Just 20")
|
||||
(hk-test
|
||||
"lookup miss"
|
||||
(hk-deep-force (hk-run "main = show (lookup 9 [(1,10)])"))
|
||||
|
||||
139
lib/haskell/tests/string-char.sx
Normal file
139
lib/haskell/tests/string-char.sx
Normal file
@@ -0,0 +1,139 @@
|
||||
;; String / Char tests — Phase 7 items 1-4.
|
||||
;;
|
||||
;; Covers:
|
||||
;; hk-str? / hk-str-head / hk-str-tail / hk-str-null? (runtime helpers)
|
||||
;; chr / ord / toUpper / toLower (builtins in eval)
|
||||
;; cons-pattern on strings via match.sx (":"-intercept)
|
||||
;; empty-list pattern on strings via match.sx ("[]"-intercept)
|
||||
|
||||
;; ── hk-str? predicate ────────────────────────────────────────────────────
|
||||
(hk-test "hk-str? native string" (hk-str? "hello") true)
|
||||
|
||||
(hk-test "hk-str? empty string" (hk-str? "") true)
|
||||
|
||||
(hk-test "hk-str? view dict" (hk-str? {:hk-off 1 :hk-str "hi"}) true)
|
||||
|
||||
(hk-test "hk-str? rejects number" (hk-str? 42) false)
|
||||
|
||||
;; ── hk-str-null? predicate ───────────────────────────────────────────────
|
||||
(hk-test "hk-str-null? empty string" (hk-str-null? "") true)
|
||||
|
||||
(hk-test "hk-str-null? non-empty" (hk-str-null? "a") false)
|
||||
|
||||
(hk-test "hk-str-null? exhausted view" (hk-str-null? {:hk-off 2 :hk-str "hi"}) true)
|
||||
|
||||
(hk-test "hk-str-null? live view" (hk-str-null? {:hk-off 1 :hk-str "hi"}) false)
|
||||
|
||||
;; ── hk-str-head ──────────────────────────────────────────────────────────
|
||||
(hk-test "hk-str-head native string" (hk-str-head "hello") 104)
|
||||
|
||||
(hk-test "hk-str-head view at offset" (hk-str-head {:hk-off 1 :hk-str "hello"}) 101)
|
||||
|
||||
;; ── hk-str-tail ──────────────────────────────────────────────────────────
|
||||
(hk-test "hk-str-tail of single char is nil" (hk-str-tail "h") (list "[]"))
|
||||
|
||||
(hk-test
|
||||
"hk-str-tail of two-char string is live view"
|
||||
(hk-str-null? (hk-str-tail "hi"))
|
||||
false)
|
||||
|
||||
(hk-test
|
||||
"hk-str-tail head of tail of hi is i"
|
||||
(hk-str-head (hk-str-tail "hi"))
|
||||
105)
|
||||
|
||||
;; ── chr / ord ────────────────────────────────────────────────────────────
|
||||
(hk-test "chr 65 = A" (hk-eval-expr-source "chr 65") "A")
|
||||
|
||||
(hk-test "chr 104 = h" (hk-eval-expr-source "chr 104") "h")
|
||||
|
||||
(hk-test "ord char literal 'A' = 65" (hk-eval-expr-source "ord 'A'") 65)
|
||||
|
||||
(hk-test "ord char literal 'a' = 97" (hk-eval-expr-source "ord 'a'") 97)
|
||||
|
||||
(hk-test
|
||||
"ord of head string = char code"
|
||||
(hk-eval-expr-source "ord (head \"hello\")")
|
||||
104)
|
||||
|
||||
;; ── toUpper / toLower ────────────────────────────────────────────────────
|
||||
(hk-test "toUpper 97 = 65 (a->A)" (hk-eval-expr-source "toUpper 97") 65)
|
||||
|
||||
(hk-test
|
||||
"toUpper 65 = 65 (already upper)"
|
||||
(hk-eval-expr-source "toUpper 65")
|
||||
65)
|
||||
|
||||
(hk-test
|
||||
"toUpper 48 = 48 (digit unchanged)"
|
||||
(hk-eval-expr-source "toUpper 48")
|
||||
48)
|
||||
|
||||
(hk-test "toLower 65 = 97 (A->a)" (hk-eval-expr-source "toLower 65") 97)
|
||||
|
||||
(hk-test
|
||||
"toLower 97 = 97 (already lower)"
|
||||
(hk-eval-expr-source "toLower 97")
|
||||
97)
|
||||
|
||||
(hk-test
|
||||
"toLower 48 = 48 (digit unchanged)"
|
||||
(hk-eval-expr-source "toLower 48")
|
||||
48)
|
||||
|
||||
;; ── Pattern matching on strings ──────────────────────────────────────────
|
||||
(hk-test
|
||||
"cons pattern: head of hello = 104"
|
||||
(hk-eval-expr-source "case \"hello\" of { (x:_) -> x }")
|
||||
104)
|
||||
|
||||
(hk-test
|
||||
"cons pattern: tail is traversable"
|
||||
(hk-eval-expr-source "case \"hi\" of { (_:xs) -> case xs of { (y:_) -> y } }")
|
||||
105)
|
||||
|
||||
(hk-test
|
||||
"empty list pattern matches empty string"
|
||||
(hk-eval-expr-source "case \"\" of { [] -> True; _ -> False }")
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"empty list pattern fails on non-empty"
|
||||
(hk-eval-expr-source "case \"a\" of { [] -> True; _ -> False }")
|
||||
(list "False"))
|
||||
|
||||
(hk-test
|
||||
"cons pattern fails on empty string"
|
||||
(hk-eval-expr-source "case \"\" of { (_:_) -> True; _ -> False }")
|
||||
(list "False"))
|
||||
|
||||
;; ── Haskell programs using string traversal ──────────────────────────────
|
||||
(hk-test
|
||||
"null prelude on empty string"
|
||||
(hk-eval-expr-source "null \"\"")
|
||||
(list "True"))
|
||||
|
||||
(hk-test
|
||||
"null prelude on non-empty string"
|
||||
(hk-eval-expr-source "null \"abc\"")
|
||||
(list "False"))
|
||||
|
||||
(hk-test
|
||||
"length of string via cons recursion"
|
||||
(hk-eval-expr-source "let { f [] = 0; f (_:xs) = 1 + f xs } in f \"hello\"")
|
||||
5)
|
||||
|
||||
(hk-test
|
||||
"map ord over string gives char codes"
|
||||
(hk-deep-force (hk-eval-expr-source "map ord \"abc\""))
|
||||
(list ":" 97 (list ":" 98 (list ":" 99 (list "[]")))))
|
||||
|
||||
(hk-test
|
||||
"map toUpper over char codes then chr"
|
||||
(hk-eval-expr-source "chr (toUpper (ord (head \"abc\")))")
|
||||
"A")
|
||||
|
||||
(hk-test
|
||||
"head then ord using prelude head"
|
||||
(hk-eval-expr-source "ord (head \"hello\")")
|
||||
104)
|
||||
@@ -226,6 +226,28 @@
|
||||
value)
|
||||
(list (quote set!) (hs-to-sx target) value)))))))
|
||||
(true (list (quote set!) (hs-to-sx target) value)))))))
|
||||
;; Throttle/debounce extraction state — module-level so they don't get
|
||||
;; redefined on every emit-on call (which was causing JIT churn). Set
|
||||
;; via _strip-throttle-debounce at the start of each emit-on, used in
|
||||
;; the handler-build step inside scan-on.
|
||||
(define _throttle-ms nil)
|
||||
(define _debounce-ms nil)
|
||||
(define
|
||||
_strip-throttle-debounce
|
||||
(fn
|
||||
(lst)
|
||||
(cond
|
||||
((<= (len lst) 1) lst)
|
||||
((= (first lst) :throttle)
|
||||
(do
|
||||
(set! _throttle-ms (nth lst 1))
|
||||
(_strip-throttle-debounce (rest (rest lst)))))
|
||||
((= (first lst) :debounce)
|
||||
(do
|
||||
(set! _debounce-ms (nth lst 1))
|
||||
(_strip-throttle-debounce (rest (rest lst)))))
|
||||
(true
|
||||
(cons (first lst) (_strip-throttle-debounce (rest lst)))))))
|
||||
(define
|
||||
emit-on
|
||||
(fn
|
||||
@@ -234,6 +256,8 @@
|
||||
((parts (rest ast)))
|
||||
(let
|
||||
((event-name (first parts)))
|
||||
(set! _throttle-ms nil)
|
||||
(set! _debounce-ms nil)
|
||||
(define
|
||||
scan-on
|
||||
(fn
|
||||
@@ -266,6 +290,13 @@
|
||||
((wrapped-body (if catch-info (let ((var (make-symbol (nth catch-info 0))) (catch-body (hs-to-sx (nth catch-info 1)))) (if finally-info (list (quote let) (list (list (quote __hs-exc) nil) (list (quote __hs-reraise) false)) (list (quote do) (list (quote guard) (list var (list true (list (quote let) (list (list var (list (quote host-hs-normalize-exc) var))) (list (quote guard) (list (quote __inner-exc) (list true (list (quote do) (list (quote set!) (quote __hs-exc) (quote __inner-exc)) (list (quote set!) (quote __hs-reraise) true)))) catch-body)))) compiled-body) (hs-to-sx finally-info) (list (quote when) (quote __hs-reraise) (list (quote raise) (quote __hs-exc))))) (list (quote let) (list (list (quote __hs-exc) nil) (list (quote __hs-reraise) false)) (list (quote do) (list (quote guard) (list var (list true (list (quote let) (list (list var (list (quote host-hs-normalize-exc) var))) (list (quote guard) (list (quote __inner-exc) (list true (list (quote do) (list (quote set!) (quote __hs-exc) (quote __inner-exc)) (list (quote set!) (quote __hs-reraise) true)))) catch-body)))) compiled-body) (list (quote when) (quote __hs-reraise) (list (quote raise) (quote __hs-exc))))))) (if finally-info (list (quote do) compiled-body (hs-to-sx finally-info)) compiled-body))))
|
||||
(let
|
||||
((handler (let ((uses-the-result? (fn (expr) (cond ((= expr (quote the-result)) true) ((list? expr) (some (fn (x) (uses-the-result? x)) expr)) (true false))))) (let ((base-handler (list (quote fn) (list (quote event)) (if (uses-the-result? wrapped-body) (list (quote let) (list (list (quote the-result) nil)) wrapped-body) wrapped-body)))) (if count-filter-info (let ((mn (get count-filter-info "min")) (mx (get count-filter-info "max"))) (list (quote let) (list (list (quote __hs-count) 0)) (list (quote fn) (list (quote event)) (list (quote begin) (list (quote set!) (quote __hs-count) (list (quote +) (quote __hs-count) 1)) (list (quote when) (if (= mx -1) (list (quote >=) (quote __hs-count) mn) (list (quote and) (list (quote >=) (quote __hs-count) mn) (list (quote <=) (quote __hs-count) mx))) (nth base-handler 2)))))) base-handler)))))
|
||||
(let
|
||||
((handler (cond
|
||||
(_throttle-ms
|
||||
(list (quote hs-throttle!) handler (hs-to-sx _throttle-ms)))
|
||||
(_debounce-ms
|
||||
(list (quote hs-debounce!) handler (hs-to-sx _debounce-ms)))
|
||||
(true handler))))
|
||||
(let
|
||||
((on-call (if every? (list (quote hs-on-every) target event-name handler) (list (quote hs-on) target event-name handler))))
|
||||
(cond
|
||||
@@ -325,7 +356,7 @@
|
||||
(first pair)
|
||||
handler))
|
||||
or-sources)))
|
||||
on-call)))))))))))))
|
||||
on-call))))))))))))))
|
||||
((= (first items) :from)
|
||||
(scan-on
|
||||
(rest (rest items))
|
||||
@@ -469,7 +500,7 @@
|
||||
count-filter-info
|
||||
elsewhere?
|
||||
or-sources)))))
|
||||
(scan-on (rest parts) nil nil false nil nil nil nil nil false nil)))))
|
||||
(scan-on (_strip-throttle-debounce (rest parts)) nil nil false nil nil nil nil nil false nil)))))
|
||||
(define
|
||||
emit-send
|
||||
(fn
|
||||
@@ -2490,6 +2521,15 @@
|
||||
(quote fn)
|
||||
(list (quote it))
|
||||
(hs-to-sx body))))
|
||||
((and (list? expr) (= (first expr) (quote attr)))
|
||||
(list
|
||||
(quote hs-attr-watch!)
|
||||
(hs-to-sx (nth expr 2))
|
||||
(nth expr 1)
|
||||
(list
|
||||
(quote fn)
|
||||
(list (quote it))
|
||||
(hs-to-sx body))))
|
||||
(true nil))))
|
||||
((= head (quote init))
|
||||
(list
|
||||
|
||||
@@ -1358,7 +1358,17 @@
|
||||
cls
|
||||
(first extra-classes)
|
||||
tgt))
|
||||
((match-kw "for")
|
||||
((and
|
||||
(= (tp-type) "keyword") (= (tp-val) "for")
|
||||
;; Only consume 'for' as a duration clause if the next
|
||||
;; token is NOT '<ident> in ...' — that pattern is a
|
||||
;; for-in loop, not a toggle duration.
|
||||
(not
|
||||
(and
|
||||
(> (len tokens) (+ p 2))
|
||||
(= (get (nth tokens (+ p 1)) "type") "ident")
|
||||
(= (get (nth tokens (+ p 2)) "value") "in")))
|
||||
(do (adv!) true))
|
||||
(let
|
||||
((dur (parse-expr)))
|
||||
(list (quote toggle-class-for) cls tgt dur)))
|
||||
@@ -3090,7 +3100,17 @@
|
||||
(= (tp-val) "queue"))
|
||||
(do (adv!) (adv!)))
|
||||
(let
|
||||
((every? (match-kw "every")))
|
||||
((every? (match-kw "every"))
|
||||
(throttle-ms nil)
|
||||
(debounce-ms nil))
|
||||
;; 'throttled at <duration>' / 'debounced at <duration>'
|
||||
;; — parsed as handler modifiers, captured as :throttle / :debounce parts.
|
||||
(when (and (= (tp-type) "ident") (= (tp-val) "throttled"))
|
||||
(adv!)
|
||||
(when (match-kw "at") (set! throttle-ms (parse-expr))))
|
||||
(when (and (= (tp-type) "ident") (= (tp-val) "debounced"))
|
||||
(adv!)
|
||||
(when (match-kw "at") (set! debounce-ms (parse-expr))))
|
||||
(let
|
||||
((having (if (or h-margin h-threshold) (dict "margin" h-margin "threshold" h-threshold) nil)))
|
||||
(let
|
||||
@@ -3105,6 +3125,10 @@
|
||||
(match-kw "end")
|
||||
(let
|
||||
((parts (list (quote on) event-name)))
|
||||
(let
|
||||
((parts (if throttle-ms (append parts (list :throttle throttle-ms)) parts)))
|
||||
(let
|
||||
((parts (if debounce-ms (append parts (list :debounce debounce-ms)) parts)))
|
||||
(let
|
||||
((parts (if every? (append parts (list :every true)) parts)))
|
||||
(let
|
||||
@@ -3127,7 +3151,7 @@
|
||||
((parts (if finally-clause (append parts (list :finally finally-clause)) parts)))
|
||||
(let
|
||||
((parts (append parts (list (if (> (len event-vars) 0) (cons (quote do) (append (map (fn (nm) (list (quote ref) nm)) event-vars) (if (and (list? body) (= (first body) (quote do))) (rest body) (list body)))) body)))))
|
||||
parts))))))))))))))))))))))))))
|
||||
parts))))))))))))))))))))))))))))
|
||||
(define
|
||||
parse-init-feat
|
||||
(fn
|
||||
@@ -3177,6 +3201,7 @@
|
||||
(or
|
||||
(= (tp-type) "hat")
|
||||
(= (tp-type) "local")
|
||||
(= (tp-type) "attr")
|
||||
(and (= (tp-type) "keyword") (= (tp-val) "dom")))
|
||||
(let
|
||||
((expr (parse-expr)))
|
||||
|
||||
@@ -12,6 +12,29 @@
|
||||
|
||||
;; Register an event listener. Returns unlisten function.
|
||||
;; (hs-on target event-name handler) → unlisten-fn
|
||||
(begin
|
||||
(define _hs-config-log-all false)
|
||||
(define _hs-log-captured (list))
|
||||
(define
|
||||
hs-set-log-all!
|
||||
(fn (flag) (set! _hs-config-log-all (if flag true false))))
|
||||
(define hs-get-log-captured (fn () _hs-log-captured))
|
||||
(define
|
||||
hs-clear-log-captured!
|
||||
(fn () (begin (set! _hs-log-captured (list)) nil)))
|
||||
(define
|
||||
hs-log-event!
|
||||
(fn
|
||||
(msg)
|
||||
(when
|
||||
_hs-config-log-all
|
||||
(begin
|
||||
(set! _hs-log-captured (append _hs-log-captured (list msg)))
|
||||
(host-call (host-global "console") "log" msg)
|
||||
nil)))))
|
||||
|
||||
;; Run an initializer function immediately.
|
||||
;; (hs-init thunk) — called at element boot time
|
||||
(define
|
||||
hs-each
|
||||
(fn
|
||||
@@ -22,17 +45,52 @@
|
||||
;; (hs-init thunk) — called at element boot time
|
||||
(define meta (host-new "Object"))
|
||||
|
||||
;; Run an initializer function immediately.
|
||||
;; (hs-init thunk) — called at element boot time
|
||||
(define
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; ── Async / timing ──────────────────────────────────────────────
|
||||
|
||||
;; Wait for a duration in milliseconds.
|
||||
;; In hyperscript, wait is async-transparent — execution pauses.
|
||||
;; Here we use perform/IO suspension for true pause semantics.
|
||||
(define
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; Throttle: drops events that arrive within the window. First event fires
|
||||
;; immediately; subsequent events within `ms` of the previous fire are dropped.
|
||||
;; Returns a wrapped handler suitable for hs-on / hs-on-every.
|
||||
(define
|
||||
hs-throttle!
|
||||
(fn
|
||||
(handler ms)
|
||||
(let
|
||||
((__hs-last-fire 0))
|
||||
(fn
|
||||
(event)
|
||||
(let
|
||||
((__hs-now (host-call (host-global "Date") "now")))
|
||||
(when
|
||||
(>= (- __hs-now __hs-last-fire) ms)
|
||||
(set! __hs-last-fire __hs-now)
|
||||
(handler event)))))))
|
||||
|
||||
;; Debounce: waits until `ms` has elapsed since the last event before firing.
|
||||
;; In our synchronous test mock no time passes, so the timer fires immediately
|
||||
;; via setTimeout(_, 0); the wrapped handler still gets called once per burst.
|
||||
(define
|
||||
hs-debounce!
|
||||
(fn
|
||||
(handler ms)
|
||||
(let
|
||||
((__hs-timer nil))
|
||||
(fn
|
||||
(event)
|
||||
(when __hs-timer (host-call (host-global "window") "clearTimeout" __hs-timer))
|
||||
(set! __hs-timer
|
||||
(host-call (host-global "window") "setTimeout"
|
||||
(host-new-function (list "ev") "return arguments[0](arguments[1]);")
|
||||
ms handler event))))))
|
||||
|
||||
;; Wait for a DOM event on a target.
|
||||
;; (hs-wait-for target event-name) — suspends until event fires
|
||||
(define
|
||||
_hs-on-caller
|
||||
(let
|
||||
@@ -45,8 +103,7 @@
|
||||
(host-set! _ctx "meta" _m)
|
||||
_ctx)))
|
||||
|
||||
;; Wait for a DOM event on a target.
|
||||
;; (hs-wait-for target event-name) — suspends until event fires
|
||||
;; Wait for CSS transitions/animations to settle on an element.
|
||||
(define
|
||||
hs-on
|
||||
(fn
|
||||
@@ -66,14 +123,14 @@
|
||||
(append prev (list unlisten)))
|
||||
unlisten))))))
|
||||
|
||||
;; Wait for CSS transitions/animations to settle on an element.
|
||||
;; ── Class manipulation ──────────────────────────────────────────
|
||||
|
||||
;; Toggle a single class on an element.
|
||||
(define
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; ── Class manipulation ──────────────────────────────────────────
|
||||
|
||||
;; Toggle a single class on an element.
|
||||
;; Toggle between two classes — exactly one is active at a time.
|
||||
(define
|
||||
hs-on-intersection-attach!
|
||||
(fn
|
||||
@@ -89,7 +146,8 @@
|
||||
(host-call observer "observe" target)
|
||||
observer)))))
|
||||
|
||||
;; Toggle between two classes — exactly one is active at a time.
|
||||
;; Take a class from siblings — add to target, remove from others.
|
||||
;; (hs-take! target cls) — like radio button class behavior
|
||||
(define
|
||||
hs-on-mutation-attach!
|
||||
(fn
|
||||
@@ -110,19 +168,18 @@
|
||||
(host-call observer "observe" target opts)
|
||||
observer))))))
|
||||
|
||||
;; Take a class from siblings — add to target, remove from others.
|
||||
;; (hs-take! target cls) — like radio button class behavior
|
||||
(define hs-init (fn (thunk) (thunk)))
|
||||
|
||||
;; ── DOM insertion ───────────────────────────────────────────────
|
||||
|
||||
;; Put content at a position relative to a target.
|
||||
;; pos: "into" | "before" | "after"
|
||||
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
||||
(define hs-init (fn (thunk) (thunk)))
|
||||
|
||||
;; ── Navigation / traversal ──────────────────────────────────────
|
||||
|
||||
;; Navigate to a URL.
|
||||
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
||||
|
||||
;; Find next sibling matching a selector (or any sibling).
|
||||
(begin
|
||||
(define
|
||||
hs-wait-for
|
||||
@@ -135,7 +192,7 @@
|
||||
(target event-name timeout-ms)
|
||||
(perform (list (quote io-wait-event) target event-name timeout-ms)))))
|
||||
|
||||
;; Find next sibling matching a selector (or any sibling).
|
||||
;; Find previous sibling matching a selector.
|
||||
(define
|
||||
hs-settle
|
||||
(fn
|
||||
@@ -143,7 +200,7 @@
|
||||
(hs-null-raise! target)
|
||||
(when (not (nil? target)) (perform (list (quote io-settle) target)))))
|
||||
|
||||
;; Find previous sibling matching a selector.
|
||||
;; First element matching selector within a scope.
|
||||
(define
|
||||
hs-toggle-class!
|
||||
(fn
|
||||
@@ -153,7 +210,7 @@
|
||||
(not (nil? target))
|
||||
(host-call (host-get target "classList") "toggle" cls))))
|
||||
|
||||
;; First element matching selector within a scope.
|
||||
;; Last element matching selector.
|
||||
(define
|
||||
hs-toggle-var-cycle!
|
||||
(fn
|
||||
@@ -175,7 +232,7 @@
|
||||
var-name
|
||||
(if (= idx -1) (first values) (nth values (mod (+ idx 1) n))))))))
|
||||
|
||||
;; Last element matching selector.
|
||||
;; First/last within a specific scope.
|
||||
(define
|
||||
hs-toggle-between!
|
||||
(fn
|
||||
@@ -188,7 +245,6 @@
|
||||
(do (dom-remove-class target cls1) (dom-add-class target cls2))
|
||||
(do (dom-remove-class target cls2) (dom-add-class target cls1))))))
|
||||
|
||||
;; First/last within a specific scope.
|
||||
(define
|
||||
hs-toggle-style!
|
||||
(fn
|
||||
@@ -212,6 +268,9 @@
|
||||
(dom-set-style target prop "hidden")
|
||||
(dom-set-style target prop "")))))))
|
||||
|
||||
;; ── Iteration ───────────────────────────────────────────────────
|
||||
|
||||
;; Repeat a thunk N times.
|
||||
(define
|
||||
hs-toggle-style-between!
|
||||
(fn
|
||||
@@ -223,9 +282,7 @@
|
||||
(dom-set-style target prop val2)
|
||||
(dom-set-style target prop val1)))))
|
||||
|
||||
;; ── Iteration ───────────────────────────────────────────────────
|
||||
|
||||
;; Repeat a thunk N times.
|
||||
;; Repeat forever (until break — relies on exception/continuation).
|
||||
(define
|
||||
hs-toggle-style-cycle!
|
||||
(fn
|
||||
@@ -246,7 +303,10 @@
|
||||
(true (find-next (rest remaining))))))
|
||||
(dom-set-style target prop (find-next vals)))))
|
||||
|
||||
;; Repeat forever (until break — relies on exception/continuation).
|
||||
;; ── Fetch ───────────────────────────────────────────────────────
|
||||
|
||||
;; Fetch a URL, parse response according to format.
|
||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||
(define
|
||||
hs-take!
|
||||
(fn
|
||||
@@ -269,8 +329,7 @@
|
||||
(when with-cls (dom-remove-class target with-cls))))
|
||||
(let
|
||||
((attr-val (if (> (len extra) 0) (first extra) nil))
|
||||
(with-val
|
||||
(if (> (len extra) 1) (nth extra 1) nil)))
|
||||
(with-val (if (> (len extra) 1) (nth extra 1) nil)))
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
@@ -287,10 +346,10 @@
|
||||
(dom-set-attr target name attr-val)
|
||||
(dom-set-attr target name ""))))))))
|
||||
|
||||
;; ── Fetch ───────────────────────────────────────────────────────
|
||||
;; ── Type coercion ───────────────────────────────────────────────
|
||||
|
||||
;; Fetch a URL, parse response according to format.
|
||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||
;; Coerce a value to a type by name.
|
||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||
(begin
|
||||
(define
|
||||
hs-element?
|
||||
@@ -447,10 +506,10 @@
|
||||
(dom-insert-adjacent-html target "beforeend" value)
|
||||
(hs-boot-subtree! target)))))))))))
|
||||
|
||||
;; ── Type coercion ───────────────────────────────────────────────
|
||||
;; ── Object creation ─────────────────────────────────────────────
|
||||
|
||||
;; Coerce a value to a type by name.
|
||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||
;; Make a new object of a given type.
|
||||
;; (hs-make type-name) — creates empty object/collection
|
||||
(define
|
||||
hs-add-to!
|
||||
(fn
|
||||
@@ -464,10 +523,11 @@
|
||||
((hs-is-set? target) (do (host-call target "add" value) target))
|
||||
(true (do (host-call target "push" value) target)))))
|
||||
|
||||
;; ── Object creation ─────────────────────────────────────────────
|
||||
;; ── Behavior installation ───────────────────────────────────────
|
||||
|
||||
;; Make a new object of a given type.
|
||||
;; (hs-make type-name) — creates empty object/collection
|
||||
;; Install a behavior on an element.
|
||||
;; A behavior is a function that takes (me ...params) and sets up features.
|
||||
;; (hs-install behavior-fn me ...args)
|
||||
(define
|
||||
hs-remove-from!
|
||||
(fn
|
||||
@@ -477,11 +537,10 @@
|
||||
((hs-is-set? target) (do (host-call target "delete" value) target))
|
||||
(true (host-call target "splice" (host-call target "indexOf" value) 1)))))
|
||||
|
||||
;; ── Behavior installation ───────────────────────────────────────
|
||||
;; ── Measurement ─────────────────────────────────────────────────
|
||||
|
||||
;; Install a behavior on an element.
|
||||
;; A behavior is a function that takes (me ...params) and sets up features.
|
||||
;; (hs-install behavior-fn me ...args)
|
||||
;; Measure an element's bounding rect, store as local variables.
|
||||
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
||||
(define
|
||||
hs-splice-at!
|
||||
(fn
|
||||
@@ -494,10 +553,7 @@
|
||||
((i (if (< idx 0) (+ n idx) idx)))
|
||||
(cond
|
||||
((or (< i 0) (>= i n)) target)
|
||||
(true
|
||||
(concat
|
||||
(slice target 0 i)
|
||||
(slice target (+ i 1) n))))))
|
||||
(true (concat (slice target 0 i) (slice target (+ i 1) n))))))
|
||||
(do
|
||||
(when
|
||||
target
|
||||
@@ -508,10 +564,10 @@
|
||||
(host-call target "splice" i 1))))
|
||||
target))))
|
||||
|
||||
;; ── Measurement ─────────────────────────────────────────────────
|
||||
|
||||
;; Measure an element's bounding rect, store as local variables.
|
||||
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
||||
;; Return the current text selection as a string. In the browser this is
|
||||
;; `window.getSelection().toString()`. In the mock test runner, a test
|
||||
;; setup stashes the desired selection text at `window.__test_selection`
|
||||
;; and the fallback path returns that so tests can assert on the result.
|
||||
(define
|
||||
hs-index
|
||||
(fn
|
||||
@@ -523,10 +579,11 @@
|
||||
((string? obj) (nth obj key))
|
||||
(true (host-get obj key)))))
|
||||
|
||||
;; Return the current text selection as a string. In the browser this is
|
||||
;; `window.getSelection().toString()`. In the mock test runner, a test
|
||||
;; setup stashes the desired selection text at `window.__test_selection`
|
||||
;; and the fallback path returns that so tests can assert on the result.
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
(define
|
||||
hs-put-at!
|
||||
(fn
|
||||
@@ -548,11 +605,6 @@
|
||||
((= pos "start") (host-call target "unshift" value)))
|
||||
target)))))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
(define
|
||||
hs-dict-without
|
||||
(fn
|
||||
@@ -589,6 +641,11 @@
|
||||
((w (host-global "window")))
|
||||
(if w (host-call w "prompt" msg) nil))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
(define
|
||||
hs-answer
|
||||
(fn
|
||||
@@ -597,11 +654,6 @@
|
||||
((w (host-global "window")))
|
||||
(if w (if (host-call w "confirm" msg) yes-val no-val) no-val))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
(define
|
||||
hs-answer-alert
|
||||
(fn
|
||||
@@ -662,6 +714,10 @@
|
||||
(if (nil? sel) "" (host-call sel "toString" (list))))
|
||||
stash)))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(define
|
||||
hs-reset!
|
||||
(fn
|
||||
@@ -708,10 +764,6 @@
|
||||
(when default-val (dom-set-prop target "value" default-val)))))
|
||||
(true nil)))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(define
|
||||
hs-next
|
||||
(fn
|
||||
@@ -730,7 +782,8 @@
|
||||
((dom-matches? el sel) el)
|
||||
(true (find-next (dom-next-sibling el))))))
|
||||
(find-next sibling)))))
|
||||
|
||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||
;; Property access — dot notation and .length
|
||||
(define
|
||||
hs-previous
|
||||
(fn
|
||||
@@ -749,10 +802,9 @@
|
||||
((dom-matches? el sel) el)
|
||||
(true (find-prev (dom-get-prop el "previousElementSibling"))))))
|
||||
(find-prev sibling)))))
|
||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||
;; Property access — dot notation and .length
|
||||
(define _hs-last-query-sel nil)
|
||||
;; DOM query stub — sandbox returns empty list
|
||||
(define _hs-last-query-sel nil)
|
||||
;; Method dispatch — obj.method(args)
|
||||
(define
|
||||
hs-null-raise!
|
||||
(fn
|
||||
@@ -763,7 +815,9 @@
|
||||
((msg (str "'" (or (host-get (host-global "window") "_hs_last_query_sel") "target") "' is null")))
|
||||
(host-set! (host-global "window") "_hs_null_error" msg)
|
||||
(guard (_null-e (true nil)) (raise msg))))))
|
||||
;; Method dispatch — obj.method(args)
|
||||
|
||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||
;; beep! — debug logging, returns value unchanged
|
||||
(define
|
||||
hs-empty-raise!
|
||||
(fn
|
||||
@@ -777,9 +831,7 @@
|
||||
((msg (str "'" (or (host-get (host-global "window") "_hs_last_query_sel") "target") "' is null")))
|
||||
(host-set! (host-global "window") "_hs_null_error" msg)
|
||||
(guard (_null-e (true nil)) (raise msg))))))
|
||||
|
||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||
;; beep! — debug logging, returns value unchanged
|
||||
;; Property-based is — check obj.key truthiness
|
||||
(define
|
||||
hs-query-all-checked
|
||||
(fn
|
||||
@@ -787,14 +839,14 @@
|
||||
(let
|
||||
((result (hs-query-all sel)))
|
||||
(do (hs-empty-raise! result) result))))
|
||||
;; Property-based is — check obj.key truthiness
|
||||
;; Array slicing (inclusive both ends)
|
||||
(define
|
||||
hs-dispatch!
|
||||
(fn
|
||||
(target event detail)
|
||||
(hs-null-raise! target)
|
||||
(when (not (nil? target)) (dom-dispatch target event detail))))
|
||||
;; Array slicing (inclusive both ends)
|
||||
;; Collection: sorted by
|
||||
(define
|
||||
hs-query-all
|
||||
(fn
|
||||
@@ -802,7 +854,7 @@
|
||||
(do
|
||||
(host-set! (host-global "window") "_hs_last_query_sel" sel)
|
||||
(dom-query-all (dom-document) sel))))
|
||||
;; Collection: sorted by
|
||||
;; Collection: sorted by descending
|
||||
(define
|
||||
hs-query-all-in
|
||||
(fn
|
||||
@@ -811,17 +863,17 @@
|
||||
(nil? target)
|
||||
(hs-query-all sel)
|
||||
(host-call target "querySelectorAll" sel))))
|
||||
;; Collection: sorted by descending
|
||||
;; Collection: split by
|
||||
(define
|
||||
hs-list-set
|
||||
(fn
|
||||
(lst idx val)
|
||||
(append (take lst idx) (cons val (drop lst (+ idx 1))))))
|
||||
;; Collection: split by
|
||||
;; Collection: joined by
|
||||
(define
|
||||
hs-to-number
|
||||
(fn (v) (if (number? v) v (or (parse-number (str v)) 0))))
|
||||
;; Collection: joined by
|
||||
|
||||
(define
|
||||
hs-query-first
|
||||
(fn
|
||||
@@ -951,7 +1003,7 @@
|
||||
((= (str ex) "hs-continue") (do-loop (rest remaining)))
|
||||
(true (raise ex))))))))
|
||||
(do-loop items))))
|
||||
|
||||
;; Collection: joined by
|
||||
(begin
|
||||
(define
|
||||
hs-append
|
||||
@@ -992,7 +1044,7 @@
|
||||
(host-get value "outerHTML")
|
||||
(str value))))
|
||||
(true nil)))))
|
||||
;; Collection: joined by
|
||||
|
||||
(define
|
||||
hs-sender
|
||||
(fn
|
||||
@@ -1084,6 +1136,7 @@
|
||||
(hs-host-to-sx (perform (list "io-parse-json" raw))))
|
||||
((= fmt "number")
|
||||
(hs-to-number (perform (list "io-parse-text" raw))))
|
||||
((= fmt "html") (perform (list "io-parse-html" raw)))
|
||||
(true (perform (list "io-parse-text" raw)))))))))
|
||||
|
||||
(define hs-fetch (fn (url format) (hs-fetch-impl url format false)))
|
||||
@@ -1623,14 +1676,10 @@
|
||||
((ch (substring sel i (+ i 1))))
|
||||
(cond
|
||||
((= ch ".")
|
||||
(do
|
||||
(flush!)
|
||||
(set! mode "class")
|
||||
(walk (+ i 1))))
|
||||
(do (flush!) (set! mode "class") (walk (+ i 1))))
|
||||
((= ch "#")
|
||||
(do (flush!) (set! mode "id") (walk (+ i 1))))
|
||||
(true
|
||||
(do (set! cur (str cur ch)) (walk (+ i 1)))))))))
|
||||
(true (do (set! cur (str cur ch)) (walk (+ i 1)))))))))
|
||||
(walk 0)
|
||||
(flush!)
|
||||
{:tag tag :classes classes :id id}))))
|
||||
@@ -1724,11 +1773,11 @@
|
||||
(value type-name)
|
||||
(if (nil? value) false (hs-type-check value type-name))))
|
||||
|
||||
|
||||
(define
|
||||
hs-strict-eq
|
||||
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
|
||||
|
||||
|
||||
(define
|
||||
hs-id=
|
||||
(fn
|
||||
@@ -1760,6 +1809,20 @@
|
||||
((nil? suffix) false)
|
||||
(true (ends-with? (str s) (str suffix))))))
|
||||
|
||||
(define
|
||||
hs-attr-watch!
|
||||
(fn
|
||||
(target attr-name handler)
|
||||
(let
|
||||
((mo-class (host-get (host-global "window") "MutationObserver")))
|
||||
(when
|
||||
mo-class
|
||||
(let
|
||||
((cb (fn (records observer) (for-each (fn (rec) (when (= (host-get rec "attributeName") attr-name) (handler (host-call target "getAttribute" attr-name)))) records))))
|
||||
(let
|
||||
((mo (host-new "MutationObserver" cb)))
|
||||
(host-call mo "observe" target {:attributeFilter (list attr-name) :attributes true})))))))
|
||||
|
||||
(define
|
||||
hs-scoped-set!
|
||||
(fn
|
||||
@@ -1805,10 +1868,7 @@
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((pos (host-call a "compareDocumentPosition" b)))
|
||||
(if
|
||||
(number? pos)
|
||||
(not (= 0 (mod (/ pos 4) 2)))
|
||||
false)))
|
||||
(if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false)))
|
||||
(true (< (str a) (str b))))))
|
||||
|
||||
(define
|
||||
@@ -1929,10 +1989,7 @@
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((pos (host-call a "compareDocumentPosition" b)))
|
||||
(if
|
||||
(number? pos)
|
||||
(not (= 0 (mod (/ pos 4) 2)))
|
||||
false)))
|
||||
(if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false)))
|
||||
(true (< (str a) (str b))))))
|
||||
|
||||
(define
|
||||
@@ -1985,9 +2042,7 @@
|
||||
|
||||
(define
|
||||
hs-morph-char
|
||||
(fn
|
||||
(s p)
|
||||
(if (or (< p 0) (>= p (string-length s))) nil (nth s p))))
|
||||
(fn (s p) (if (or (< p 0) (>= p (string-length s))) nil (nth s p))))
|
||||
|
||||
(define
|
||||
hs-morph-index-from
|
||||
@@ -2015,10 +2070,7 @@
|
||||
(q)
|
||||
(let
|
||||
((c (hs-morph-char s q)))
|
||||
(if
|
||||
(and c (< (index-of stop c) 0))
|
||||
(loop (+ q 1))
|
||||
q))))
|
||||
(if (and c (< (index-of stop c) 0)) (loop (+ q 1)) q))))
|
||||
(let ((e (loop p))) (list (substring s p e) e))))
|
||||
|
||||
(define
|
||||
@@ -2060,9 +2112,7 @@
|
||||
(append
|
||||
acc
|
||||
(list
|
||||
(list
|
||||
name
|
||||
(substring s (+ p4 1) close)))))))
|
||||
(list name (substring s (+ p4 1) close)))))))
|
||||
((= c2 "'")
|
||||
(let
|
||||
((close (hs-morph-index-from s "'" (+ p4 1))))
|
||||
@@ -2072,9 +2122,7 @@
|
||||
(append
|
||||
acc
|
||||
(list
|
||||
(list
|
||||
name
|
||||
(substring s (+ p4 1) close)))))))
|
||||
(list name (substring s (+ p4 1) close)))))))
|
||||
(true
|
||||
(let
|
||||
((r2 (hs-morph-read-until s p4 " \t\n/>")))
|
||||
@@ -2158,9 +2206,7 @@
|
||||
(for-each
|
||||
(fn
|
||||
(c)
|
||||
(when
|
||||
(> (string-length c) 0)
|
||||
(dom-add-class el c)))
|
||||
(when (> (string-length c) 0) (dom-add-class el c)))
|
||||
(split v " ")))
|
||||
((and keep-id (= n "id")) nil)
|
||||
(true (dom-set-attr el n v)))))
|
||||
@@ -2261,8 +2307,7 @@
|
||||
((parts (split resolved ":")))
|
||||
(let
|
||||
((prop (first parts))
|
||||
(val
|
||||
(if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(val (if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(cond
|
||||
((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop))
|
||||
(let
|
||||
@@ -2302,8 +2347,7 @@
|
||||
((parts (split resolved ":")))
|
||||
(let
|
||||
((prop (first parts))
|
||||
(val
|
||||
(if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(val (if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(cond
|
||||
((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop))
|
||||
(let
|
||||
@@ -2408,14 +2452,10 @@
|
||||
(if
|
||||
(= depth 1)
|
||||
j
|
||||
(find-close
|
||||
(+ j 1)
|
||||
(- depth 1)))
|
||||
(find-close (+ j 1) (- depth 1)))
|
||||
(if
|
||||
(= (nth raw j) "{")
|
||||
(find-close
|
||||
(+ j 1)
|
||||
(+ depth 1))
|
||||
(find-close (+ j 1) (+ depth 1))
|
||||
(find-close (+ j 1) depth))))))
|
||||
(let
|
||||
((close (find-close start 1)))
|
||||
@@ -2526,10 +2566,7 @@
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
-1
|
||||
(if
|
||||
(= (first lst) item)
|
||||
i
|
||||
(idx-loop (rest lst) (+ i 1))))))
|
||||
(if (= (first lst) item) i (idx-loop (rest lst) (+ i 1))))))
|
||||
(idx-loop obj 0)))
|
||||
(true
|
||||
(let
|
||||
@@ -2621,8 +2658,7 @@
|
||||
(cond
|
||||
((= end "hs-pick-end") n)
|
||||
((= end "hs-pick-start") 0)
|
||||
((and (number? end) (< end 0))
|
||||
(max 0 (+ n end)))
|
||||
((and (number? end) (< end 0)) (max 0 (+ n end)))
|
||||
(true end))))
|
||||
(cond
|
||||
((string? col) (slice col s e))
|
||||
@@ -2802,6 +2838,8 @@
|
||||
hs-sorted-by-desc
|
||||
(fn (col key-fn) (reverse (hs-sorted-by col key-fn))))
|
||||
|
||||
;; ── SourceInfo API ────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
hs-dom-has-var?
|
||||
(fn
|
||||
@@ -2821,8 +2859,6 @@
|
||||
((store (host-get el "__hs_vars")))
|
||||
(if (nil? store) nil (host-get store name)))))
|
||||
|
||||
;; ── SourceInfo API ────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
hs-dom-set-var-raw!
|
||||
(fn
|
||||
@@ -2913,7 +2949,12 @@
|
||||
|
||||
(define
|
||||
hs-null-error!
|
||||
(fn (selector) (raise (str "'" selector "' is null"))))
|
||||
(fn
|
||||
(selector)
|
||||
(let
|
||||
((msg (str "'" selector "' is null")))
|
||||
(host-set! (host-global "window") "_hs_null_error" msg)
|
||||
(guard (_null-e (true nil)) (raise msg)))))
|
||||
|
||||
(define
|
||||
hs-named-target
|
||||
@@ -2933,9 +2974,7 @@
|
||||
((results (hs-query-all selector)))
|
||||
(if
|
||||
(and
|
||||
(or
|
||||
(nil? results)
|
||||
(and (list? results) (= (len results) 0)))
|
||||
(or (nil? results) (and (list? results) (= (len results) 0)))
|
||||
(string? selector)
|
||||
(> (len selector) 0)
|
||||
(= (substring selector 0 1) "#"))
|
||||
|
||||
@@ -855,4 +855,230 @@
|
||||
:else (do (t-advance! 1) (scan-template!)))))))
|
||||
(scan-template!)
|
||||
(t-emit! "eof" nil)
|
||||
tokens)))
|
||||
tokens)))
|
||||
|
||||
;; ── Stream wrapper for upstream-style stateful tokenizer API ───────────────
|
||||
;;
|
||||
;; Upstream _hyperscript exposes a Tokens object with cursor + follow-set
|
||||
;; semantics on _hyperscript.internals.tokenizer. Our hs-tokenize returns a
|
||||
;; flat list; the stream wrapper adds the stateful operations.
|
||||
;;
|
||||
;; Type names map ours → upstream's (e.g. "ident" → "IDENTIFIER").
|
||||
|
||||
(define
|
||||
hs-stream-type-map
|
||||
(fn
|
||||
(t)
|
||||
(cond
|
||||
((= t "ident") "IDENTIFIER")
|
||||
((= t "number") "NUMBER")
|
||||
((= t "string") "STRING")
|
||||
((= t "class") "CLASS_REF")
|
||||
((= t "id") "ID_REF")
|
||||
((= t "attr") "ATTRIBUTE_REF")
|
||||
((= t "style") "STYLE_REF")
|
||||
((= t "whitespace") "WHITESPACE")
|
||||
((= t "op") "OPERATOR")
|
||||
((= t "eof") "EOF")
|
||||
(true (upcase t)))))
|
||||
|
||||
;; Create a stream from a source string.
|
||||
;; Returns a dict — mutable via dict-set!.
|
||||
(define
|
||||
hs-stream
|
||||
(fn
|
||||
(src)
|
||||
{:tokens (hs-tokenize src) :pos 0 :follows (list) :last-match nil :last-ws nil}))
|
||||
|
||||
;; Skip whitespace tokens, advancing pos to the next non-WS token.
|
||||
;; Captures the last skipped whitespace value into :last-ws.
|
||||
(define
|
||||
hs-stream-skip-ws!
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((tokens (get s :tokens)))
|
||||
(define
|
||||
loop
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((p (get s :pos)))
|
||||
(when
|
||||
(and (< p (len tokens))
|
||||
(= (get (nth tokens p) :type) "whitespace"))
|
||||
(do
|
||||
(dict-set! s :last-ws (get (nth tokens p) :value))
|
||||
(dict-set! s :pos (+ p 1))
|
||||
(loop))))))
|
||||
(loop))))
|
||||
|
||||
;; Current token (after skipping whitespace).
|
||||
(define
|
||||
hs-stream-current
|
||||
(fn
|
||||
(s)
|
||||
(do
|
||||
(hs-stream-skip-ws! s)
|
||||
(let
|
||||
((tokens (get s :tokens)) (p (get s :pos)))
|
||||
(if (< p (len tokens)) (nth tokens p) nil)))))
|
||||
|
||||
;; Returns the current token if its value matches; advances and updates
|
||||
;; :last-match. Returns nil otherwise (no advance).
|
||||
;; Honors the follow set: tokens whose value is in :follows do NOT match.
|
||||
(define
|
||||
hs-stream-match
|
||||
(fn
|
||||
(s value)
|
||||
(let
|
||||
((cur (hs-stream-current s)))
|
||||
(cond
|
||||
((nil? cur) nil)
|
||||
((some (fn (f) (= f value)) (get s :follows)) nil)
|
||||
((= (get cur :value) value)
|
||||
(do
|
||||
(dict-set! s :pos (+ (get s :pos) 1))
|
||||
(dict-set! s :last-match cur)
|
||||
cur))
|
||||
(true nil)))))
|
||||
|
||||
;; Match by upstream-style type name. Accepts any number of allowed types.
|
||||
(define
|
||||
hs-stream-match-type
|
||||
(fn
|
||||
(s &rest types)
|
||||
(let
|
||||
((cur (hs-stream-current s)))
|
||||
(cond
|
||||
((nil? cur) nil)
|
||||
((some (fn (t) (= (hs-stream-type-map (get cur :type)) t)) types)
|
||||
(do
|
||||
(dict-set! s :pos (+ (get s :pos) 1))
|
||||
(dict-set! s :last-match cur)
|
||||
cur))
|
||||
(true nil)))))
|
||||
|
||||
;; Match if value is one of the given names.
|
||||
(define
|
||||
hs-stream-match-any
|
||||
(fn
|
||||
(s &rest names)
|
||||
(let
|
||||
((cur (hs-stream-current s)))
|
||||
(cond
|
||||
((nil? cur) nil)
|
||||
((some (fn (n) (= (get cur :value) n)) names)
|
||||
(do
|
||||
(dict-set! s :pos (+ (get s :pos) 1))
|
||||
(dict-set! s :last-match cur)
|
||||
cur))
|
||||
(true nil)))))
|
||||
|
||||
;; Match an op token whose value is in the list.
|
||||
(define
|
||||
hs-stream-match-any-op
|
||||
(fn
|
||||
(s &rest ops)
|
||||
(let
|
||||
((cur (hs-stream-current s)))
|
||||
(cond
|
||||
((nil? cur) nil)
|
||||
((and (= (get cur :type) "op")
|
||||
(some (fn (o) (= (get cur :value) o)) ops))
|
||||
(do
|
||||
(dict-set! s :pos (+ (get s :pos) 1))
|
||||
(dict-set! s :last-match cur)
|
||||
cur))
|
||||
(true nil)))))
|
||||
|
||||
;; Peek N non-WS tokens ahead. Returns the token if its value matches; nil otherwise.
|
||||
(define
|
||||
hs-stream-peek
|
||||
(fn
|
||||
(s value offset)
|
||||
(let
|
||||
((tokens (get s :tokens)))
|
||||
(define
|
||||
skip-n-non-ws
|
||||
(fn
|
||||
(p remaining)
|
||||
(cond
|
||||
((>= p (len tokens)) -1)
|
||||
((= (get (nth tokens p) :type) "whitespace")
|
||||
(skip-n-non-ws (+ p 1) remaining))
|
||||
((= remaining 0) p)
|
||||
(true (skip-n-non-ws (+ p 1) (- remaining 1))))))
|
||||
(let
|
||||
((p (skip-n-non-ws (get s :pos) offset)))
|
||||
(if (and (>= p 0) (< p (len tokens))
|
||||
(= (get (nth tokens p) :value) value))
|
||||
(nth tokens p)
|
||||
nil)))))
|
||||
|
||||
;; Consume tokens until one whose value matches the marker. Returns
|
||||
;; the consumed list (excluding the marker). Marker becomes current.
|
||||
(define
|
||||
hs-stream-consume-until
|
||||
(fn
|
||||
(s marker)
|
||||
(let
|
||||
((tokens (get s :tokens)) (out (list)))
|
||||
(define
|
||||
loop
|
||||
(fn
|
||||
(acc)
|
||||
(let
|
||||
((p (get s :pos)))
|
||||
(cond
|
||||
((>= p (len tokens)) acc)
|
||||
((= (get (nth tokens p) :value) marker) acc)
|
||||
(true
|
||||
(do
|
||||
(dict-set! s :pos (+ p 1))
|
||||
(loop (append acc (list (nth tokens p))))))))))
|
||||
(loop out))))
|
||||
|
||||
;; Consume until the next whitespace token; returns the consumed list.
|
||||
(define
|
||||
hs-stream-consume-until-ws
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((tokens (get s :tokens)))
|
||||
(define
|
||||
loop
|
||||
(fn
|
||||
(acc)
|
||||
(let
|
||||
((p (get s :pos)))
|
||||
(cond
|
||||
((>= p (len tokens)) acc)
|
||||
((= (get (nth tokens p) :type) "whitespace") acc)
|
||||
(true
|
||||
(do
|
||||
(dict-set! s :pos (+ p 1))
|
||||
(loop (append acc (list (nth tokens p))))))))))
|
||||
(loop (list)))))
|
||||
|
||||
;; Follow-set management.
|
||||
(define hs-stream-push-follow! (fn (s v) (dict-set! s :follows (cons v (get s :follows)))))
|
||||
(define
|
||||
hs-stream-pop-follow!
|
||||
(fn (s) (let ((f (get s :follows))) (when (> (len f) 0) (dict-set! s :follows (rest f))))))
|
||||
(define
|
||||
hs-stream-push-follows!
|
||||
(fn (s vs) (for-each (fn (v) (hs-stream-push-follow! s v)) vs)))
|
||||
(define
|
||||
hs-stream-pop-follows!
|
||||
(fn (s n) (when (> n 0) (do (hs-stream-pop-follow! s) (hs-stream-pop-follows! s (- n 1))))))
|
||||
(define
|
||||
hs-stream-clear-follows!
|
||||
(fn (s) (let ((saved (get s :follows))) (do (dict-set! s :follows (list)) saved))))
|
||||
(define
|
||||
hs-stream-restore-follows!
|
||||
(fn (s saved) (dict-set! s :follows saved)))
|
||||
|
||||
;; Last-consumed token / whitespace.
|
||||
(define hs-stream-last-match (fn (s) (get s :last-match)))
|
||||
(define hs-stream-last-ws (fn (s) (get s :last-ws)))
|
||||
234
lib/kernel/eval.sx
Normal file
234
lib/kernel/eval.sx
Normal file
@@ -0,0 +1,234 @@
|
||||
;; lib/kernel/eval.sx — Kernel evaluator.
|
||||
;;
|
||||
;; The evaluator is `lookup-and-combine`: there are no hardcoded special
|
||||
;; forms. Even $if / $define! / $lambda are ordinary operatives bound in
|
||||
;; the standard environment (Phase 4). This file builds the dispatch
|
||||
;; machinery and the operative/applicative tagged-value protocol.
|
||||
;;
|
||||
;; Tagged values
|
||||
;; -------------
|
||||
;; {:knl-tag :env :bindings DICT :parent PARENT-OR-NIL}
|
||||
;; A first-class Kernel environment. Bindings is a mutable SX dict
|
||||
;; keyed by symbol name; parent walks up the lookup chain.
|
||||
;;
|
||||
;; {:knl-tag :operative :impl FN}
|
||||
;; Primitive operative. FN receives (args dyn-env) — args are the
|
||||
;; UN-evaluated argument expressions, dyn-env is the calling env.
|
||||
;;
|
||||
;; {:knl-tag :operative :params P :env-param EP :body B :static-env SE}
|
||||
;; User-defined operative (built by $vau). Same tag; dispatch in
|
||||
;; kernel-call-operative forks on which keys are present.
|
||||
;;
|
||||
;; {:knl-tag :applicative :underlying OP}
|
||||
;; An applicative wraps an operative. Calls evaluate args first,
|
||||
;; then forward to the underlying operative.
|
||||
;;
|
||||
;; The env-param of a user operative may be the sentinel :knl-ignore,
|
||||
;; in which case the dynamic env is not bound.
|
||||
;;
|
||||
;; Public API
|
||||
;; (kernel-eval EXPR ENV) — primary entry
|
||||
;; (kernel-combine COMBINER ARGS DYN-ENV)
|
||||
;; (kernel-call-operative OP ARGS DYN-ENV)
|
||||
;; (kernel-bind-params! ENV PARAMS ARGS)
|
||||
;; (kernel-make-env) / (kernel-extend-env P)
|
||||
;; (kernel-env-bind! E N V) / (kernel-env-lookup E N)
|
||||
;; (kernel-env-has? E N) / (kernel-env? V)
|
||||
;; (kernel-make-primitive-operative IMPL)
|
||||
;; (kernel-make-primitive-applicative IMPL)
|
||||
;; (kernel-make-user-operative PARAMS EPARAM BODY STATIC-ENV)
|
||||
;; (kernel-wrap OP) / (kernel-unwrap APP)
|
||||
;; (kernel-operative? V) / (kernel-applicative? V) / (kernel-combiner? V)
|
||||
;;
|
||||
;; Consumes: lib/kernel/parser.sx (kernel-string?, kernel-string-value)
|
||||
|
||||
;; ── Environments — first-class, pure-SX (binding dict + parent) ──
|
||||
|
||||
(define kernel-env? (fn (v) (and (dict? v) (= (get v :knl-tag) :env))))
|
||||
|
||||
(define kernel-make-env (fn () {:parent nil :knl-tag :env :bindings {}}))
|
||||
|
||||
(define kernel-extend-env (fn (parent) {:parent parent :knl-tag :env :bindings {}}))
|
||||
|
||||
(define
|
||||
kernel-env-bind!
|
||||
(fn (env name val) (dict-set! (get env :bindings) name val) val))
|
||||
|
||||
(define
|
||||
kernel-env-has?
|
||||
(fn
|
||||
(env name)
|
||||
(cond
|
||||
((nil? env) false)
|
||||
((not (kernel-env? env)) false)
|
||||
((dict-has? (get env :bindings) name) true)
|
||||
(:else (kernel-env-has? (get env :parent) name)))))
|
||||
|
||||
(define
|
||||
kernel-env-lookup
|
||||
(fn
|
||||
(env name)
|
||||
(cond
|
||||
((nil? env) (error (str "kernel-eval: unbound symbol: " name)))
|
||||
((not (kernel-env? env))
|
||||
(error (str "kernel-eval: corrupt env: " env)))
|
||||
((dict-has? (get env :bindings) name) (get (get env :bindings) name))
|
||||
(:else (kernel-env-lookup (get env :parent) name)))))
|
||||
|
||||
;; ── Tagged-value constructors and predicates ─────────────────────
|
||||
|
||||
(define kernel-make-primitive-operative (fn (impl) {:impl impl :knl-tag :operative}))
|
||||
|
||||
(define
|
||||
kernel-make-user-operative
|
||||
(fn (params eparam body static-env) {:knl-tag :operative :static-env static-env :params params :body body :env-param eparam}))
|
||||
|
||||
(define
|
||||
kernel-operative?
|
||||
(fn (v) (and (dict? v) (= (get v :knl-tag) :operative))))
|
||||
|
||||
(define
|
||||
kernel-applicative?
|
||||
(fn (v) (and (dict? v) (= (get v :knl-tag) :applicative))))
|
||||
|
||||
(define
|
||||
kernel-combiner?
|
||||
(fn (v) (or (kernel-operative? v) (kernel-applicative? v))))
|
||||
|
||||
(define
|
||||
kernel-wrap
|
||||
(fn
|
||||
(op)
|
||||
(cond
|
||||
((kernel-operative? op) {:knl-tag :applicative :underlying op})
|
||||
(:else (error "kernel-wrap: argument must be an operative")))))
|
||||
|
||||
(define
|
||||
kernel-unwrap
|
||||
(fn
|
||||
(app)
|
||||
(cond
|
||||
((kernel-applicative? app) (get app :underlying))
|
||||
(:else (error "kernel-unwrap: argument must be an applicative")))))
|
||||
|
||||
(define
|
||||
kernel-make-primitive-applicative
|
||||
(fn
|
||||
(impl)
|
||||
(kernel-wrap
|
||||
(kernel-make-primitive-operative (fn (args dyn-env) (impl args))))))
|
||||
|
||||
;; As above, but IMPL receives (args dyn-env). Used by combinators that
|
||||
;; re-enter the evaluator (map, filter, reduce, apply, eval, ...).
|
||||
(define kernel-make-primitive-applicative-with-env
|
||||
(fn (impl)
|
||||
(kernel-wrap
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env) (impl args dyn-env))))))
|
||||
|
||||
;; ── The evaluator ────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
kernel-eval
|
||||
(fn
|
||||
(expr env)
|
||||
(cond
|
||||
((number? expr) expr)
|
||||
((boolean? expr) expr)
|
||||
((nil? expr) expr)
|
||||
((kernel-string? expr) (kernel-string-value expr))
|
||||
((string? expr) (kernel-env-lookup env expr))
|
||||
((list? expr)
|
||||
(cond
|
||||
((= (length expr) 0) expr)
|
||||
(:else
|
||||
(let
|
||||
((combiner (kernel-eval (first expr) env))
|
||||
(args (rest expr)))
|
||||
(kernel-combine combiner args env)))))
|
||||
(:else (error (str "kernel-eval: unknown form: " expr))))))
|
||||
|
||||
(define
|
||||
kernel-combine
|
||||
(fn
|
||||
(combiner args dyn-env)
|
||||
(cond
|
||||
((kernel-operative? combiner)
|
||||
(kernel-call-operative combiner args dyn-env))
|
||||
((kernel-applicative? combiner)
|
||||
(kernel-combine
|
||||
(get combiner :underlying)
|
||||
(kernel-eval-args args dyn-env)
|
||||
dyn-env))
|
||||
(:else (error (str "kernel-eval: not a combiner: " combiner))))))
|
||||
|
||||
;; Operatives may be primitive (:impl is a host fn) or user-defined
|
||||
;; (carry :params / :env-param / :body / :static-env). The dispatch
|
||||
;; fork is here so kernel-combine stays small.
|
||||
(define
|
||||
kernel-call-operative
|
||||
(fn
|
||||
(op args dyn-env)
|
||||
(cond
|
||||
((dict-has? op :impl) ((get op :impl) args dyn-env))
|
||||
((dict-has? op :body)
|
||||
(let
|
||||
((local (kernel-extend-env (get op :static-env))))
|
||||
(kernel-bind-params! local (get op :params) args)
|
||||
(let
|
||||
((eparam (get op :env-param)))
|
||||
(when
|
||||
(not (= eparam :knl-ignore))
|
||||
(kernel-env-bind! local eparam dyn-env)))
|
||||
;; :body is a list of forms — evaluate in sequence, return last.
|
||||
(knl-eval-body (get op :body) local)))
|
||||
(:else (error "kernel-call-operative: malformed operative")))))
|
||||
|
||||
(define knl-eval-body
|
||||
(fn (forms env)
|
||||
(cond
|
||||
((= (length forms) 1) (kernel-eval (first forms) env))
|
||||
(:else
|
||||
(begin
|
||||
(kernel-eval (first forms) env)
|
||||
(knl-eval-body (rest forms) env))))))
|
||||
|
||||
;; Phase 3 supports a flat parameter list only — destructuring later.
|
||||
(define
|
||||
kernel-bind-params!
|
||||
(fn
|
||||
(env params args)
|
||||
(cond
|
||||
((or (nil? params) (= (length params) 0))
|
||||
(cond
|
||||
((or (nil? args) (= (length args) 0)) nil)
|
||||
(:else (error "kernel-call: too many arguments"))))
|
||||
((or (nil? args) (= (length args) 0))
|
||||
(error "kernel-call: too few arguments"))
|
||||
(:else
|
||||
(begin
|
||||
(kernel-env-bind! env (first params) (first args))
|
||||
(kernel-bind-params! env (rest params) (rest args)))))))
|
||||
|
||||
(define
|
||||
kernel-eval-args
|
||||
(fn
|
||||
(args env)
|
||||
(cond
|
||||
((or (nil? args) (= (length args) 0)) (list))
|
||||
(:else
|
||||
(cons
|
||||
(kernel-eval (first args) env)
|
||||
(kernel-eval-args (rest args) env))))))
|
||||
|
||||
(define
|
||||
kernel-eval-program
|
||||
(fn
|
||||
(forms env)
|
||||
(cond
|
||||
((or (nil? forms) (= (length forms) 0)) nil)
|
||||
((= (length forms) 1) (kernel-eval (first forms) env))
|
||||
(:else
|
||||
(begin
|
||||
(kernel-eval (first forms) env)
|
||||
(kernel-eval-program (rest forms) env))))))
|
||||
253
lib/kernel/parser.sx
Normal file
253
lib/kernel/parser.sx
Normal file
@@ -0,0 +1,253 @@
|
||||
;; lib/kernel/parser.sx — Kernel s-expression reader.
|
||||
;;
|
||||
;; Reads R-1RK lexical syntax: numbers, strings, symbols, booleans (#t/#f),
|
||||
;; the empty list (), nested lists, and ; line comments. Reader macros
|
||||
;; (' ` , ,@) deferred to Phase 6 per the plan.
|
||||
;;
|
||||
;; Public AST shape:
|
||||
;; number → SX number
|
||||
;; #t / #f → SX true / false
|
||||
;; () → SX empty list (Kernel's nil — the empty list)
|
||||
;; "..." → {:knl-string "..."} wrapped to distinguish from symbols
|
||||
;; foo → "foo" bare SX string is a Kernel symbol
|
||||
;; (a b c) → SX list of forms
|
||||
;;
|
||||
;; Public API:
|
||||
;; (kernel-parse SRC) — first form; errors on extra trailing input
|
||||
;; (kernel-parse-all SRC) — all top-level forms, as SX list
|
||||
;; (kernel-string? V) — recognise wrapped string literal
|
||||
;; (kernel-string-value V) — extract the underlying string
|
||||
;;
|
||||
;; Consumes: lib/guest/lex.sx (lex-digit?, lex-whitespace?)
|
||||
|
||||
(define kernel-string-make (fn (s) {:knl-string s}))
|
||||
(define
|
||||
kernel-string?
|
||||
(fn (v) (and (dict? v) (string? (get v :knl-string)))))
|
||||
(define kernel-string-value (fn (v) (get v :knl-string)))
|
||||
|
||||
;; Atom delimiters: characters that end a symbol or numeric token.
|
||||
(define
|
||||
knl-delim?
|
||||
(fn
|
||||
(c)
|
||||
(or
|
||||
(nil? c)
|
||||
(lex-whitespace? c)
|
||||
(= c "(")
|
||||
(= c ")")
|
||||
(= c "\"")
|
||||
(= c ";")
|
||||
(= c "'")
|
||||
(= c "`")
|
||||
(= c ","))))
|
||||
|
||||
;; Numeric grammar: [+-]? (digit+ ('.' digit+)? | '.' digit+) ([eE][+-]?digit+)?
|
||||
(define
|
||||
knl-numeric?
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((n (string-length s)))
|
||||
(cond
|
||||
((= n 0) false)
|
||||
(:else
|
||||
(let
|
||||
((c0 (substring s 0 1)))
|
||||
(let
|
||||
((start (if (or (= c0 "+") (= c0 "-")) 1 0)))
|
||||
(knl-num-body? s start n))))))))
|
||||
|
||||
(define
|
||||
knl-num-body?
|
||||
(fn
|
||||
(s start n)
|
||||
(cond
|
||||
((>= start n) false)
|
||||
((= (substring s start (+ start 1)) ".")
|
||||
(knl-num-need-digits? s (+ start 1) n false))
|
||||
((lex-digit? (substring s start (+ start 1)))
|
||||
(knl-num-int-tail? s (+ start 1) n))
|
||||
(:else false))))
|
||||
|
||||
(define
|
||||
knl-num-int-tail?
|
||||
(fn
|
||||
(s i n)
|
||||
(cond
|
||||
((>= i n) true)
|
||||
((lex-digit? (substring s i (+ i 1)))
|
||||
(knl-num-int-tail? s (+ i 1) n))
|
||||
((= (substring s i (+ i 1)) ".")
|
||||
(knl-num-need-digits? s (+ i 1) n true))
|
||||
((or (= (substring s i (+ i 1)) "e") (= (substring s i (+ i 1)) "E"))
|
||||
(knl-num-exp-sign? s (+ i 1) n))
|
||||
(:else false))))
|
||||
|
||||
(define
|
||||
knl-num-need-digits?
|
||||
(fn
|
||||
(s i n had-int)
|
||||
(cond
|
||||
((>= i n) had-int)
|
||||
((lex-digit? (substring s i (+ i 1)))
|
||||
(knl-num-frac-tail? s (+ i 1) n))
|
||||
(:else false))))
|
||||
|
||||
(define
|
||||
knl-num-frac-tail?
|
||||
(fn
|
||||
(s i n)
|
||||
(cond
|
||||
((>= i n) true)
|
||||
((lex-digit? (substring s i (+ i 1)))
|
||||
(knl-num-frac-tail? s (+ i 1) n))
|
||||
((or (= (substring s i (+ i 1)) "e") (= (substring s i (+ i 1)) "E"))
|
||||
(knl-num-exp-sign? s (+ i 1) n))
|
||||
(:else false))))
|
||||
|
||||
(define
|
||||
knl-num-exp-sign?
|
||||
(fn
|
||||
(s i n)
|
||||
(cond
|
||||
((>= i n) false)
|
||||
((or (= (substring s i (+ i 1)) "+") (= (substring s i (+ i 1)) "-"))
|
||||
(knl-num-exp-digits? s (+ i 1) n false))
|
||||
(:else (knl-num-exp-digits? s i n false)))))
|
||||
|
||||
(define
|
||||
knl-num-exp-digits?
|
||||
(fn
|
||||
(s i n had)
|
||||
(cond
|
||||
((>= i n) had)
|
||||
((lex-digit? (substring s i (+ i 1)))
|
||||
(knl-num-exp-digits? s (+ i 1) n true))
|
||||
(:else false))))
|
||||
|
||||
;; Reader: a closure over (src, pos). Exposes :read-form and :read-all.
|
||||
(define
|
||||
knl-make-reader
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((pos 0) (n (string-length src)))
|
||||
(define
|
||||
at
|
||||
(fn () (if (< pos n) (substring src pos (+ pos 1)) nil)))
|
||||
(define adv (fn () (set! pos (+ pos 1))))
|
||||
(define
|
||||
skip-line
|
||||
(fn () (when (and (at) (not (= (at) "\n"))) (adv) (skip-line))))
|
||||
(define
|
||||
skip-ws
|
||||
(fn
|
||||
()
|
||||
(cond
|
||||
((nil? (at)) nil)
|
||||
((lex-whitespace? (at)) (do (adv) (skip-ws)))
|
||||
((= (at) ";") (do (adv) (skip-line) (skip-ws)))
|
||||
(:else nil))))
|
||||
(define
|
||||
read-string-body
|
||||
(fn
|
||||
(acc)
|
||||
(cond
|
||||
((nil? (at)) (error "kernel-parse: unterminated string"))
|
||||
((= (at) "\"") (do (adv) acc))
|
||||
((= (at) "\\")
|
||||
(do
|
||||
(adv)
|
||||
(let
|
||||
((c (at)))
|
||||
(when (nil? c) (error "kernel-parse: trailing backslash"))
|
||||
(adv)
|
||||
(read-string-body
|
||||
(str
|
||||
acc
|
||||
(cond
|
||||
((= c "n") "\n")
|
||||
((= c "t") "\t")
|
||||
((= c "r") "\r")
|
||||
((= c "\"") "\"")
|
||||
((= c "\\") "\\")
|
||||
(:else c)))))))
|
||||
(:else
|
||||
(let ((c (at))) (adv) (read-string-body (str acc c)))))))
|
||||
(define
|
||||
read-atom-body
|
||||
(fn
|
||||
(acc)
|
||||
(cond
|
||||
((knl-delim? (at)) acc)
|
||||
(:else (let ((c (at))) (adv) (read-atom-body (str acc c)))))))
|
||||
(define
|
||||
classify-atom
|
||||
(fn
|
||||
(s)
|
||||
(cond
|
||||
((= s "#t") true)
|
||||
((= s "#f") false)
|
||||
((knl-numeric? s) (string->number s))
|
||||
(:else s))))
|
||||
(define
|
||||
read-form
|
||||
(fn
|
||||
()
|
||||
(skip-ws)
|
||||
(cond
|
||||
((nil? (at)) :knl-eof)
|
||||
((= (at) ")") (error "kernel-parse: unexpected ')'"))
|
||||
((= (at) "(") (do (adv) (read-list (list))))
|
||||
((= (at) "\"")
|
||||
(do (adv) (kernel-string-make (read-string-body ""))))
|
||||
((= (at) "'")
|
||||
(do (adv) (list "$quote" (read-form))))
|
||||
((= (at) "`")
|
||||
(do (adv) (list "$quasiquote" (read-form))))
|
||||
((= (at) ",")
|
||||
(do (adv)
|
||||
(cond
|
||||
((= (at) "@")
|
||||
(do (adv) (list "$unquote-splicing" (read-form))))
|
||||
(:else (list "$unquote" (read-form))))))
|
||||
(:else (classify-atom (read-atom-body ""))))))
|
||||
(define
|
||||
read-list
|
||||
(fn
|
||||
(acc)
|
||||
(skip-ws)
|
||||
(cond
|
||||
((nil? (at)) (error "kernel-parse: unterminated list"))
|
||||
((= (at) ")") (do (adv) acc))
|
||||
(:else (read-list (append acc (list (read-form))))))))
|
||||
(define
|
||||
read-all
|
||||
(fn
|
||||
(acc)
|
||||
(skip-ws)
|
||||
(if (nil? (at)) acc (read-all (append acc (list (read-form)))))))
|
||||
{:read-form read-form :read-all read-all})))
|
||||
|
||||
(define
|
||||
kernel-parse-all
|
||||
(fn (src) ((get (knl-make-reader src) :read-all) (list))))
|
||||
|
||||
(define
|
||||
kernel-parse
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((r (knl-make-reader src)))
|
||||
(let
|
||||
((form ((get r :read-form))))
|
||||
(cond
|
||||
((= form :knl-eof) (error "kernel-parse: empty input"))
|
||||
(:else
|
||||
(let
|
||||
((next ((get r :read-form))))
|
||||
(if
|
||||
(= next :knl-eof)
|
||||
form
|
||||
(error "kernel-parse: trailing input after first form")))))))))
|
||||
911
lib/kernel/runtime.sx
Normal file
911
lib/kernel/runtime.sx
Normal file
@@ -0,0 +1,911 @@
|
||||
;; lib/kernel/runtime.sx — the operative–applicative substrate and the
|
||||
;; standard Kernel environment.
|
||||
;;
|
||||
;; Phase 3 supplied four user-visible combiners ($vau, $lambda, wrap,
|
||||
;; unwrap). Phase 4 fills out the rest of the R-1RK core: $if, $define!,
|
||||
;; $sequence, eval, make-environment, get-current-environment, plus
|
||||
;; arithmetic, equality, list/pair, and boolean primitives — enough to
|
||||
;; write factorial.
|
||||
;;
|
||||
;; The standard env is built by EXTENDING the base env, not replacing
|
||||
;; it. So `kernel-standard-env` includes everything from `kernel-base-env`.
|
||||
;;
|
||||
;; Public API
|
||||
;; (kernel-base-env) — Phase 3 combiners
|
||||
;; (kernel-standard-env) — Phase 4 standard environment
|
||||
|
||||
(define
|
||||
knl-eparam-sentinel
|
||||
(fn
|
||||
(sym)
|
||||
(cond
|
||||
((= sym "_") :knl-ignore)
|
||||
((= sym "#ignore") :knl-ignore)
|
||||
(:else sym))))
|
||||
|
||||
(define
|
||||
knl-formals-ok?
|
||||
(fn
|
||||
(formals)
|
||||
(cond
|
||||
((not (list? formals)) false)
|
||||
((= (length formals) 0) true)
|
||||
((string? (first formals)) (knl-formals-ok? (rest formals)))
|
||||
(:else false))))
|
||||
|
||||
;; ── $vau ─────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
kernel-vau-impl
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(cond
|
||||
((< (length args) 3)
|
||||
(error "$vau: expects (formals env-param body...)"))
|
||||
(:else
|
||||
(let
|
||||
((formals (first args))
|
||||
(eparam-raw (nth args 1))
|
||||
(body-forms (rest (rest args))))
|
||||
(cond
|
||||
((not (knl-formals-ok? formals))
|
||||
(error "$vau: formals must be a list of symbols"))
|
||||
((not (string? eparam-raw))
|
||||
(error "$vau: env-param must be a symbol"))
|
||||
(:else
|
||||
(kernel-make-user-operative
|
||||
formals
|
||||
(knl-eparam-sentinel eparam-raw)
|
||||
body-forms
|
||||
dyn-env))))))))
|
||||
|
||||
(define
|
||||
kernel-vau-operative
|
||||
(kernel-make-primitive-operative kernel-vau-impl))
|
||||
|
||||
;; ── $lambda ──────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
kernel-lambda-impl
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(cond
|
||||
((< (length args) 2)
|
||||
(error "$lambda: expects (formals body...)"))
|
||||
(:else
|
||||
(let
|
||||
((formals (first args)) (body-forms (rest args)))
|
||||
(cond
|
||||
((not (knl-formals-ok? formals))
|
||||
(error "$lambda: formals must be a list of symbols"))
|
||||
(:else
|
||||
(kernel-wrap
|
||||
(kernel-make-user-operative
|
||||
formals
|
||||
:knl-ignore
|
||||
body-forms
|
||||
dyn-env)))))))))
|
||||
|
||||
(define
|
||||
kernel-lambda-operative
|
||||
(kernel-make-primitive-operative kernel-lambda-impl))
|
||||
|
||||
;; ── wrap / unwrap / predicates ───────────────────────────────────
|
||||
|
||||
(define
|
||||
kernel-wrap-applicative
|
||||
(kernel-make-primitive-applicative
|
||||
(fn
|
||||
(args)
|
||||
(cond
|
||||
((not (= (length args) 1))
|
||||
(error "wrap: expects exactly 1 argument"))
|
||||
(:else (kernel-wrap (first args)))))))
|
||||
|
||||
(define
|
||||
kernel-unwrap-applicative
|
||||
(kernel-make-primitive-applicative
|
||||
(fn
|
||||
(args)
|
||||
(cond
|
||||
((not (= (length args) 1))
|
||||
(error "unwrap: expects exactly 1 argument"))
|
||||
(:else (kernel-unwrap (first args)))))))
|
||||
|
||||
(define
|
||||
kernel-operative?-applicative
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args) (kernel-operative? (first args)))))
|
||||
|
||||
(define
|
||||
kernel-applicative?-applicative
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args) (kernel-applicative? (first args)))))
|
||||
|
||||
(define
|
||||
kernel-base-env
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((env (kernel-make-env)))
|
||||
(kernel-env-bind! env "$vau" kernel-vau-operative)
|
||||
(kernel-env-bind! env "$lambda" kernel-lambda-operative)
|
||||
(kernel-env-bind! env "wrap" kernel-wrap-applicative)
|
||||
(kernel-env-bind! env "unwrap" kernel-unwrap-applicative)
|
||||
(kernel-env-bind! env "operative?" kernel-operative?-applicative)
|
||||
(kernel-env-bind! env "applicative?" kernel-applicative?-applicative)
|
||||
env)))
|
||||
|
||||
;; ── $if / $define! / $sequence ───────────────────────────────────
|
||||
|
||||
(define
|
||||
kernel-if-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 3))
|
||||
(error "$if: expects (condition then-expr else-expr)"))
|
||||
(:else
|
||||
(let
|
||||
((c (kernel-eval (first args) dyn-env)))
|
||||
(if
|
||||
c
|
||||
(kernel-eval (nth args 1) dyn-env)
|
||||
(kernel-eval (nth args 2) dyn-env))))))))
|
||||
|
||||
(define
|
||||
kernel-define!-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 2))
|
||||
(error "$define!: expects (name expr)"))
|
||||
((not (string? (first args)))
|
||||
(error "$define!: name must be a symbol"))
|
||||
(:else
|
||||
(let
|
||||
((v (kernel-eval (nth args 1) dyn-env)))
|
||||
(kernel-env-bind! dyn-env (first args) v)
|
||||
v))))))
|
||||
|
||||
(define
|
||||
kernel-sequence-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(cond
|
||||
((or (nil? args) (= (length args) 0)) nil)
|
||||
((= (length args) 1) (kernel-eval (first args) dyn-env))
|
||||
(:else
|
||||
(begin
|
||||
(kernel-eval (first args) dyn-env)
|
||||
((get kernel-sequence-operative :impl) (rest args) dyn-env)))))))
|
||||
|
||||
;; ── eval / make-environment / get-current-environment ───────────
|
||||
|
||||
(define
|
||||
kernel-quote-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 1)) (error "$quote: expects 1 argument"))
|
||||
(:else (first args))))))
|
||||
|
||||
;; Quasiquote: walks the template, evaluating `$unquote` forms in the
|
||||
;; dynamic env and splicing `$unquote-splicing` list results.
|
||||
(define knl-quasi-walk
|
||||
(fn (form dyn-env)
|
||||
(cond
|
||||
((not (list? form)) form)
|
||||
((= (length form) 0) form)
|
||||
((and (string? (first form)) (= (first form) "$unquote"))
|
||||
(cond
|
||||
((not (= (length form) 2))
|
||||
(error "$unquote: expects exactly 1 argument"))
|
||||
(:else (kernel-eval (nth form 1) dyn-env))))
|
||||
(:else (knl-quasi-walk-list form dyn-env)))))
|
||||
|
||||
(define knl-quasi-walk-list
|
||||
(fn (forms dyn-env)
|
||||
(cond
|
||||
((or (nil? forms) (= (length forms) 0)) (list))
|
||||
(:else
|
||||
(let ((head (first forms)))
|
||||
(cond
|
||||
((and (list? head)
|
||||
(= (length head) 2)
|
||||
(string? (first head))
|
||||
(= (first head) "$unquote-splicing"))
|
||||
(let ((spliced (kernel-eval (nth head 1) dyn-env)))
|
||||
(cond
|
||||
((not (list? spliced))
|
||||
(error "$unquote-splicing: value must be a list"))
|
||||
(:else
|
||||
(knl-list-concat
|
||||
spliced
|
||||
(knl-quasi-walk-list (rest forms) dyn-env))))))
|
||||
(:else
|
||||
(cons (knl-quasi-walk head dyn-env)
|
||||
(knl-quasi-walk-list (rest forms) dyn-env)))))))))
|
||||
|
||||
(define knl-list-concat
|
||||
(fn (xs ys)
|
||||
(cond
|
||||
((or (nil? xs) (= (length xs) 0)) ys)
|
||||
(:else (cons (first xs) (knl-list-concat (rest xs) ys))))))
|
||||
|
||||
;; $cond — multi-clause branch.
|
||||
;; ($cond (TEST1 EXPR1 ...) (TEST2 EXPR2 ...) ...)
|
||||
;; Evaluates each TEST in order; first truthy one runs its EXPRs (in
|
||||
;; sequence) and returns the last; if no TEST is truthy, returns nil.
|
||||
;; A clause with TEST = `else` always matches (sugar for $if's default).
|
||||
(define knl-cond-impl
|
||||
(fn (clauses dyn-env)
|
||||
(cond
|
||||
((or (nil? clauses) (= (length clauses) 0)) nil)
|
||||
(:else
|
||||
(let ((clause (first clauses)))
|
||||
(cond
|
||||
((not (list? clause))
|
||||
(error "$cond: each clause must be a list"))
|
||||
((= (length clause) 0)
|
||||
(error "$cond: empty clause"))
|
||||
((and (string? (first clause)) (= (first clause) "else"))
|
||||
(knl-cond-eval-body (rest clause) dyn-env))
|
||||
(:else
|
||||
(let ((test-val (kernel-eval (first clause) dyn-env)))
|
||||
(cond
|
||||
(test-val (knl-cond-eval-body (rest clause) dyn-env))
|
||||
(:else (knl-cond-impl (rest clauses) dyn-env)))))))))))
|
||||
|
||||
(define knl-cond-eval-body
|
||||
(fn (body dyn-env)
|
||||
(cond
|
||||
((or (nil? body) (= (length body) 0)) nil)
|
||||
((= (length body) 1) (kernel-eval (first body) dyn-env))
|
||||
(:else
|
||||
(begin
|
||||
(kernel-eval (first body) dyn-env)
|
||||
(knl-cond-eval-body (rest body) dyn-env))))))
|
||||
|
||||
(define kernel-cond-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env) (knl-cond-impl args dyn-env))))
|
||||
|
||||
;; $when COND BODY... — evaluate body iff COND is truthy; else nil.
|
||||
(define kernel-when-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((< (length args) 1)
|
||||
(error "$when: expects (cond body...)"))
|
||||
(:else
|
||||
(let ((c (kernel-eval (first args) dyn-env)))
|
||||
(cond
|
||||
(c (knl-cond-eval-body (rest args) dyn-env))
|
||||
(:else nil))))))))
|
||||
|
||||
;; $and? — short-circuit AND. Operative (not applicative) so untaken
|
||||
;; clauses are NOT evaluated. Empty $and? returns true (the identity).
|
||||
(define knl-and?-impl
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((or (nil? args) (= (length args) 0)) true)
|
||||
((= (length args) 1) (kernel-eval (first args) dyn-env))
|
||||
(:else
|
||||
(let ((v (kernel-eval (first args) dyn-env)))
|
||||
(cond
|
||||
(v (knl-and?-impl (rest args) dyn-env))
|
||||
(:else v)))))))
|
||||
|
||||
(define kernel-and?-operative
|
||||
(kernel-make-primitive-operative knl-and?-impl))
|
||||
|
||||
;; $or? — short-circuit OR. Operative; untaken clauses NOT evaluated.
|
||||
;; Empty $or? returns false (the identity).
|
||||
(define knl-or?-impl
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((or (nil? args) (= (length args) 0)) false)
|
||||
((= (length args) 1) (kernel-eval (first args) dyn-env))
|
||||
(:else
|
||||
(let ((v (kernel-eval (first args) dyn-env)))
|
||||
(cond
|
||||
(v v)
|
||||
(:else (knl-or?-impl (rest args) dyn-env))))))))
|
||||
|
||||
(define kernel-or?-operative
|
||||
(kernel-make-primitive-operative knl-or?-impl))
|
||||
|
||||
;; $unless COND BODY... — evaluate body iff COND is falsy; else nil.
|
||||
(define kernel-unless-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((< (length args) 1)
|
||||
(error "$unless: expects (cond body...)"))
|
||||
(:else
|
||||
(let ((c (kernel-eval (first args) dyn-env)))
|
||||
(cond
|
||||
(c nil)
|
||||
(:else (knl-cond-eval-body (rest args) dyn-env)))))))))
|
||||
|
||||
(define kernel-quasiquote-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 1))
|
||||
(error "$quasiquote: expects exactly 1 argument"))
|
||||
(:else (knl-quasi-walk (first args) dyn-env))))))
|
||||
|
||||
(define
|
||||
kernel-eval-applicative
|
||||
(kernel-make-primitive-applicative
|
||||
(fn
|
||||
(args)
|
||||
(cond
|
||||
((not (= (length args) 2))
|
||||
(error "eval: expects (expr env)"))
|
||||
((not (kernel-env? (nth args 1)))
|
||||
(error "eval: second arg must be a kernel env"))
|
||||
(:else (kernel-eval (first args) (nth args 1)))))))
|
||||
|
||||
(define
|
||||
kernel-make-environment-applicative
|
||||
(kernel-make-primitive-applicative
|
||||
(fn
|
||||
(args)
|
||||
(cond
|
||||
((= (length args) 0) (kernel-make-env))
|
||||
((= (length args) 1)
|
||||
(cond
|
||||
((not (kernel-env? (first args)))
|
||||
(error "make-environment: parent must be a kernel env"))
|
||||
(:else (kernel-extend-env (first args)))))
|
||||
(:else (error "make-environment: 0 or 1 argument"))))))
|
||||
|
||||
;; ── arithmetic and comparison (binary; trivial to extend later) ─
|
||||
|
||||
(define
|
||||
kernel-get-current-env-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 0))
|
||||
(error "get-current-environment: expects 0 arguments"))
|
||||
(:else dyn-env)))))
|
||||
|
||||
(define
|
||||
knl-bin-app
|
||||
(fn
|
||||
(name f)
|
||||
(kernel-make-primitive-applicative
|
||||
(fn
|
||||
(args)
|
||||
(cond
|
||||
((not (= (length args) 2))
|
||||
(error (str name ": expects 2 arguments")))
|
||||
(:else (f (first args) (nth args 1))))))))
|
||||
|
||||
;; Variadic left-fold helper. ZERO-RES is the identity (`(+)` → 0);
|
||||
;; ONE-FN handles single-arg case (`(- x)` negates; `(+ x)` returns x).
|
||||
(define knl-fold-step
|
||||
(fn (f acc rest-args)
|
||||
(cond
|
||||
((or (nil? rest-args) (= (length rest-args) 0)) acc)
|
||||
(:else
|
||||
(knl-fold-step f (f acc (first rest-args)) (rest rest-args))))))
|
||||
|
||||
(define knl-fold-app
|
||||
(fn (name f zero-res one-fn)
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args)
|
||||
(cond
|
||||
((= (length args) 0) zero-res)
|
||||
((= (length args) 1) (one-fn (first args)))
|
||||
(:else (knl-fold-step f (first args) (rest args))))))))
|
||||
|
||||
;; Variadic n-ary chained comparison: `(< 1 2 3)` ≡ `(< 1 2)` AND `(< 2 3)`.
|
||||
(define knl-chain-step
|
||||
(fn (cmp prev rest-args)
|
||||
(cond
|
||||
((or (nil? rest-args) (= (length rest-args) 0)) true)
|
||||
(:else
|
||||
(let ((next (first rest-args)))
|
||||
(cond
|
||||
((cmp prev next)
|
||||
(knl-chain-step cmp next (rest rest-args)))
|
||||
(:else false)))))))
|
||||
|
||||
(define knl-chain-cmp
|
||||
(fn (name cmp)
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args)
|
||||
(cond
|
||||
((< (length args) 2)
|
||||
(error (str name ": expects at least 2 arguments")))
|
||||
(:else (knl-chain-step cmp (first args) (rest args))))))))
|
||||
|
||||
;; ── list / pair primitives ──────────────────────────────────────
|
||||
|
||||
(define
|
||||
knl-unary-app
|
||||
(fn
|
||||
(name f)
|
||||
(kernel-make-primitive-applicative
|
||||
(fn
|
||||
(args)
|
||||
(cond
|
||||
((not (= (length args) 1))
|
||||
(error (str name ": expects 1 argument")))
|
||||
(:else (f (first args))))))))
|
||||
|
||||
(define kernel-cons-applicative (knl-bin-app "cons" (fn (a b) (cons a b))))
|
||||
|
||||
(define
|
||||
kernel-car-applicative
|
||||
(knl-unary-app
|
||||
"car"
|
||||
(fn
|
||||
(xs)
|
||||
(cond
|
||||
((or (nil? xs) (and (list? xs) (= (length xs) 0)))
|
||||
(error "car: empty list"))
|
||||
(:else (first xs))))))
|
||||
|
||||
(define
|
||||
kernel-cdr-applicative
|
||||
(knl-unary-app
|
||||
"cdr"
|
||||
(fn
|
||||
(xs)
|
||||
(cond
|
||||
((or (nil? xs) (and (list? xs) (= (length xs) 0)))
|
||||
(error "cdr: empty list"))
|
||||
(:else (rest xs))))))
|
||||
|
||||
(define
|
||||
kernel-list-applicative
|
||||
(kernel-make-primitive-applicative (fn (args) args)))
|
||||
|
||||
(define
|
||||
kernel-length-applicative
|
||||
(knl-unary-app "length" (fn (xs) (length xs))))
|
||||
|
||||
(define
|
||||
kernel-null?-applicative
|
||||
(knl-unary-app
|
||||
"null?"
|
||||
(fn (v) (or (nil? v) (and (list? v) (= (length v) 0))))))
|
||||
|
||||
;; ── boolean / equality ──────────────────────────────────────────
|
||||
|
||||
(define
|
||||
kernel-pair?-applicative
|
||||
(knl-unary-app
|
||||
"pair?"
|
||||
(fn (v) (and (list? v) (> (length v) 0)))))
|
||||
|
||||
(define knl-append-step
|
||||
(fn (xs ys)
|
||||
(cond
|
||||
((or (nil? xs) (= (length xs) 0)) ys)
|
||||
(:else (cons (first xs) (knl-append-step (rest xs) ys))))))
|
||||
|
||||
(define knl-all-lists?
|
||||
(fn (xs)
|
||||
(cond
|
||||
((or (nil? xs) (= (length xs) 0)) true)
|
||||
((list? (first xs)) (knl-all-lists? (rest xs)))
|
||||
(:else false))))
|
||||
|
||||
(define knl-append-all
|
||||
(fn (lists)
|
||||
(cond
|
||||
((or (nil? lists) (= (length lists) 0)) (list))
|
||||
((= (length lists) 1) (first lists))
|
||||
(:else
|
||||
(knl-append-step (first lists)
|
||||
(knl-append-all (rest lists)))))))
|
||||
|
||||
(define kernel-append-applicative
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args)
|
||||
(cond
|
||||
((knl-all-lists? args) (knl-append-all args))
|
||||
(:else (error "append: all arguments must be lists"))))))
|
||||
|
||||
(define knl-reverse-step
|
||||
(fn (xs acc)
|
||||
(cond
|
||||
((or (nil? xs) (= (length xs) 0)) acc)
|
||||
(:else (knl-reverse-step (rest xs) (cons (first xs) acc))))))
|
||||
|
||||
(define kernel-reverse-applicative
|
||||
(knl-unary-app "reverse"
|
||||
(fn (xs)
|
||||
(cond
|
||||
((not (list? xs)) (error "reverse: argument must be a list"))
|
||||
(:else (knl-reverse-step xs (list)))))))
|
||||
|
||||
(define kernel-not-applicative (knl-unary-app "not" (fn (v) (not v))))
|
||||
|
||||
;; Type predicates (Kernel-visible). Note `string?` covers BOTH symbols
|
||||
;; and string-literals in our representation (symbols are bare SX
|
||||
;; strings); a `kernel-string?` applicative distinguishes the two if
|
||||
;; needed.
|
||||
(define kernel-number?-applicative
|
||||
(knl-unary-app "number?" (fn (v) (number? v))))
|
||||
(define kernel-string?-applicative
|
||||
(knl-unary-app "string?" (fn (v) (string? v))))
|
||||
(define kernel-list?-applicative
|
||||
(knl-unary-app "list?" (fn (v) (list? v))))
|
||||
(define kernel-boolean?-applicative
|
||||
(knl-unary-app "boolean?" (fn (v) (boolean? v))))
|
||||
(define kernel-symbol?-applicative
|
||||
(knl-unary-app "symbol?" (fn (v) (string? v))))
|
||||
|
||||
(define kernel-eq?-applicative (knl-bin-app "eq?" (fn (a b) (= a b))))
|
||||
|
||||
;; ── the standard environment ────────────────────────────────────
|
||||
|
||||
(define
|
||||
kernel-equal?-applicative
|
||||
(knl-bin-app "equal?" (fn (a b) (= a b))))
|
||||
|
||||
;; ── List combinators: map / filter / reduce ─────────────────────
|
||||
;; These re-enter the evaluator on each element, so they use the
|
||||
;; with-env applicative constructor.
|
||||
|
||||
;; When the combiner is an applicative, we MUST unwrap before calling
|
||||
;; — otherwise kernel-combine will re-evaluate the already-evaluated
|
||||
;; element values (and crash if an element is itself a list).
|
||||
(define knl-apply-op
|
||||
(fn (combiner)
|
||||
(cond
|
||||
((kernel-applicative? combiner) (kernel-unwrap combiner))
|
||||
(:else combiner))))
|
||||
|
||||
(define knl-map-step
|
||||
(fn (fn-val xs dyn-env)
|
||||
(let ((op (knl-apply-op fn-val)))
|
||||
(knl-map-walk op xs dyn-env))))
|
||||
|
||||
(define knl-map-walk
|
||||
(fn (op xs dyn-env)
|
||||
(cond
|
||||
((or (nil? xs) (= (length xs) 0)) (list))
|
||||
(:else
|
||||
(cons (kernel-combine op (list (first xs)) dyn-env)
|
||||
(knl-map-walk op (rest xs) dyn-env))))))
|
||||
|
||||
(define kernel-map-applicative
|
||||
(kernel-make-primitive-applicative-with-env
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 2))
|
||||
(error "map: expects (fn list)"))
|
||||
((not (kernel-combiner? (first args)))
|
||||
(error "map: first arg must be a combiner"))
|
||||
((not (list? (nth args 1)))
|
||||
(error "map: second arg must be a list"))
|
||||
(:else (knl-map-step (first args) (nth args 1) dyn-env))))))
|
||||
|
||||
(define knl-filter-step
|
||||
(fn (pred xs dyn-env)
|
||||
(knl-filter-walk (knl-apply-op pred) xs dyn-env)))
|
||||
|
||||
(define knl-filter-walk
|
||||
(fn (op xs dyn-env)
|
||||
(cond
|
||||
((or (nil? xs) (= (length xs) 0)) (list))
|
||||
(:else
|
||||
(let ((keep? (kernel-combine op (list (first xs)) dyn-env)))
|
||||
(cond
|
||||
(keep?
|
||||
(cons (first xs) (knl-filter-walk op (rest xs) dyn-env)))
|
||||
(:else (knl-filter-walk op (rest xs) dyn-env))))))))
|
||||
|
||||
(define kernel-filter-applicative
|
||||
(kernel-make-primitive-applicative-with-env
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 2))
|
||||
(error "filter: expects (pred list)"))
|
||||
((not (kernel-combiner? (first args)))
|
||||
(error "filter: first arg must be a combiner"))
|
||||
((not (list? (nth args 1)))
|
||||
(error "filter: second arg must be a list"))
|
||||
(:else (knl-filter-step (first args) (nth args 1) dyn-env))))))
|
||||
|
||||
(define knl-reduce-step
|
||||
(fn (fn-val xs acc dyn-env)
|
||||
(knl-reduce-walk (knl-apply-op fn-val) xs acc dyn-env)))
|
||||
|
||||
(define knl-reduce-walk
|
||||
(fn (op xs acc dyn-env)
|
||||
(cond
|
||||
((or (nil? xs) (= (length xs) 0)) acc)
|
||||
(:else
|
||||
(knl-reduce-walk
|
||||
op
|
||||
(rest xs)
|
||||
(kernel-combine op (list acc (first xs)) dyn-env)
|
||||
dyn-env)))))
|
||||
|
||||
;; (apply COMBINER ARGS-LIST) — call COMBINER with the elements of
|
||||
;; ARGS-LIST as arguments. The Kernel canonical use: turn a constructed
|
||||
;; list of values into a function call. We skip the applicative's
|
||||
;; auto-eval step (via unwrap) because ARGS-LIST is already values, not
|
||||
;; expressions; for a bare operative, we pass through directly.
|
||||
(define kernel-apply-applicative
|
||||
(kernel-make-primitive-applicative-with-env
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 2))
|
||||
(error "apply: expects (combiner args-list)"))
|
||||
((not (kernel-combiner? (first args)))
|
||||
(error "apply: first arg must be a combiner"))
|
||||
((not (list? (nth args 1)))
|
||||
(error "apply: second arg must be a list"))
|
||||
(:else
|
||||
(let ((op (cond
|
||||
((kernel-applicative? (first args))
|
||||
(kernel-unwrap (first args)))
|
||||
(:else (first args)))))
|
||||
(kernel-combine op (nth args 1) dyn-env)))))))
|
||||
|
||||
(define kernel-reduce-applicative
|
||||
(kernel-make-primitive-applicative-with-env
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 3))
|
||||
(error "reduce: expects (fn init list)"))
|
||||
((not (kernel-combiner? (first args)))
|
||||
(error "reduce: first arg must be a combiner"))
|
||||
((not (list? (nth args 2)))
|
||||
(error "reduce: third arg must be a list"))
|
||||
(:else
|
||||
(knl-reduce-step (first args) (nth args 2)
|
||||
(nth args 1) dyn-env))))))
|
||||
|
||||
;; ── Encapsulations: Kernel's opaque-type idiom ──────────────────
|
||||
;;
|
||||
;; (make-encapsulation-type) → (encapsulator predicate decapsulator)
|
||||
;;
|
||||
;; Each call returns three applicatives over a fresh family identity.
|
||||
;; - (encapsulator V) → an opaque wrapper around V.
|
||||
;; - (predicate V) → true iff V was wrapped by THIS family.
|
||||
;; - (decapsulator W) → the inner value; errors on wrong family.
|
||||
;;
|
||||
;; Family identity is a fresh empty dict; SX compares dicts by reference,
|
||||
;; so two `(make-encapsulation-type)` calls return distinct families.
|
||||
;;
|
||||
;; Pattern usage (Phase 5 lacks destructuring, so accessors are explicit):
|
||||
;; ($define! triple (make-encapsulation-type))
|
||||
;; ($define! wrap-promise (car triple))
|
||||
;; ($define! promise? (car (cdr triple)))
|
||||
;; ($define! unwrap-promise (car (cdr (cdr triple))))
|
||||
|
||||
(define kernel-make-encap-type-impl
|
||||
(fn (args)
|
||||
(cond
|
||||
((not (= (length args) 0))
|
||||
(error "make-encapsulation-type: expects 0 arguments"))
|
||||
(:else
|
||||
(let ((family {}))
|
||||
(let ((encap
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (vargs)
|
||||
(cond
|
||||
((not (= (length vargs) 1))
|
||||
(error "encapsulator: expects 1 argument"))
|
||||
(:else
|
||||
{:knl-tag :encap
|
||||
:family family
|
||||
:value (first vargs)})))))
|
||||
(pred
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (vargs)
|
||||
(cond
|
||||
((not (= (length vargs) 1))
|
||||
(error "predicate: expects 1 argument"))
|
||||
(:else
|
||||
(let ((v (first vargs)))
|
||||
(and (dict? v)
|
||||
(= (get v :knl-tag) :encap)
|
||||
(= (get v :family) family))))))))
|
||||
(decap
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (vargs)
|
||||
(cond
|
||||
((not (= (length vargs) 1))
|
||||
(error "decapsulator: expects 1 argument"))
|
||||
(:else
|
||||
(let ((v (first vargs)))
|
||||
(cond
|
||||
((not (and (dict? v)
|
||||
(= (get v :knl-tag) :encap)))
|
||||
(error "decapsulator: not an encapsulation"))
|
||||
((not (= (get v :family) family))
|
||||
(error "decapsulator: wrong family"))
|
||||
(:else (get v :value))))))))))
|
||||
(list encap pred decap)))))))
|
||||
|
||||
(define kernel-make-encap-type-applicative
|
||||
(kernel-make-primitive-applicative kernel-make-encap-type-impl))
|
||||
|
||||
;; ── Hygiene: $let, $define-in!, make-environment ────────────────
|
||||
;;
|
||||
;; Kernel-on-SX is hygienic *by default* because user-defined operatives
|
||||
;; (Phase 3) bind their formals + any $define! in a CHILD env extending
|
||||
;; the operative's static-env, never the dyn-env. The caller's env is
|
||||
;; only mutated when code explicitly says so (e.g. `(eval expr env-arg)`).
|
||||
;;
|
||||
;; Phase 6 adds two helpers that make the property easy to lean on:
|
||||
;;
|
||||
;; ($let ((NAME EXPR) ...) BODY)
|
||||
;; Evaluates each EXPR in the calling env, binds NAME in a fresh
|
||||
;; child env, evaluates BODY in that child env. NAMES don't leak.
|
||||
;;
|
||||
;; ($define-in! ENV NAME EXPR)
|
||||
;; Binds NAME=value-of-EXPR in the *specified* env, not the dyn-env.
|
||||
;; Useful for operatives that need to mutate a sandbox env without
|
||||
;; touching their caller's env.
|
||||
;;
|
||||
;; Shutt's full scope-set / frame-stamp hygiene (lifted symbols carrying
|
||||
;; provenance markers so introduced bindings can shadow without
|
||||
;; capturing) is research-grade and not implemented here. Notes for
|
||||
;; `lib/guest/reflective/hygiene.sx` candidate API below the std env.
|
||||
|
||||
(define knl-bind-let-vals!
|
||||
(fn (local bindings dyn-env)
|
||||
(cond
|
||||
((or (nil? bindings) (= (length bindings) 0)) nil)
|
||||
(:else
|
||||
(let ((b (first bindings)))
|
||||
(cond
|
||||
((not (and (list? b) (= (length b) 2)))
|
||||
(error "$let: each binding must be (name expr)"))
|
||||
((not (string? (first b)))
|
||||
(error "$let: binding name must be a symbol"))
|
||||
(:else
|
||||
(begin
|
||||
(kernel-env-bind! local
|
||||
(first b)
|
||||
(kernel-eval (nth b 1) dyn-env))
|
||||
(knl-bind-let-vals! local (rest bindings) dyn-env)))))))))
|
||||
|
||||
(define kernel-let-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((< (length args) 2)
|
||||
(error "$let: expects (bindings body...)"))
|
||||
((not (list? (first args)))
|
||||
(error "$let: bindings must be a list"))
|
||||
(:else
|
||||
(let ((local (kernel-extend-env dyn-env)))
|
||||
(knl-bind-let-vals! local (first args) dyn-env)
|
||||
(knl-eval-body (rest args) local)))))))
|
||||
|
||||
;; $let* — sequential let. Each binding sees prior names in scope.
|
||||
;; Implemented by nesting envs one per binding; the body runs in the
|
||||
;; innermost env, so later bindings shadow earlier ones if names repeat.
|
||||
(define knl-let*-step
|
||||
(fn (bindings env body-forms)
|
||||
(cond
|
||||
((or (nil? bindings) (= (length bindings) 0))
|
||||
(knl-eval-body body-forms env))
|
||||
(:else
|
||||
(let ((b (first bindings)))
|
||||
(cond
|
||||
((not (and (list? b) (= (length b) 2)))
|
||||
(error "$let*: each binding must be (name expr)"))
|
||||
((not (string? (first b)))
|
||||
(error "$let*: binding name must be a symbol"))
|
||||
(:else
|
||||
(let ((child (kernel-extend-env env)))
|
||||
(kernel-env-bind! child
|
||||
(first b)
|
||||
(kernel-eval (nth b 1) env))
|
||||
(knl-let*-step (rest bindings) child body-forms)))))))))
|
||||
|
||||
(define kernel-let*-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((< (length args) 2)
|
||||
(error "$let*: expects (bindings body...)"))
|
||||
((not (list? (first args)))
|
||||
(error "$let*: bindings must be a list"))
|
||||
(:else
|
||||
(knl-let*-step (first args) dyn-env (rest args)))))))
|
||||
|
||||
(define kernel-define-in!-operative
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env)
|
||||
(cond
|
||||
((not (= (length args) 3))
|
||||
(error "$define-in!: expects (env-expr name expr)"))
|
||||
((not (string? (nth args 1)))
|
||||
(error "$define-in!: name must be a symbol"))
|
||||
(:else
|
||||
(let ((target (kernel-eval (first args) dyn-env)))
|
||||
(cond
|
||||
((not (kernel-env? target))
|
||||
(error "$define-in!: first arg must evaluate to an env"))
|
||||
(:else
|
||||
(let ((v (kernel-eval (nth args 2) dyn-env)))
|
||||
(kernel-env-bind! target (nth args 1) v)
|
||||
v)))))))))
|
||||
|
||||
(define
|
||||
kernel-standard-env
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((env (kernel-base-env)))
|
||||
(kernel-env-bind! env "$if" kernel-if-operative)
|
||||
(kernel-env-bind! env "$define!" kernel-define!-operative)
|
||||
(kernel-env-bind! env "$sequence" kernel-sequence-operative)
|
||||
(kernel-env-bind! env "$quote" kernel-quote-operative)
|
||||
(kernel-env-bind! env "$quasiquote" kernel-quasiquote-operative)
|
||||
(kernel-env-bind! env "$cond" kernel-cond-operative)
|
||||
(kernel-env-bind! env "$when" kernel-when-operative)
|
||||
(kernel-env-bind! env "$unless" kernel-unless-operative)
|
||||
(kernel-env-bind! env "$and?" kernel-and?-operative)
|
||||
(kernel-env-bind! env "$or?" kernel-or?-operative)
|
||||
(kernel-env-bind! env "eval" kernel-eval-applicative)
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"make-environment"
|
||||
kernel-make-environment-applicative)
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"get-current-environment"
|
||||
kernel-get-current-env-operative)
|
||||
(kernel-env-bind! env "+"
|
||||
(knl-fold-app "+" (fn (a b) (+ a b)) 0 (fn (x) x)))
|
||||
(kernel-env-bind! env "-"
|
||||
(knl-fold-app "-" (fn (a b) (- a b)) 0 (fn (x) (- 0 x))))
|
||||
(kernel-env-bind! env "*"
|
||||
(knl-fold-app "*" (fn (a b) (* a b)) 1 (fn (x) x)))
|
||||
(kernel-env-bind! env "/"
|
||||
(knl-fold-app "/" (fn (a b) (/ a b)) 1 (fn (x) (/ 1 x))))
|
||||
(kernel-env-bind! env "<" (knl-chain-cmp "<" (fn (a b) (< a b))))
|
||||
(kernel-env-bind! env ">" (knl-chain-cmp ">" (fn (a b) (> a b))))
|
||||
(kernel-env-bind! env "<=?" (knl-chain-cmp "<=?" (fn (a b) (<= a b))))
|
||||
(kernel-env-bind! env ">=?" (knl-chain-cmp ">=?" (fn (a b) (>= a b))))
|
||||
(kernel-env-bind! env "=?" kernel-eq?-applicative)
|
||||
(kernel-env-bind! env "equal?" kernel-equal?-applicative)
|
||||
(kernel-env-bind! env "eq?" kernel-eq?-applicative)
|
||||
(kernel-env-bind! env "cons" kernel-cons-applicative)
|
||||
(kernel-env-bind! env "car" kernel-car-applicative)
|
||||
(kernel-env-bind! env "cdr" kernel-cdr-applicative)
|
||||
(kernel-env-bind! env "list" kernel-list-applicative)
|
||||
(kernel-env-bind! env "length" kernel-length-applicative)
|
||||
(kernel-env-bind! env "null?" kernel-null?-applicative)
|
||||
(kernel-env-bind! env "pair?" kernel-pair?-applicative)
|
||||
(kernel-env-bind! env "map" kernel-map-applicative)
|
||||
(kernel-env-bind! env "filter" kernel-filter-applicative)
|
||||
(kernel-env-bind! env "reduce" kernel-reduce-applicative)
|
||||
(kernel-env-bind! env "apply" kernel-apply-applicative)
|
||||
(kernel-env-bind! env "append" kernel-append-applicative)
|
||||
(kernel-env-bind! env "reverse" kernel-reverse-applicative)
|
||||
(kernel-env-bind! env "number?" kernel-number?-applicative)
|
||||
(kernel-env-bind! env "string?" kernel-string?-applicative)
|
||||
(kernel-env-bind! env "list?" kernel-list?-applicative)
|
||||
(kernel-env-bind! env "boolean?" kernel-boolean?-applicative)
|
||||
(kernel-env-bind! env "symbol?" kernel-symbol?-applicative)
|
||||
(kernel-env-bind! env "not" kernel-not-applicative)
|
||||
(kernel-env-bind! env "make-encapsulation-type"
|
||||
kernel-make-encap-type-applicative)
|
||||
(kernel-env-bind! env "$let" kernel-let-operative)
|
||||
(kernel-env-bind! env "$let*" kernel-let*-operative)
|
||||
(kernel-env-bind! env "$define-in!" kernel-define-in!-operative)
|
||||
env)))
|
||||
183
lib/kernel/tests/encap.sx
Normal file
183
lib/kernel/tests/encap.sx
Normal file
@@ -0,0 +1,183 @@
|
||||
;; lib/kernel/tests/encap.sx — exercises make-encapsulation-type.
|
||||
;;
|
||||
;; The Phase 5 Kernel idiom: build opaque types whose constructor,
|
||||
;; predicate, and accessor are all standard Kernel applicatives. The
|
||||
;; identity is per-call, so two `(make-encapsulation-type)` calls
|
||||
;; produce non-interchangeable families.
|
||||
|
||||
(define ken-test-pass 0)
|
||||
(define ken-test-fail 0)
|
||||
(define ken-test-fails (list))
|
||||
|
||||
(define
|
||||
ken-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! ken-test-pass (+ ken-test-pass 1))
|
||||
(begin
|
||||
(set! ken-test-fail (+ ken-test-fail 1))
|
||||
(append! ken-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
(define ken-eval-in (fn (src env) (kernel-eval (kernel-parse src) env)))
|
||||
|
||||
;; A helper that builds a standard env with `encap`/`pred?`/`decap`
|
||||
;; bound from a single call to make-encapsulation-type.
|
||||
(define
|
||||
ken-make-encap-env
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ken-eval-in "($define! triple (make-encapsulation-type))" env)
|
||||
(ken-eval-in "($define! encap (car triple))" env)
|
||||
(ken-eval-in "($define! pred? (car (cdr triple)))" env)
|
||||
(ken-eval-in "($define! decap (car (cdr (cdr triple))))" env)
|
||||
env)))
|
||||
|
||||
;; ── construction ────────────────────────────────────────────────
|
||||
(ken-test
|
||||
"make: returns 3-element list"
|
||||
(ken-eval-in "(length (make-encapsulation-type))" (kernel-standard-env))
|
||||
3)
|
||||
|
||||
(ken-test
|
||||
"make: first is applicative"
|
||||
(kernel-applicative?
|
||||
(ken-eval-in "(car (make-encapsulation-type))" (kernel-standard-env)))
|
||||
true)
|
||||
|
||||
(ken-test
|
||||
"make: second is applicative"
|
||||
(kernel-applicative?
|
||||
(ken-eval-in
|
||||
"(car (cdr (make-encapsulation-type)))"
|
||||
(kernel-standard-env)))
|
||||
true)
|
||||
|
||||
(ken-test
|
||||
"make: third is applicative"
|
||||
(kernel-applicative?
|
||||
(ken-eval-in
|
||||
"(car (cdr (cdr (make-encapsulation-type))))"
|
||||
(kernel-standard-env)))
|
||||
true)
|
||||
|
||||
;; ── round-trip ──────────────────────────────────────────────────
|
||||
(ken-test
|
||||
"round-trip: number"
|
||||
(ken-eval-in "(decap (encap 42))" (ken-make-encap-env))
|
||||
42)
|
||||
|
||||
(ken-test
|
||||
"round-trip: string"
|
||||
(ken-eval-in "(decap (encap ($quote hello)))" (ken-make-encap-env))
|
||||
"hello")
|
||||
|
||||
(ken-test
|
||||
"round-trip: list"
|
||||
(ken-eval-in "(decap (encap (list 1 2 3)))" (ken-make-encap-env))
|
||||
(list 1 2 3))
|
||||
|
||||
;; ── predicate ───────────────────────────────────────────────────
|
||||
(ken-test
|
||||
"pred?: wrapped value"
|
||||
(ken-eval-in "(pred? (encap 1))" (ken-make-encap-env))
|
||||
true)
|
||||
|
||||
(ken-test
|
||||
"pred?: raw value"
|
||||
(ken-eval-in "(pred? 1)" (ken-make-encap-env))
|
||||
false)
|
||||
|
||||
(ken-test
|
||||
"pred?: raw string"
|
||||
(ken-eval-in "(pred? ($quote foo))" (ken-make-encap-env))
|
||||
false)
|
||||
|
||||
(ken-test
|
||||
"pred?: raw list"
|
||||
(ken-eval-in "(pred? (list))" (ken-make-encap-env))
|
||||
false)
|
||||
|
||||
;; ── opacity: different families are not interchangeable ─────────
|
||||
(ken-test
|
||||
"opacity: foreign value rejected by predicate"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ken-eval-in "($define! tA (make-encapsulation-type))" env)
|
||||
(ken-eval-in "($define! tB (make-encapsulation-type))" env)
|
||||
(ken-eval-in "($define! encA (car tA))" env)
|
||||
(ken-eval-in "($define! predB (car (cdr tB)))" env)
|
||||
(ken-eval-in "(predB (encA 42))" env))
|
||||
false)
|
||||
|
||||
(ken-test
|
||||
"opacity: decap rejects foreign value"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ken-eval-in "($define! tA (make-encapsulation-type))" env)
|
||||
(ken-eval-in "($define! tB (make-encapsulation-type))" env)
|
||||
(ken-eval-in "($define! encA (car tA))" env)
|
||||
(ken-eval-in "($define! decapB (car (cdr (cdr tB))))" env)
|
||||
(guard (e (true :raised)) (ken-eval-in "(decapB (encA 42))" env)))
|
||||
:raised)
|
||||
|
||||
(ken-test
|
||||
"opacity: decap rejects raw value"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(ken-eval-in "(decap 42)" (ken-make-encap-env)))
|
||||
:raised)
|
||||
|
||||
;; ── promise: classic Kernel encapsulation use case ──────────────
|
||||
;; A "promise" wraps a thunk to compute on demand and memoises the
|
||||
;; first result. Built entirely with the standard encap idiom.
|
||||
(ken-test
|
||||
"promise: force returns thunk result"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ken-eval-in
|
||||
"($sequence\n ($define! ptriple (make-encapsulation-type))\n ($define! make-promise (car ptriple))\n ($define! promise? (car (cdr ptriple)))\n ($define! decode-promise (car (cdr (cdr ptriple))))\n ($define! force ($lambda (p) ((decode-promise p))))\n ($define! delay ($lambda (thunk) (make-promise thunk)))\n (force (delay ($lambda () (+ 19 23)))))"
|
||||
env))
|
||||
42)
|
||||
|
||||
(ken-test
|
||||
"promise: promise? recognises its own type"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ken-eval-in
|
||||
"($sequence\n ($define! ptriple (make-encapsulation-type))\n ($define! make-promise (car ptriple))\n ($define! promise? (car (cdr ptriple)))\n (promise? (make-promise ($lambda () 42))))"
|
||||
env))
|
||||
true)
|
||||
|
||||
(ken-test
|
||||
"promise: promise? false on plain value"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ken-eval-in
|
||||
"($sequence\n ($define! ptriple (make-encapsulation-type))\n ($define! promise? (car (cdr ptriple)))\n (promise? 99))"
|
||||
env))
|
||||
false)
|
||||
|
||||
;; ── independent families don't leak ─────────────────────────────
|
||||
(ken-test
|
||||
"two families: distinct identity"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ken-eval-in
|
||||
"($sequence\n ($define! t1 (make-encapsulation-type))\n ($define! t2 (make-encapsulation-type))\n ($define! enc1 (car t1))\n ($define! pred2 (car (cdr t2)))\n (pred2 (enc1 ($quote stuff))))"
|
||||
env))
|
||||
false)
|
||||
|
||||
(ken-test
|
||||
"same family: re-bound shares identity"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ken-eval-in
|
||||
"($sequence\n ($define! t (make-encapsulation-type))\n ($define! e (car t))\n ($define! p (car (cdr t)))\n ($define! d (car (cdr (cdr t))))\n (list (p (e 7)) (d (e 7))))"
|
||||
env))
|
||||
(list true 7))
|
||||
|
||||
(define ken-tests-run! (fn () {:total (+ ken-test-pass ken-test-fail) :passed ken-test-pass :failed ken-test-fail :fails ken-test-fails}))
|
||||
270
lib/kernel/tests/eval.sx
Normal file
270
lib/kernel/tests/eval.sx
Normal file
@@ -0,0 +1,270 @@
|
||||
;; lib/kernel/tests/eval.sx — exercises lib/kernel/eval.sx.
|
||||
;;
|
||||
;; Phase 2 covers literal evaluation, symbol lookup, and combiner
|
||||
;; dispatch (operative vs applicative). Standard-environment operatives
|
||||
;; ($if, $define!, $lambda, …) arrive in Phase 4, so tests build a
|
||||
;; minimal env on the fly and verify the dispatch contract directly.
|
||||
|
||||
(define ke-test-pass 0)
|
||||
(define ke-test-fail 0)
|
||||
(define ke-test-fails (list))
|
||||
|
||||
(define
|
||||
ke-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! ke-test-pass (+ ke-test-pass 1))
|
||||
(begin
|
||||
(set! ke-test-fail (+ ke-test-fail 1))
|
||||
(append! ke-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
;; ── helpers ──────────────────────────────────────────────────────
|
||||
|
||||
(define ke-eval-src (fn (src env) (kernel-eval (kernel-parse src) env)))
|
||||
|
||||
(define
|
||||
ke-make-test-env
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((env (kernel-make-env)))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"+"
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args) (+ (first args) (nth args 1)))))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"list"
|
||||
(kernel-make-primitive-applicative (fn (args) args)))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"$quote"
|
||||
(kernel-make-primitive-operative (fn (args dyn-env) (first args))))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"$if"
|
||||
(kernel-make-primitive-operative
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(if
|
||||
(kernel-eval (first args) dyn-env)
|
||||
(kernel-eval (nth args 1) dyn-env)
|
||||
(kernel-eval (nth args 2) dyn-env)))))
|
||||
env)))
|
||||
|
||||
;; ── literal evaluation ───────────────────────────────────────────
|
||||
(ke-test "lit: number" (ke-eval-src "42" (kernel-make-env)) 42)
|
||||
(ke-test "lit: zero" (ke-eval-src "0" (kernel-make-env)) 0)
|
||||
(ke-test "lit: float" (ke-eval-src "3.14" (kernel-make-env)) 3.14)
|
||||
(ke-test "lit: true" (ke-eval-src "#t" (kernel-make-env)) true)
|
||||
(ke-test "lit: false" (ke-eval-src "#f" (kernel-make-env)) false)
|
||||
(ke-test "lit: string" (ke-eval-src "\"hello\"" (kernel-make-env)) "hello")
|
||||
(ke-test "lit: empty list" (ke-eval-src "()" (kernel-make-env)) (list))
|
||||
|
||||
;; ── symbol lookup ────────────────────────────────────────────────
|
||||
(ke-test
|
||||
"sym: bound to number"
|
||||
(let
|
||||
((env (kernel-make-env)))
|
||||
(kernel-env-bind! env "x" 100)
|
||||
(ke-eval-src "x" env))
|
||||
100)
|
||||
|
||||
(ke-test
|
||||
"sym: bound to string"
|
||||
(let
|
||||
((env (kernel-make-env)))
|
||||
(kernel-env-bind! env "name" "kernel")
|
||||
(ke-eval-src "name" env))
|
||||
"kernel")
|
||||
|
||||
(ke-test
|
||||
"sym: parent-chain lookup"
|
||||
(let
|
||||
((p (kernel-make-env)))
|
||||
(kernel-env-bind! p "outer" 1)
|
||||
(let
|
||||
((c (kernel-extend-env p)))
|
||||
(kernel-env-bind! c "inner" 2)
|
||||
(+ (ke-eval-src "outer" c) (ke-eval-src "inner" c))))
|
||||
3)
|
||||
|
||||
(ke-test
|
||||
"sym: child shadows parent"
|
||||
(let
|
||||
((p (kernel-make-env)))
|
||||
(kernel-env-bind! p "x" 1)
|
||||
(let
|
||||
((c (kernel-extend-env p)))
|
||||
(kernel-env-bind! c "x" 2)
|
||||
(ke-eval-src "x" c)))
|
||||
2)
|
||||
|
||||
(ke-test
|
||||
"env-has?: present"
|
||||
(let
|
||||
((env (kernel-make-env)))
|
||||
(kernel-env-bind! env "x" 1)
|
||||
(kernel-env-has? env "x"))
|
||||
true)
|
||||
|
||||
(ke-test
|
||||
"env-has?: missing"
|
||||
(kernel-env-has? (kernel-make-env) "nope")
|
||||
false)
|
||||
|
||||
;; ── tagged-value predicates ─────────────────────────────────────
|
||||
(ke-test
|
||||
"tag: operative?"
|
||||
(kernel-operative? (kernel-make-primitive-operative (fn (a e) nil)))
|
||||
true)
|
||||
|
||||
(ke-test
|
||||
"tag: applicative?"
|
||||
(kernel-applicative? (kernel-make-primitive-applicative (fn (a) nil)))
|
||||
true)
|
||||
|
||||
(ke-test
|
||||
"tag: combiner? operative"
|
||||
(kernel-combiner? (kernel-make-primitive-operative (fn (a e) nil)))
|
||||
true)
|
||||
|
||||
(ke-test
|
||||
"tag: combiner? applicative"
|
||||
(kernel-combiner? (kernel-make-primitive-applicative (fn (a) nil)))
|
||||
true)
|
||||
|
||||
(ke-test "tag: combiner? number" (kernel-combiner? 42) false)
|
||||
|
||||
(ke-test "tag: number is not operative" (kernel-operative? 42) false)
|
||||
|
||||
;; ── wrap / unwrap ────────────────────────────────────────────────
|
||||
(ke-test
|
||||
"wrap+unwrap roundtrip"
|
||||
(let
|
||||
((op (kernel-make-primitive-operative (fn (a e) :sentinel))))
|
||||
(= (kernel-unwrap (kernel-wrap op)) op))
|
||||
true)
|
||||
|
||||
(ke-test
|
||||
"wrap produces applicative"
|
||||
(kernel-applicative?
|
||||
(kernel-wrap (kernel-make-primitive-operative (fn (a e) nil))))
|
||||
true)
|
||||
|
||||
(ke-test
|
||||
"unwrap of primitive-applicative is operative"
|
||||
(kernel-operative?
|
||||
(kernel-unwrap (kernel-make-primitive-applicative (fn (a) nil))))
|
||||
true)
|
||||
|
||||
;; ── combiner dispatch — applicatives evaluate their args ─────────
|
||||
(ke-test
|
||||
"applicative: simple call"
|
||||
(ke-eval-src "(+ 2 3)" (ke-make-test-env))
|
||||
5)
|
||||
|
||||
(ke-test
|
||||
"applicative: nested"
|
||||
(ke-eval-src "(+ (+ 1 2) (+ 3 4))" (ke-make-test-env))
|
||||
10)
|
||||
|
||||
(ke-test
|
||||
"applicative: receives evaluated args"
|
||||
(let
|
||||
((env (ke-make-test-env)))
|
||||
(kernel-env-bind! env "x" 10)
|
||||
(kernel-env-bind! env "y" 20)
|
||||
(ke-eval-src "(+ x y)" env))
|
||||
30)
|
||||
|
||||
(ke-test
|
||||
"applicative: list builds an SX list of values"
|
||||
(let
|
||||
((env (ke-make-test-env)))
|
||||
(kernel-env-bind! env "a" 1)
|
||||
(kernel-env-bind! env "b" 2)
|
||||
(ke-eval-src "(list a b 99)" env))
|
||||
(list 1 2 99))
|
||||
|
||||
;; ── combiner dispatch — operatives DO NOT evaluate their args ───
|
||||
(ke-test
|
||||
"operative: $quote returns symbol unevaluated"
|
||||
(ke-eval-src "($quote foo)" (ke-make-test-env))
|
||||
"foo")
|
||||
|
||||
(ke-test
|
||||
"operative: $quote returns list unevaluated"
|
||||
(ke-eval-src "($quote (+ 1 2))" (ke-make-test-env))
|
||||
(list "+" 1 2))
|
||||
|
||||
(ke-test
|
||||
"operative: $if true branch"
|
||||
(ke-eval-src "($if #t 1 2)" (ke-make-test-env))
|
||||
1)
|
||||
|
||||
(ke-test
|
||||
"operative: $if false branch"
|
||||
(ke-eval-src "($if #f 1 2)" (ke-make-test-env))
|
||||
2)
|
||||
|
||||
(ke-test
|
||||
"operative: $if doesn't eval untaken branch"
|
||||
(ke-eval-src "($if #t 99 unbound)" (ke-make-test-env))
|
||||
99)
|
||||
|
||||
(ke-test
|
||||
"operative: $if takes dynamic env for branches"
|
||||
(let
|
||||
((env (ke-make-test-env)))
|
||||
(kernel-env-bind! env "x" 7)
|
||||
(ke-eval-src "($if #t x 0)" env))
|
||||
7)
|
||||
|
||||
;; ── operative built ON-THE-FLY can inspect raw expressions ──────
|
||||
(ke-test
|
||||
"operative: sees raw symbol head"
|
||||
(let
|
||||
((env (kernel-make-env)))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"head"
|
||||
(kernel-make-primitive-operative (fn (args dyn-env) (first args))))
|
||||
(ke-eval-src "(head (+ 1 2))" env))
|
||||
(list "+" 1 2))
|
||||
|
||||
(ke-test
|
||||
"operative: sees dynamic env"
|
||||
(let
|
||||
((env (kernel-make-env)))
|
||||
(kernel-env-bind! env "x" 999)
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"$probe"
|
||||
(kernel-make-primitive-operative
|
||||
(fn (args dyn-env) (kernel-env-lookup dyn-env "x"))))
|
||||
(ke-eval-src "($probe ignored)" env))
|
||||
999)
|
||||
|
||||
;; ── error cases ──────────────────────────────────────────────────
|
||||
(ke-test
|
||||
"error: unbound symbol"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(kernel-eval (kernel-parse "nope") (kernel-make-env)))
|
||||
:raised)
|
||||
|
||||
(ke-test
|
||||
"error: combine non-combiner"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(let
|
||||
((env (kernel-make-env)))
|
||||
(kernel-env-bind! env "x" 42)
|
||||
(kernel-eval (kernel-parse "(x 1)") env)))
|
||||
:raised)
|
||||
|
||||
(define ke-tests-run! (fn () {:total (+ ke-test-pass ke-test-fail) :passed ke-test-pass :failed ke-test-fail :fails ke-test-fails}))
|
||||
220
lib/kernel/tests/hygiene.sx
Normal file
220
lib/kernel/tests/hygiene.sx
Normal file
@@ -0,0 +1,220 @@
|
||||
;; lib/kernel/tests/hygiene.sx — exercises Phase 6 hygiene helpers.
|
||||
;;
|
||||
;; Kernel-on-SX is hygienic by default: $vau/$lambda close over their
|
||||
;; static env, and bind their formals (plus any $define!s in the body)
|
||||
;; in a CHILD env. The caller's env is only mutated when user code
|
||||
;; explicitly threads the env-param through `eval` or `$define-in!`.
|
||||
;;
|
||||
;; These tests verify the property, plus the Phase 6 helpers ($let and
|
||||
;; $define-in!). Shutt's full scope-set hygiene (lifted symbols with
|
||||
;; provenance markers) is research-grade and is NOT implemented — see
|
||||
;; the plan's reflective-API notes for the proposed approach.
|
||||
|
||||
(define kh-test-pass 0)
|
||||
(define kh-test-fail 0)
|
||||
(define kh-test-fails (list))
|
||||
|
||||
(define
|
||||
kh-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! kh-test-pass (+ kh-test-pass 1))
|
||||
(begin
|
||||
(set! kh-test-fail (+ kh-test-fail 1))
|
||||
(append! kh-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
(define kh-eval-in (fn (src env) (kernel-eval (kernel-parse src) env)))
|
||||
|
||||
;; ── Default hygiene: $define! inside operative body stays local ─
|
||||
|
||||
(kh-test
|
||||
"hygiene: vau body $define! doesn't escape"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! x 1)" env)
|
||||
(kh-eval-in
|
||||
"($define! my-op ($vau () _ ($sequence ($define! x 999) x)))"
|
||||
env)
|
||||
(kh-eval-in "(my-op)" env)
|
||||
(kh-eval-in "x" env))
|
||||
1)
|
||||
|
||||
(kh-test
|
||||
"hygiene: vau body $define! visible inside body"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! x 1)" env)
|
||||
(kh-eval-in
|
||||
"($define! my-op ($vau () _ ($sequence ($define! x 999) x)))"
|
||||
env)
|
||||
(kh-eval-in "(my-op)" env))
|
||||
999)
|
||||
|
||||
(kh-test
|
||||
"hygiene: lambda body $define! doesn't escape"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! y 50)" env)
|
||||
(kh-eval-in "($define! f ($lambda () ($sequence ($define! y 7) y)))" env)
|
||||
(kh-eval-in "(f)" env)
|
||||
(kh-eval-in "y" env))
|
||||
50)
|
||||
|
||||
(kh-test
|
||||
"hygiene: caller's binding visible inside operative"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! caller-x 88)" env)
|
||||
(kh-eval-in "($define! my-op ($vau () _ caller-x))" env)
|
||||
(kh-eval-in "(my-op)" env))
|
||||
88)
|
||||
|
||||
;; ── $let — proper hygienic scoping ──────────────────────────────
|
||||
|
||||
(kh-test
|
||||
"let: returns body value"
|
||||
(kh-eval-in "($let ((x 5)) (+ x 1))" (kernel-standard-env))
|
||||
6)
|
||||
|
||||
(kh-test
|
||||
"let: multiple bindings"
|
||||
(kh-eval-in "($let ((x 3) (y 4)) (+ x y))" (kernel-standard-env))
|
||||
7)
|
||||
|
||||
(kh-test
|
||||
"let: bindings shadow outer"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! x 1)" env)
|
||||
(kh-eval-in "($let ((x 99)) x)" env))
|
||||
99)
|
||||
|
||||
(kh-test
|
||||
"let: bindings don't leak after"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! x 1)" env)
|
||||
(kh-eval-in "($let ((x 99)) x)" env)
|
||||
(kh-eval-in "x" env))
|
||||
1)
|
||||
|
||||
(kh-test
|
||||
"let: parallel — RHS sees outer, not inner"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! x 1)" env)
|
||||
(kh-eval-in "($let ((x 10) (y x)) y)" env))
|
||||
1)
|
||||
|
||||
(kh-test
|
||||
"let: nested"
|
||||
(kh-eval-in "($let ((x 1)) ($let ((y 2)) (+ x y)))" (kernel-standard-env))
|
||||
3)
|
||||
|
||||
(kh-test
|
||||
"let: error on malformed binding"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(kh-eval-in "($let ((x)) x)" (kernel-standard-env)))
|
||||
:raised)
|
||||
|
||||
(kh-test
|
||||
"let: error on non-symbol name"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(kh-eval-in "($let ((1 2)) 1)" (kernel-standard-env)))
|
||||
:raised)
|
||||
|
||||
;; ── $define-in! — explicit env targeting ────────────────────────
|
||||
|
||||
(kh-test
|
||||
"define-in!: binds in chosen env, not dyn-env"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! sandbox (make-environment))" env)
|
||||
(kh-eval-in "($define-in! sandbox z 77)" env)
|
||||
(kernel-env-has? (kh-eval-in "sandbox" env) "z"))
|
||||
true)
|
||||
|
||||
(kh-test
|
||||
"define-in!: doesn't pollute caller"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! sandbox (make-environment))" env)
|
||||
(kh-eval-in "($define-in! sandbox z 77)" env)
|
||||
(kernel-env-has? env "z"))
|
||||
false)
|
||||
|
||||
(kh-test
|
||||
"define-in!: error on non-env target"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define-in! 42 x 1)" env)))
|
||||
:raised)
|
||||
|
||||
;; ── Closure does NOT see post-definition caller binds ───────────
|
||||
;; The classic "lexical scope wins over dynamic" test.
|
||||
|
||||
(kh-test
|
||||
"lexical: closure sees its own static env"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! x 1)" env)
|
||||
(kh-eval-in "($define! get-x ($lambda () x))" env)
|
||||
(kh-eval-in "($define! x 999)" env)
|
||||
(kh-eval-in "(get-x)" env))
|
||||
999)
|
||||
|
||||
(kh-test
|
||||
"lexical: $let-bound name invisible outside"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($let ((private 42)) private)" env)
|
||||
(kh-eval-in "private" env)))
|
||||
:raised)
|
||||
|
||||
;; ── Operative + $let: hygiene compose ───────────────────────────
|
||||
|
||||
(kh-test
|
||||
"let-inside-vau: temp doesn't escape body"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! x 1)" env)
|
||||
(kh-eval-in "($define! op ($vau () _ ($let ((x 5)) x)))" env)
|
||||
(kh-eval-in "(op)" env)
|
||||
(kh-eval-in "x" env))
|
||||
1)
|
||||
|
||||
;; ── $let* — sequential let ──────────────────────────────────────
|
||||
(kh-test "let*: empty bindings"
|
||||
(kh-eval-in "($let* () 42)" (kernel-standard-env)) 42)
|
||||
(kh-test "let*: single binding"
|
||||
(kh-eval-in "($let* ((x 5)) (+ x 1))" (kernel-standard-env)) 6)
|
||||
(kh-test "let*: later sees earlier"
|
||||
(kh-eval-in "($let* ((x 1) (y (+ x 1)) (z (+ y 1))) z)"
|
||||
(kernel-standard-env)) 3)
|
||||
(kh-test "let*: bindings don't leak after"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(kh-eval-in "($define! x 1)" env)
|
||||
(kh-eval-in "($let* ((x 99) (y (+ x 1))) y)" env)
|
||||
(kh-eval-in "x" env)) 1)
|
||||
(kh-test "let*: same-name later binding shadows earlier"
|
||||
(kh-eval-in "($let* ((x 1) (x 2)) x)" (kernel-standard-env)) 2)
|
||||
(kh-test "let*: multi-expression body"
|
||||
(kh-eval-in "($let* ((x 5)) ($define! double (+ x x)) double)"
|
||||
(kernel-standard-env)) 10)
|
||||
(kh-test "let*: error on malformed binding"
|
||||
(guard (e (true :raised))
|
||||
(kh-eval-in "($let* ((x)) x)" (kernel-standard-env)))
|
||||
:raised)
|
||||
(kh-test "let: multi-body"
|
||||
(kh-eval-in "($let ((x 5)) ($define! tmp (+ x 1)) tmp)"
|
||||
(kernel-standard-env)) 6)
|
||||
|
||||
(define kh-tests-run! (fn () {:total (+ kh-test-pass kh-test-fail) :passed kh-test-pass :failed kh-test-fail :fails kh-test-fails}))
|
||||
162
lib/kernel/tests/metacircular.sx
Normal file
162
lib/kernel/tests/metacircular.sx
Normal file
@@ -0,0 +1,162 @@
|
||||
;; lib/kernel/tests/metacircular.sx — Kernel-in-Kernel demo.
|
||||
;;
|
||||
;; Demonstrates reflective completeness: a Kernel program implements
|
||||
;; a recognisable subset of Kernel's own evaluation rules and produces
|
||||
;; matching values for a battery of test programs.
|
||||
;;
|
||||
;; This is a SHALLOW metacircular: it dispatches on expression shape
|
||||
;; itself (numbers, booleans, lists, symbols), recursively meta-evals
|
||||
;; each argument of an applicative call, and delegates only to the
|
||||
;; host evaluator for the leaf cases (operatives, symbol lookup). The
|
||||
;; point is to show that env-as-value, first-class operatives, and
|
||||
;; first-class evaluators all line up — enough so a Kernel program
|
||||
;; can itself reason about Kernel programs.
|
||||
|
||||
(define kmc-test-pass 0)
|
||||
(define kmc-test-fail 0)
|
||||
(define kmc-test-fails (list))
|
||||
|
||||
(define
|
||||
kmc-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! kmc-test-pass (+ kmc-test-pass 1))
|
||||
(begin
|
||||
(set! kmc-test-fail (+ kmc-test-fail 1))
|
||||
(append! kmc-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
;; Build a Kernel env with m-eval and m-apply defined. The two refer
|
||||
;; to each other and to standard primitives, so we use the standard
|
||||
;; env as the static-env for both.
|
||||
(define
|
||||
kmc-make-env
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(kernel-eval
|
||||
(kernel-parse
|
||||
"($define! m-eval\n ($lambda (expr env)\n ($cond\n ((number? expr) expr)\n ((boolean? expr) expr)\n ((null? expr) expr)\n ((symbol? expr) (eval expr env))\n ((list? expr)\n ($let ((head-val (m-eval (car expr) env)))\n ($cond\n ((applicative? head-val)\n (apply head-val\n (map ($lambda (a) (m-eval a env)) (cdr expr))))\n (else (eval expr env)))))\n (else expr))))")
|
||||
env)
|
||||
env)))
|
||||
|
||||
(define
|
||||
kmc-eval
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((env (kmc-make-env)))
|
||||
(kernel-eval
|
||||
(kernel-parse
|
||||
(str "(m-eval (quote " src ") (get-current-environment))"))
|
||||
env))))
|
||||
|
||||
;; ── literals self-evaluate via m-eval ──────────────────────────
|
||||
(kmc-test
|
||||
"m-eval: integer literal"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval 42 (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
42)
|
||||
|
||||
(kmc-test
|
||||
"m-eval: boolean true"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval #t (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
true)
|
||||
|
||||
(kmc-test
|
||||
"m-eval: boolean false"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval #f (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
false)
|
||||
|
||||
(kmc-test
|
||||
"m-eval: empty list"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval () (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
(list))
|
||||
|
||||
;; ── symbol lookup goes through env ─────────────────────────────
|
||||
(kmc-test
|
||||
"m-eval: symbol lookup"
|
||||
(let
|
||||
((env (kmc-make-env)))
|
||||
(kernel-eval (kernel-parse "($define! shared-x 99)") env)
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote shared-x) (get-current-environment))")
|
||||
env))
|
||||
99)
|
||||
|
||||
;; ── applicative calls are dispatched by m-eval recursively ─────
|
||||
(kmc-test
|
||||
"m-eval: addition"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote (+ 1 2)) (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
3)
|
||||
|
||||
(kmc-test
|
||||
"m-eval: nested arithmetic"
|
||||
(kernel-eval
|
||||
(kernel-parse
|
||||
"(m-eval ($quote (+ (* 2 3) (- 10 4))) (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
12)
|
||||
|
||||
(kmc-test
|
||||
"m-eval: variadic +"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote (+ 1 2 3 4 5)) (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
15)
|
||||
|
||||
(kmc-test
|
||||
"m-eval: list construction"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote (list 1 2 3)) (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
(list 1 2 3))
|
||||
|
||||
(kmc-test "m-eval: cons reverse-style"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote (cons 0 (list 1 2))) (get-current-environment))")
|
||||
(kmc-make-env)) (list 0 1 2))
|
||||
|
||||
(kmc-test "m-eval: nested apply"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote (apply + (list 10 20 30))) (get-current-environment))")
|
||||
(kmc-make-env)) 60)
|
||||
|
||||
;; ── operatives delegate to host eval (transparently for the caller) ─
|
||||
(kmc-test
|
||||
"m-eval: $if true branch (via delegation)"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote ($if #t 1 2)) (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
1)
|
||||
|
||||
(kmc-test
|
||||
"m-eval: $if false branch"
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote ($if #f 1 2)) (get-current-environment))")
|
||||
(kmc-make-env))
|
||||
2)
|
||||
|
||||
;; ── m-eval can call a user-defined lambda ──────────────────────
|
||||
(kmc-test
|
||||
"m-eval: user lambda call"
|
||||
(let
|
||||
((env (kmc-make-env)))
|
||||
(kernel-eval (kernel-parse "($define! sq ($lambda (x) (* x x)))") env)
|
||||
(kernel-eval
|
||||
(kernel-parse "(m-eval ($quote (sq 7)) (get-current-environment))")
|
||||
env))
|
||||
49)
|
||||
|
||||
(define kmc-tests-run! (fn () {:total (+ kmc-test-pass kmc-test-fail) :passed kmc-test-pass :failed kmc-test-fail :fails kmc-test-fails}))
|
||||
158
lib/kernel/tests/parse.sx
Normal file
158
lib/kernel/tests/parse.sx
Normal file
@@ -0,0 +1,158 @@
|
||||
;; lib/kernel/tests/parse.sx — exercises lib/kernel/parser.sx.
|
||||
|
||||
(define knl-test-pass 0)
|
||||
(define knl-test-fail 0)
|
||||
(define knl-test-fails (list))
|
||||
|
||||
(define
|
||||
knl-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! knl-test-pass (+ knl-test-pass 1))
|
||||
(begin
|
||||
(set! knl-test-fail (+ knl-test-fail 1))
|
||||
(append! knl-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
;; ── atoms: numbers ────────────────────────────────────────────────
|
||||
(knl-test "num: integer" (kernel-parse "42") 42)
|
||||
(knl-test "num: zero" (kernel-parse "0") 0)
|
||||
(knl-test "num: negative integer" (kernel-parse "-7") -7)
|
||||
(knl-test "num: positive sign" (kernel-parse "+5") 5)
|
||||
(knl-test "num: float" (kernel-parse "3.14") 3.14)
|
||||
(knl-test "num: negative float" (kernel-parse "-2.5") -2.5)
|
||||
(knl-test "num: leading dot" (kernel-parse ".5") 0.5)
|
||||
(knl-test "num: exponent" (kernel-parse "1e3") 1000)
|
||||
(knl-test "num: exponent with sign" (kernel-parse "2.5e-1") 0.25)
|
||||
(knl-test "num: capital E exponent" (kernel-parse "1E2") 100)
|
||||
|
||||
;; ── atoms: booleans ───────────────────────────────────────────────
|
||||
(knl-test "bool: true" (kernel-parse "#t") true)
|
||||
(knl-test "bool: false" (kernel-parse "#f") false)
|
||||
|
||||
;; ── atoms: empty list (Kernel nil) ────────────────────────────────
|
||||
(knl-test "nil: ()" (kernel-parse "()") (list))
|
||||
(knl-test "nil: (= () (list))" (= (kernel-parse "()") (list)) true)
|
||||
|
||||
;; ── atoms: symbols ────────────────────────────────────────────────
|
||||
(knl-test "sym: word" (kernel-parse "foo") "foo")
|
||||
(knl-test "sym: hyphenated" (kernel-parse "foo-bar") "foo-bar")
|
||||
(knl-test "sym: dollar-bang" (kernel-parse "$define!") "$define!")
|
||||
(knl-test "sym: question" (kernel-parse "null?") "null?")
|
||||
(knl-test "sym: lt-eq" (kernel-parse "<=") "<=")
|
||||
(knl-test "sym: bare plus" (kernel-parse "+") "+")
|
||||
(knl-test "sym: bare minus" (kernel-parse "-") "-")
|
||||
(knl-test "sym: plus-letter" (kernel-parse "+a") "+a")
|
||||
(knl-test "sym: arrow" (kernel-parse "->") "->")
|
||||
(knl-test "sym: dot-prefixed" (kernel-parse ".foo") ".foo")
|
||||
|
||||
;; ── atoms: strings ────────────────────────────────────────────────
|
||||
(knl-test "str: empty" (kernel-string-value (kernel-parse "\"\"")) "")
|
||||
(knl-test
|
||||
"str: hello"
|
||||
(kernel-string-value (kernel-parse "\"hello\""))
|
||||
"hello")
|
||||
(knl-test "str: predicate" (kernel-string? (kernel-parse "\"x\"")) true)
|
||||
(knl-test "str: not symbol" (kernel-string? (kernel-parse "x")) false)
|
||||
(knl-test
|
||||
"str: escape newline"
|
||||
(kernel-string-value (kernel-parse "\"a\\nb\""))
|
||||
"a\nb")
|
||||
(knl-test
|
||||
"str: escape tab"
|
||||
(kernel-string-value (kernel-parse "\"a\\tb\""))
|
||||
"a\tb")
|
||||
(knl-test
|
||||
"str: escape quote"
|
||||
(kernel-string-value (kernel-parse "\"a\\\"b\""))
|
||||
"a\"b")
|
||||
(knl-test
|
||||
"str: escape backslash"
|
||||
(kernel-string-value (kernel-parse "\"a\\\\b\""))
|
||||
"a\\b")
|
||||
|
||||
;; ── lists ─────────────────────────────────────────────────────────
|
||||
(knl-test "list: flat" (kernel-parse "(a b c)") (list "a" "b" "c"))
|
||||
(knl-test
|
||||
"list: nested"
|
||||
(kernel-parse "(a (b c) d)")
|
||||
(list "a" (list "b" "c") "d"))
|
||||
(knl-test
|
||||
"list: deeply nested"
|
||||
(kernel-parse "(((x)))")
|
||||
(list (list (list "x"))))
|
||||
(knl-test
|
||||
"list: mixed atoms"
|
||||
(kernel-parse "(1 #t foo)")
|
||||
(list 1 true "foo"))
|
||||
(knl-test
|
||||
"list: empty inside"
|
||||
(kernel-parse "(a () b)")
|
||||
(list "a" (list) "b"))
|
||||
|
||||
;; ── whitespace + comments ─────────────────────────────────────────
|
||||
(knl-test "ws: leading" (kernel-parse " 42") 42)
|
||||
(knl-test "ws: trailing" (kernel-parse "42 ") 42)
|
||||
(knl-test "ws: tabs/newlines" (kernel-parse "\n\t 42 \n") 42)
|
||||
(knl-test "comment: line" (kernel-parse "; nope\n42") 42)
|
||||
(knl-test "comment: trailing" (kernel-parse "42 ; tail") 42)
|
||||
(knl-test
|
||||
"comment: inside list"
|
||||
(kernel-parse "(a ; mid\n b)")
|
||||
(list "a" "b"))
|
||||
|
||||
;; ── parse-all ─────────────────────────────────────────────────────
|
||||
(knl-test "all: empty input" (kernel-parse-all "") (list))
|
||||
(knl-test "all: only whitespace" (kernel-parse-all " ") (list))
|
||||
(knl-test "all: only comment" (kernel-parse-all "; nope") (list))
|
||||
(knl-test
|
||||
"all: three forms"
|
||||
(kernel-parse-all "1 2 3")
|
||||
(list 1 2 3))
|
||||
(knl-test
|
||||
"all: mixed"
|
||||
(kernel-parse-all "($if #t 1 2) foo")
|
||||
(list (list "$if" true 1 2) "foo"))
|
||||
|
||||
;; ── classic Kernel programs (smoke) ───────────────────────────────
|
||||
(knl-test
|
||||
"klisp: vau form"
|
||||
(kernel-parse "($vau (x e) e (eval x e))")
|
||||
(list "$vau" (list "x" "e") "e" (list "eval" "x" "e")))
|
||||
(knl-test
|
||||
"klisp: define lambda"
|
||||
(kernel-parse "($define! sq ($lambda (x) (* x x)))")
|
||||
(list "$define!" "sq" (list "$lambda" (list "x") (list "*" "x" "x"))))
|
||||
|
||||
;; ── round-trip identity for primitive symbols ─────────────────────
|
||||
(knl-test "identity: $vau" (kernel-parse "$vau") "$vau")
|
||||
(knl-test "identity: $lambda" (kernel-parse "$lambda") "$lambda")
|
||||
(knl-test "identity: wrap" (kernel-parse "wrap") "wrap")
|
||||
(knl-test "identity: unwrap" (kernel-parse "unwrap") "unwrap")
|
||||
|
||||
;; ── reader macros ─────────────────────────────────────────────────
|
||||
(knl-test "reader: 'foo → ($quote foo)"
|
||||
(kernel-parse "'foo") (list "$quote" "foo"))
|
||||
(knl-test "reader: '(a b c)"
|
||||
(kernel-parse "'(a b c)") (list "$quote" (list "a" "b" "c")))
|
||||
(knl-test "reader: nested quotes"
|
||||
(kernel-parse "''x")
|
||||
(list "$quote" (list "$quote" "x")))
|
||||
(knl-test "reader: ` quasiquote"
|
||||
(kernel-parse "`x") (list "$quasiquote" "x"))
|
||||
(knl-test "reader: , unquote"
|
||||
(kernel-parse ",x") (list "$unquote" "x"))
|
||||
(knl-test "reader: ,@ unquote-splicing"
|
||||
(kernel-parse ",@x") (list "$unquote-splicing" "x"))
|
||||
(knl-test "reader: quasi-mix"
|
||||
(kernel-parse "`(a ,b ,@c)")
|
||||
(list "$quasiquote"
|
||||
(list "a"
|
||||
(list "$unquote" "b")
|
||||
(list "$unquote-splicing" "c"))))
|
||||
(knl-test "reader: quote separates from neighbouring atom"
|
||||
(kernel-parse "(a 'b c)")
|
||||
(list "a" (list "$quote" "b") "c"))
|
||||
|
||||
(define knl-tests-run! (fn () {:total (+ knl-test-pass knl-test-fail) :passed knl-test-pass :failed knl-test-fail :fails knl-test-fails}))
|
||||
445
lib/kernel/tests/standard.sx
Normal file
445
lib/kernel/tests/standard.sx
Normal file
@@ -0,0 +1,445 @@
|
||||
;; lib/kernel/tests/standard.sx — exercises the Kernel standard env.
|
||||
;;
|
||||
;; Phase 4 tests verify that the standard env is rich enough to run
|
||||
;; classic Kernel programs: factorial via recursion, list operations,
|
||||
;; first-class environment manipulation. Each test starts from a fresh
|
||||
;; standard env via `(kernel-standard-env)`.
|
||||
|
||||
(define ks-test-pass 0)
|
||||
(define ks-test-fail 0)
|
||||
(define ks-test-fails (list))
|
||||
|
||||
(define
|
||||
ks-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! ks-test-pass (+ ks-test-pass 1))
|
||||
(begin
|
||||
(set! ks-test-fail (+ ks-test-fail 1))
|
||||
(append! ks-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
(define
|
||||
ks-eval
|
||||
(fn (src) (kernel-eval (kernel-parse src) (kernel-standard-env))))
|
||||
|
||||
(define ks-eval-in (fn (src env) (kernel-eval (kernel-parse src) env)))
|
||||
|
||||
(define
|
||||
ks-eval-all
|
||||
(fn (src env) (kernel-eval-program (kernel-parse-all src) env)))
|
||||
|
||||
;; ── $if ──────────────────────────────────────────────────────────
|
||||
(ks-test "if: true branch" (ks-eval "($if #t 1 2)") 1)
|
||||
(ks-test "if: false branch" (ks-eval "($if #f 1 2)") 2)
|
||||
(ks-test "if: predicate"
|
||||
(ks-eval "($if (<=? 1 2) ($quote yes) ($quote no))") "yes")
|
||||
(ks-test
|
||||
"if: untaken branch not evaluated"
|
||||
(ks-eval "($if #t 42 nope)")
|
||||
42)
|
||||
|
||||
;; ── $define! + arithmetic ───────────────────────────────────────
|
||||
(ks-test
|
||||
"define!: returns value"
|
||||
(let ((env (kernel-standard-env))) (ks-eval-in "($define! x 5)" env))
|
||||
5)
|
||||
|
||||
(ks-test
|
||||
"define!: bound in env"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! x 5)" env)
|
||||
(ks-eval-in "x" env))
|
||||
5)
|
||||
|
||||
(ks-test "arith: +" (ks-eval "(+ 2 3)") 5)
|
||||
(ks-test "arith: -" (ks-eval "(- 10 4)") 6)
|
||||
(ks-test "arith: *" (ks-eval "(* 6 7)") 42)
|
||||
(ks-test "arith: /" (ks-eval "(/ 20 5)") 4)
|
||||
(ks-test "cmp: < true" (ks-eval "(< 1 2)") true)
|
||||
(ks-test "cmp: < false" (ks-eval "(< 2 1)") false)
|
||||
(ks-test "cmp: >=" (ks-eval "(>=? 2 2)") true)
|
||||
(ks-test "cmp: <=" (ks-eval "(<=? 2 3)") true)
|
||||
(ks-test "cmp: =" (ks-eval "(=? 7 7)") true)
|
||||
|
||||
;; ── $sequence ────────────────────────────────────────────────────
|
||||
(ks-test "sequence: empty" (ks-eval "($sequence)") nil)
|
||||
(ks-test "sequence: single" (ks-eval "($sequence 99)") 99)
|
||||
(ks-test
|
||||
"sequence: multi-effect"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in "($sequence ($define! a 1) ($define! b 2) (+ a b))" env))
|
||||
3)
|
||||
|
||||
;; ── list primitives ──────────────────────────────────────────────
|
||||
(ks-test
|
||||
"list: builds"
|
||||
(ks-eval "(list 1 2 3)")
|
||||
(list 1 2 3))
|
||||
(ks-test "list: empty" (ks-eval "(list)") (list))
|
||||
(ks-test
|
||||
"cons: prepend"
|
||||
(ks-eval "(cons 0 (list 1 2 3))")
|
||||
(list 0 1 2 3))
|
||||
(ks-test "car: head" (ks-eval "(car (list 10 20 30))") 10)
|
||||
(ks-test
|
||||
"cdr: tail"
|
||||
(ks-eval "(cdr (list 10 20 30))")
|
||||
(list 20 30))
|
||||
(ks-test "length: 3" (ks-eval "(length (list 1 2 3))") 3)
|
||||
(ks-test "length: 0" (ks-eval "(length (list))") 0)
|
||||
(ks-test "null?: empty" (ks-eval "(null? (list))") true)
|
||||
(ks-test "null?: nonempty" (ks-eval "(null? (list 1))") false)
|
||||
(ks-test "pair?: empty" (ks-eval "(pair? (list))") false)
|
||||
(ks-test "pair?: nonempty" (ks-eval "(pair? (list 1))") true)
|
||||
|
||||
;; ── $quote ───────────────────────────────────────────────────────
|
||||
(ks-test "quote: symbol" (ks-eval "($quote foo)") "foo")
|
||||
(ks-test
|
||||
"quote: list"
|
||||
(ks-eval "($quote (+ 1 2))")
|
||||
(list "+" 1 2))
|
||||
|
||||
;; ── boolean / not ────────────────────────────────────────────────
|
||||
(ks-test "not: true" (ks-eval "(not #t)") false)
|
||||
(ks-test "not: false" (ks-eval "(not #f)") true)
|
||||
|
||||
;; ── factorial ────────────────────────────────────────────────────
|
||||
(ks-test
|
||||
"factorial: 5!"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! factorial ($lambda (n) ($if (<=? n 1) 1 (* n (factorial (- n 1))))))"
|
||||
env)
|
||||
(ks-eval-in "(factorial 5)" env))
|
||||
120)
|
||||
|
||||
(ks-test
|
||||
"factorial: 0! = 1"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! factorial ($lambda (n) ($if (<=? n 1) 1 (* n (factorial (- n 1))))))"
|
||||
env)
|
||||
(ks-eval-in "(factorial 0)" env))
|
||||
1)
|
||||
|
||||
(ks-test
|
||||
"factorial: 10!"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! factorial ($lambda (n) ($if (<=? n 1) 1 (* n (factorial (- n 1))))))"
|
||||
env)
|
||||
(ks-eval-in "(factorial 10)" env))
|
||||
3628800)
|
||||
|
||||
;; ── recursive list operations ────────────────────────────────────
|
||||
(ks-test
|
||||
"sum: recursive over list"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! sum ($lambda (xs) ($if (null? xs) 0 (+ (car xs) (sum (cdr xs))))))"
|
||||
env)
|
||||
(ks-eval-in "(sum (list 1 2 3 4 5))" env))
|
||||
15)
|
||||
|
||||
(ks-test
|
||||
"len: recursive count"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! mylen ($lambda (xs) ($if (null? xs) 0 (+ 1 (mylen (cdr xs))))))"
|
||||
env)
|
||||
(ks-eval-in "(mylen (list 1 2 3 4))" env))
|
||||
4)
|
||||
|
||||
(ks-test
|
||||
"map-add1: build new list"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! add1-all ($lambda (xs) ($if (null? xs) (list) (cons (+ 1 (car xs)) (add1-all (cdr xs))))))"
|
||||
env)
|
||||
(ks-eval-in "(add1-all (list 10 20 30))" env))
|
||||
(list 11 21 31))
|
||||
|
||||
;; ── eval as a first-class applicative ────────────────────────────
|
||||
(ks-test
|
||||
"eval: applies to constructed form"
|
||||
(ks-eval "(eval (list ($quote +) 2 3) (get-current-environment))")
|
||||
5)
|
||||
|
||||
(ks-test
|
||||
"eval: with a fresh make-environment"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(ks-eval "(eval ($quote (+ 1 2)) (make-environment))"))
|
||||
:raised)
|
||||
|
||||
(ks-test
|
||||
"eval: in extended env sees parent's bindings"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! shared 7)" env)
|
||||
(ks-eval-in
|
||||
"(eval ($quote shared) (make-environment (get-current-environment)))"
|
||||
env))
|
||||
7)
|
||||
|
||||
;; ── get-current-environment ──────────────────────────────────────
|
||||
(ks-test
|
||||
"get-current-environment: returns env"
|
||||
(kernel-env? (ks-eval "(get-current-environment)"))
|
||||
true)
|
||||
|
||||
(ks-test
|
||||
"get-current-environment: contains $if"
|
||||
(let
|
||||
((env (ks-eval "(get-current-environment)")))
|
||||
(kernel-env-has? env "$if"))
|
||||
true)
|
||||
|
||||
(ks-test
|
||||
"make-environment: empty"
|
||||
(let ((env (ks-eval "(make-environment)"))) (kernel-env-has? env "$if"))
|
||||
false)
|
||||
|
||||
(ks-test
|
||||
"make-environment: child sees parent"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! marker 123)" env)
|
||||
(let
|
||||
((child (ks-eval-in "(make-environment (get-current-environment))" env)))
|
||||
(kernel-env-has? child "marker")))
|
||||
true)
|
||||
|
||||
;; ── closures and lexical scope ───────────────────────────────────
|
||||
(ks-test
|
||||
"closure: captures binding"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! make-adder ($lambda (n) ($lambda (x) (+ x n))))"
|
||||
env)
|
||||
(ks-eval-in "($define! add5 (make-adder 5))" env)
|
||||
(ks-eval-in "(add5 10)" env))
|
||||
15)
|
||||
|
||||
(ks-test
|
||||
"closure: nested lookups"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! curry-add ($lambda (a) ($lambda (b) ($lambda (c) (+ a (+ b c))))))"
|
||||
env)
|
||||
(ks-eval-in "(((curry-add 1) 2) 3)" env))
|
||||
6)
|
||||
|
||||
;; ── operative defined in standard env can reach $define! ─────────
|
||||
(ks-test
|
||||
"custom: define-via-vau"
|
||||
(let
|
||||
((env (kernel-standard-env)))
|
||||
(ks-eval-in
|
||||
"($define! $let-it ($vau (name expr) e ($sequence ($define! tmp (eval expr e)) (eval (list ($quote $define!) name (list ($quote $quote) tmp)) e) tmp)))"
|
||||
env)
|
||||
(ks-eval-in "($let-it z 77)" env)
|
||||
(ks-eval-in "z" env))
|
||||
77)
|
||||
|
||||
;; ── quasiquote ──────────────────────────────────────────────────
|
||||
(ks-test "qq: plain atom" (ks-eval "`hello") "hello")
|
||||
(ks-test "qq: plain list" (ks-eval "`(a b c)") (list "a" "b" "c"))
|
||||
(ks-test "qq: unquote splices value"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! x 42)" env)
|
||||
(ks-eval-in "`(a ,x b)" env)) (list "a" 42 "b"))
|
||||
(ks-test "qq: unquote-splicing splices list"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! xs (list 1 2 3))" env)
|
||||
(ks-eval-in "`(a ,@xs b)" env)) (list "a" 1 2 3 "b"))
|
||||
(ks-test "qq: unquote-splicing at end"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! xs (list 9 8))" env)
|
||||
(ks-eval-in "`(a b ,@xs)" env)) (list "a" "b" 9 8))
|
||||
(ks-test "qq: unquote-splicing at start"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! xs (list 1 2))" env)
|
||||
(ks-eval-in "`(,@xs c)" env)) (list 1 2 "c"))
|
||||
(ks-test "qq: nested list with unquote inside"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! x 5)" env)
|
||||
(ks-eval-in "`(a (b ,x) c)" env))
|
||||
(list "a" (list "b" 5) "c"))
|
||||
(ks-test "qq: error on bare unquote-splicing into non-list"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! x 42)" env)
|
||||
(guard (e (true :raised))
|
||||
(ks-eval-in "`(a ,@x b)" env)))
|
||||
:raised)
|
||||
|
||||
;; ── $cond / $when / $unless ─────────────────────────────────────
|
||||
(ks-test "cond: first match"
|
||||
(ks-eval "($cond (#f 1) (#t 2) (#t 3))") 2)
|
||||
(ks-test "cond: else fallback"
|
||||
(ks-eval "($cond (#f 1) (else 99))") 99)
|
||||
(ks-test "cond: no match returns nil"
|
||||
(ks-eval "($cond (#f 1) (#f 2))") nil)
|
||||
(ks-test "cond: empty clauses returns nil"
|
||||
(ks-eval "($cond)") nil)
|
||||
(ks-test "cond: multi-expr body"
|
||||
(ks-eval "($cond (#t 1 2 3))") 3)
|
||||
(ks-test "cond: doesn't evaluate untaken clauses"
|
||||
;; If the second clause's test were evaluated, the unbound `nope` would error.
|
||||
(ks-eval "($cond (#t 7) (nope ignored))") 7)
|
||||
(ks-test "cond: predicate evaluation"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! n 5)" env)
|
||||
(ks-eval-in "($cond ((< n 0) ($quote negative)) ((= n 0) ($quote zero)) (else ($quote positive)))" env))
|
||||
"positive")
|
||||
|
||||
(ks-test "when: true runs body"
|
||||
(ks-eval "($when #t 1 2 3)") 3)
|
||||
(ks-test "when: false returns nil"
|
||||
(ks-eval "($when #f 1 2 3)") nil)
|
||||
(ks-test "when: skips body when false"
|
||||
(ks-eval "($when #f nope)") nil)
|
||||
|
||||
(ks-test "unless: false runs body"
|
||||
(ks-eval "($unless #f 99)") 99)
|
||||
(ks-test "unless: true returns nil"
|
||||
(ks-eval "($unless #t 99)") nil)
|
||||
(ks-test "unless: skips body when true"
|
||||
(ks-eval "($unless #t nope)") nil)
|
||||
|
||||
;; ── $and? / $or? short-circuit ──────────────────────────────────
|
||||
(ks-test "and: empty returns true" (ks-eval "($and?)") true)
|
||||
(ks-test "and: single returns value" (ks-eval "($and? 42)") 42)
|
||||
(ks-test "and: all true returns last"
|
||||
(ks-eval "($and? 1 2 3)") 3)
|
||||
(ks-test "and: first false short-circuits"
|
||||
(ks-eval "($and? #f nope)") false)
|
||||
(ks-test "and: false in middle short-circuits"
|
||||
(ks-eval "($and? 1 #f nope)") false)
|
||||
(ks-test "or: empty returns false" (ks-eval "($or?)") false)
|
||||
(ks-test "or: single returns value" (ks-eval "($or? 42)") 42)
|
||||
(ks-test "or: first truthy short-circuits"
|
||||
(ks-eval "($or? 99 nope)") 99)
|
||||
(ks-test "or: all false returns last"
|
||||
(ks-eval "($or? #f #f #f)") false)
|
||||
(ks-test "or: middle truthy"
|
||||
(ks-eval "($or? #f 42 nope)") 42)
|
||||
|
||||
;; ── variadic arithmetic ─────────────────────────────────────────
|
||||
(ks-test "+: zero args = 0" (ks-eval "(+)") 0)
|
||||
(ks-test "+: one arg = arg" (ks-eval "(+ 7)") 7)
|
||||
(ks-test "+: two args" (ks-eval "(+ 3 4)") 7)
|
||||
(ks-test "+: five args" (ks-eval "(+ 1 2 3 4 5)") 15)
|
||||
|
||||
(ks-test "*: zero args = 1" (ks-eval "(*)") 1)
|
||||
(ks-test "*: one arg" (ks-eval "(* 7)") 7)
|
||||
(ks-test "*: four args" (ks-eval "(* 1 2 3 4)") 24)
|
||||
|
||||
(ks-test "-: one arg negates" (ks-eval "(- 10)") -10)
|
||||
(ks-test "-: two args" (ks-eval "(- 10 3)") 7)
|
||||
(ks-test "-: four args fold" (ks-eval "(- 100 1 2 3)") 94)
|
||||
|
||||
(ks-test "/: two args" (ks-eval "(/ 20 5)") 4)
|
||||
(ks-test "/: three args fold" (ks-eval "(/ 100 2 5)") 10)
|
||||
|
||||
;; ── variadic chained comparison ─────────────────────────────────
|
||||
(ks-test "<: chained ascending" (ks-eval "(< 1 2 3 4 5)") true)
|
||||
(ks-test "<: not strict" (ks-eval "(< 1 2 2 3)") false)
|
||||
(ks-test "<: anti-monotonic" (ks-eval "(< 5 3)") false)
|
||||
(ks-test ">: chained descending" (ks-eval "(> 5 4 3 2 1)") true)
|
||||
(ks-test "<=? ascending equals" (ks-eval "(<=? 1 1 2 3 3)") true)
|
||||
(ks-test "<=? violation" (ks-eval "(<=? 1 2 1)") false)
|
||||
(ks-test ">=? descending equals" (ks-eval "(>=? 3 3 2 1)") true)
|
||||
|
||||
;; ── list combinators ────────────────────────────────────────────
|
||||
(ks-test "map: square"
|
||||
(ks-eval "(map ($lambda (x) (* x x)) (list 1 2 3 4))")
|
||||
(list 1 4 9 16))
|
||||
(ks-test "map: empty list"
|
||||
(ks-eval "(map ($lambda (x) x) (list))") (list))
|
||||
(ks-test "map: identity preserves"
|
||||
(ks-eval "(map ($lambda (x) x) (list 1 2 3))") (list 1 2 3))
|
||||
(ks-test "map: with closure over outer"
|
||||
(let ((env (kernel-standard-env)))
|
||||
(ks-eval-in "($define! k 10)" env)
|
||||
(ks-eval-in "(map ($lambda (x) (+ x k)) (list 1 2 3))" env))
|
||||
(list 11 12 13))
|
||||
|
||||
(ks-test "filter: positives"
|
||||
(ks-eval "(filter ($lambda (x) (< 0 x)) (list -2 -1 0 1 2))")
|
||||
(list 1 2))
|
||||
(ks-test "filter: empty result"
|
||||
(ks-eval "(filter ($lambda (x) #f) (list 1 2 3))") (list))
|
||||
(ks-test "filter: all match"
|
||||
(ks-eval "(filter ($lambda (x) #t) (list 1 2 3))") (list 1 2 3))
|
||||
|
||||
(ks-test "reduce: sum"
|
||||
(ks-eval "(reduce ($lambda (a b) (+ a b)) 0 (list 1 2 3 4 5))") 15)
|
||||
(ks-test "reduce: product"
|
||||
(ks-eval "(reduce ($lambda (a b) (* a b)) 1 (list 1 2 3 4))") 24)
|
||||
(ks-test "reduce: empty returns init"
|
||||
(ks-eval "(reduce ($lambda (a b) (+ a b)) 42 (list))") 42)
|
||||
(ks-test "reduce: build list"
|
||||
(ks-eval "(reduce ($lambda (acc x) (cons x acc)) () (list 1 2 3))")
|
||||
(list 3 2 1))
|
||||
|
||||
;; ── apply ────────────────────────────────────────────────────────
|
||||
(ks-test "apply: + over list"
|
||||
(ks-eval "(apply + (list 1 2 3 4 5))") 15)
|
||||
(ks-test "apply: lambda"
|
||||
(ks-eval "(apply ($lambda (a b c) (* a (+ b c))) (list 2 3 4))") 14)
|
||||
(ks-test "apply: list identity"
|
||||
(ks-eval "(apply list (list 1 2 3))") (list 1 2 3))
|
||||
(ks-test "apply: empty args list"
|
||||
(ks-eval "(apply + (list))") 0)
|
||||
(ks-test "apply: single arg list"
|
||||
(ks-eval "(apply ($lambda (x) (* x 10)) (list 7))") 70)
|
||||
(ks-test "apply: built via map+apply"
|
||||
;; (apply + (map ($lambda (x) (* x x)) (list 1 2 3))) → 1+4+9 = 14
|
||||
(ks-eval
|
||||
"(apply + (map ($lambda (x) (* x x)) (list 1 2 3)))") 14)
|
||||
(ks-test "apply: error on non-list args"
|
||||
(guard (e (true :raised))
|
||||
(ks-eval "(apply + 5)"))
|
||||
:raised)
|
||||
|
||||
;; ── append / reverse ────────────────────────────────────────────
|
||||
(ks-test "append: two lists"
|
||||
(ks-eval "(append (list 1 2) (list 3 4))") (list 1 2 3 4))
|
||||
(ks-test "append: three lists"
|
||||
(ks-eval "(append (list 1) (list 2) (list 3))") (list 1 2 3))
|
||||
(ks-test "append: empty list"
|
||||
(ks-eval "(append)") (list))
|
||||
(ks-test "append: one list"
|
||||
(ks-eval "(append (list 1 2 3))") (list 1 2 3))
|
||||
(ks-test "append: empty + nonempty"
|
||||
(ks-eval "(append (list) (list 1 2))") (list 1 2))
|
||||
(ks-test "append: nonempty + empty"
|
||||
(ks-eval "(append (list 1 2) (list))") (list 1 2))
|
||||
(ks-test "append: error on non-list"
|
||||
(guard (e (true :raised))
|
||||
(ks-eval "(append (list 1) 5)"))
|
||||
:raised)
|
||||
|
||||
(ks-test "reverse: four elements"
|
||||
(ks-eval "(reverse (list 1 2 3 4))") (list 4 3 2 1))
|
||||
(ks-test "reverse: empty"
|
||||
(ks-eval "(reverse (list))") (list))
|
||||
(ks-test "reverse: single"
|
||||
(ks-eval "(reverse (list 99))") (list 99))
|
||||
(ks-test "reverse: double reverse is identity"
|
||||
(ks-eval "(reverse (reverse (list 1 2 3)))") (list 1 2 3))
|
||||
|
||||
(define ks-tests-run! (fn () {:total (+ ks-test-pass ks-test-fail) :passed ks-test-pass :failed ks-test-fail :fails ks-test-fails}))
|
||||
309
lib/kernel/tests/vau.sx
Normal file
309
lib/kernel/tests/vau.sx
Normal file
@@ -0,0 +1,309 @@
|
||||
;; lib/kernel/tests/vau.sx — exercises lib/kernel/runtime.sx.
|
||||
;;
|
||||
;; Verifies the Phase 3 promise: user-defined operatives and applicatives
|
||||
;; constructible from inside the language. Tests build a Kernel
|
||||
;; base-env, bind a few helper applicatives (+, *, list, =, $if), and
|
||||
;; run programs that construct and use custom combiners.
|
||||
|
||||
(define kv-test-pass 0)
|
||||
(define kv-test-fail 0)
|
||||
(define kv-test-fails (list))
|
||||
|
||||
(define
|
||||
kv-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! kv-test-pass (+ kv-test-pass 1))
|
||||
(begin
|
||||
(set! kv-test-fail (+ kv-test-fail 1))
|
||||
(append! kv-test-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
(define kv-eval-src (fn (src env) (kernel-eval (kernel-parse src) env)))
|
||||
|
||||
(define
|
||||
kv-make-env
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((env (kernel-base-env)))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"+"
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args) (+ (first args) (nth args 1)))))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"*"
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args) (* (first args) (nth args 1)))))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"-"
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args) (- (first args) (nth args 1)))))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"="
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args) (= (first args) (nth args 1)))))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"list"
|
||||
(kernel-make-primitive-applicative (fn (args) args)))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"cons"
|
||||
(kernel-make-primitive-applicative
|
||||
(fn (args) (cons (first args) (nth args 1)))))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"$quote"
|
||||
(kernel-make-primitive-operative (fn (args dyn-env) (first args))))
|
||||
(kernel-env-bind!
|
||||
env
|
||||
"$if"
|
||||
(kernel-make-primitive-operative
|
||||
(fn
|
||||
(args dyn-env)
|
||||
(if
|
||||
(kernel-eval (first args) dyn-env)
|
||||
(kernel-eval (nth args 1) dyn-env)
|
||||
(kernel-eval (nth args 2) dyn-env)))))
|
||||
env)))
|
||||
|
||||
;; ── $vau: builds an operative ───────────────────────────────────
|
||||
(kv-test
|
||||
"vau: identity returns first arg unevaluated"
|
||||
(kv-eval-src "(($vau (a) _ a) hello)" (kv-make-env))
|
||||
"hello")
|
||||
|
||||
(kv-test
|
||||
"vau: returns args as raw expressions"
|
||||
(kv-eval-src "(($vau (a b) _ (list a b)) (+ 1 2) (+ 3 4))" (kv-make-env))
|
||||
(list (list "+" 1 2) (list "+" 3 4)))
|
||||
|
||||
(kv-test
|
||||
"vau: env-param is a kernel env"
|
||||
(kernel-env? (kv-eval-src "(($vau () e e))" (kv-make-env)))
|
||||
true)
|
||||
|
||||
(kv-test
|
||||
"vau: returns operative"
|
||||
(kernel-operative? (kv-eval-src "($vau (x) _ x)" (kv-make-env)))
|
||||
true)
|
||||
|
||||
(kv-test
|
||||
"vau: returns operative not applicative"
|
||||
(kernel-applicative? (kv-eval-src "($vau (x) _ x)" (kv-make-env)))
|
||||
false)
|
||||
|
||||
(kv-test
|
||||
"vau: zero-arg body"
|
||||
(kv-eval-src "(($vau () _ 42))" (kv-make-env))
|
||||
42)
|
||||
|
||||
(kv-test
|
||||
"vau: static-env closure captured"
|
||||
(let
|
||||
((outer (kv-make-env)))
|
||||
(kernel-env-bind! outer "captured" 17)
|
||||
(let
|
||||
((op (kv-eval-src "($vau () _ captured)" outer))
|
||||
(caller (kv-make-env)))
|
||||
(kernel-env-bind! caller "captured" 99)
|
||||
(kernel-combine op (list) caller)))
|
||||
17)
|
||||
|
||||
(kv-test
|
||||
"vau: env-param exposes caller's dynamic env"
|
||||
(let
|
||||
((outer (kv-make-env)))
|
||||
(kernel-env-bind! outer "x" 1)
|
||||
(let
|
||||
((op (kv-eval-src "($vau () e e)" outer)) (caller (kv-make-env)))
|
||||
(kernel-env-bind! caller "x" 2)
|
||||
(let
|
||||
((e-val (kernel-combine op (list) caller)))
|
||||
(kernel-env-lookup e-val "x"))))
|
||||
2)
|
||||
|
||||
;; ── $lambda: applicatives evaluate their args ───────────────────
|
||||
(kv-test
|
||||
"lambda: identity"
|
||||
(kv-eval-src "(($lambda (x) x) 42)" (kv-make-env))
|
||||
42)
|
||||
|
||||
(kv-test
|
||||
"lambda: addition"
|
||||
(kv-eval-src "(($lambda (x y) (+ x y)) 3 4)" (kv-make-env))
|
||||
7)
|
||||
|
||||
(kv-test
|
||||
"lambda: args are evaluated before bind"
|
||||
(kv-eval-src "(($lambda (x) x) (+ 2 3))" (kv-make-env))
|
||||
5)
|
||||
|
||||
(kv-test
|
||||
"lambda: zero args"
|
||||
(kv-eval-src "(($lambda () 99))" (kv-make-env))
|
||||
99)
|
||||
|
||||
(kv-test
|
||||
"lambda: returns applicative"
|
||||
(kernel-applicative? (kv-eval-src "($lambda (x) x)" (kv-make-env)))
|
||||
true)
|
||||
|
||||
(kv-test
|
||||
"lambda: returns applicative not operative"
|
||||
(kernel-operative? (kv-eval-src "($lambda (x) x)" (kv-make-env)))
|
||||
false)
|
||||
|
||||
(kv-test
|
||||
"lambda: higher-order"
|
||||
(kv-eval-src "(($lambda (f) (f 10)) ($lambda (x) (+ x 1)))" (kv-make-env))
|
||||
11)
|
||||
|
||||
;; ── wrap / unwrap as user-callable applicatives ─────────────────
|
||||
|
||||
(kv-test
|
||||
"wrap: makes applicative from operative"
|
||||
(kernel-applicative? (kv-eval-src "(wrap ($vau (x) _ x))" (kv-make-env)))
|
||||
true)
|
||||
|
||||
(kv-test
|
||||
"wrap: result evaluates its arg"
|
||||
(kv-eval-src "((wrap ($vau (x) _ x)) (+ 1 2))" (kv-make-env))
|
||||
3)
|
||||
|
||||
(kv-test
|
||||
"unwrap: extracts operative from applicative"
|
||||
(kernel-operative? (kv-eval-src "(unwrap ($lambda (x) x))" (kv-make-env)))
|
||||
true)
|
||||
|
||||
(kv-test
|
||||
"wrap/unwrap roundtrip preserves identity"
|
||||
(kv-eval-src
|
||||
"(($lambda (op) (= op (unwrap (wrap op)))) ($vau (x) _ x))"
|
||||
(kv-make-env))
|
||||
true)
|
||||
|
||||
;; ── operative? / applicative? as user-visible predicates ────────
|
||||
|
||||
(kv-test
|
||||
"operative? on vau result"
|
||||
(kv-eval-src "(operative? ($vau (x) _ x))" (kv-make-env))
|
||||
true)
|
||||
|
||||
(kv-test
|
||||
"operative? on lambda result"
|
||||
(kv-eval-src "(operative? ($lambda (x) x))" (kv-make-env))
|
||||
false)
|
||||
|
||||
(kv-test
|
||||
"applicative? on lambda result"
|
||||
(kv-eval-src "(applicative? ($lambda (x) x))" (kv-make-env))
|
||||
true)
|
||||
|
||||
(kv-test
|
||||
"applicative? on vau result"
|
||||
(kv-eval-src "(applicative? ($vau (x) _ x))" (kv-make-env))
|
||||
false)
|
||||
|
||||
(kv-test
|
||||
"operative? on number"
|
||||
(kv-eval-src "(operative? 42)" (kv-make-env))
|
||||
false)
|
||||
|
||||
;; ── Build BOTH layers from user code ────────────────────────────
|
||||
;; The headline Phase 3 test: defining an operative on top of an
|
||||
;; applicative defined on top of a vau.
|
||||
|
||||
(kv-test
|
||||
"custom: applicative + operative compose"
|
||||
(let
|
||||
((env (kv-make-env)))
|
||||
(kernel-env-bind! env "square" (kv-eval-src "($lambda (x) (* x x))" env))
|
||||
(kv-eval-src "(square 4)" env))
|
||||
16)
|
||||
|
||||
(kv-test "custom: operative captures argument syntax"
|
||||
;; ($capture x) returns the raw expression `x`, regardless of value.
|
||||
(let ((env (kv-make-env)))
|
||||
(kernel-env-bind! env "$capture"
|
||||
(kv-eval-src "($vau (form) _ form)" env))
|
||||
(kv-eval-src "($capture (+ 1 2))" env))
|
||||
(list "+" 1 2))
|
||||
|
||||
(kv-test "custom: applicative re-wraps an operative"
|
||||
;; Build a captured operative, then wrap it into an applicative that
|
||||
;; evaluates args before re-entry. This exercises wrap+$vau composed.
|
||||
(let ((env (kv-make-env)))
|
||||
(kernel-env-bind! env "id-app"
|
||||
(kv-eval-src "(wrap ($vau (x) _ x))" env))
|
||||
(kv-eval-src "(id-app (+ 10 20))" env))
|
||||
30)
|
||||
|
||||
;; ── Error cases ──────────────────────────────────────────────────
|
||||
|
||||
(kv-test
|
||||
"vau: rejects non-list formals"
|
||||
(guard (e (true :raised)) (kv-eval-src "($vau x _ x)" (kv-make-env)))
|
||||
:raised)
|
||||
|
||||
(kv-test
|
||||
"vau: rejects non-symbol formal"
|
||||
(guard (e (true :raised)) (kv-eval-src "($vau (1) _ x)" (kv-make-env)))
|
||||
:raised)
|
||||
|
||||
(kv-test
|
||||
"vau: rejects non-symbol env-param"
|
||||
(guard (e (true :raised)) (kv-eval-src "($vau (x) 7 x)" (kv-make-env)))
|
||||
:raised)
|
||||
|
||||
(kv-test
|
||||
"vau: too few args at call site"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(kv-eval-src "(($vau (x y) _ x) 1)" (kv-make-env)))
|
||||
:raised)
|
||||
|
||||
(kv-test
|
||||
"vau: too many args at call site"
|
||||
(guard
|
||||
(e (true :raised))
|
||||
(kv-eval-src "(($vau (x) _ x) 1 2)" (kv-make-env)))
|
||||
:raised)
|
||||
|
||||
(kv-test
|
||||
"wrap: rejects non-operative"
|
||||
(guard (e (true :raised)) (kv-eval-src "(wrap 42)" (kv-make-env)))
|
||||
:raised)
|
||||
|
||||
(kv-test
|
||||
"unwrap: rejects non-applicative"
|
||||
(guard (e (true :raised)) (kv-eval-src "(unwrap 42)" (kv-make-env)))
|
||||
:raised)
|
||||
|
||||
;; ── Multi-expression body (implicit $sequence) ──────────────────
|
||||
|
||||
(kv-test "lambda: two body forms — value of last"
|
||||
(kv-eval-src "(($lambda (n) (+ n 1) (+ n 10)) 5)" (kv-make-env)) 15)
|
||||
|
||||
(kv-test "lambda: three body forms"
|
||||
(kv-eval-src "(($lambda (n) n (+ n 1) (+ n 2)) 10)" (kv-make-env)) 12)
|
||||
|
||||
(kv-test "vau: two body forms"
|
||||
(kv-eval-src "(($vau (a b) _ a (list a b)) 7 8)" (kv-make-env))
|
||||
(list 7 8))
|
||||
|
||||
(kv-test "lambda: $define! in early body visible in later body"
|
||||
(kv-eval-src
|
||||
"(($lambda (n) ($define! double (+ n n)) double) 6)"
|
||||
(kv-make-env)) 12)
|
||||
|
||||
(kv-test "lambda: zero-arg multi-body"
|
||||
(kv-eval-src "(($lambda () 1 2 3))" (kv-make-env)) 3)
|
||||
|
||||
(define kv-tests-run! (fn () {:total (+ kv-test-pass kv-test-fail) :passed kv-test-pass :failed kv-test-fail :fails kv-test-fails}))
|
||||
@@ -230,10 +230,8 @@
|
||||
(let
|
||||
((s2 (fd-add-constraint s c)))
|
||||
(let
|
||||
((s2-or-nil (c s2)))
|
||||
(let
|
||||
((s3 (cond ((= s2-or-nil nil) nil) (:else (fd-fire-store s2-or-nil)))))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3))))))))))
|
||||
((s3 (c s2)))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3)))))))))
|
||||
|
||||
;; --- fd-lt ---
|
||||
|
||||
@@ -296,10 +294,8 @@
|
||||
(let
|
||||
((s2 (fd-add-constraint s c)))
|
||||
(let
|
||||
((s2-or-nil (c s2)))
|
||||
(let
|
||||
((s3 (cond ((= s2-or-nil nil) nil) (:else (fd-fire-store s2-or-nil)))))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3))))))))))
|
||||
((s3 (c s2)))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3)))))))))
|
||||
|
||||
;; --- fd-lte ---
|
||||
|
||||
@@ -362,10 +358,8 @@
|
||||
(let
|
||||
((s2 (fd-add-constraint s c)))
|
||||
(let
|
||||
((s2-or-nil (c s2)))
|
||||
(let
|
||||
((s3 (cond ((= s2-or-nil nil) nil) (:else (fd-fire-store s2-or-nil)))))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3))))))))))
|
||||
((s3 (c s2)))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3)))))))))
|
||||
|
||||
;; --- fd-eq ---
|
||||
|
||||
@@ -434,10 +428,8 @@
|
||||
(let
|
||||
((s2 (fd-add-constraint s c)))
|
||||
(let
|
||||
((s2-or-nil (c s2)))
|
||||
(let
|
||||
((s3 (cond ((= s2-or-nil nil) nil) (:else (fd-fire-store s2-or-nil)))))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3))))))))))
|
||||
((s3 (c s2)))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3)))))))))
|
||||
|
||||
;; --- labelling ---
|
||||
|
||||
@@ -527,118 +519,6 @@
|
||||
(cond ((= s2 nil) nil) (:else s2)))))))
|
||||
(:else nil))))
|
||||
|
||||
(define
|
||||
fd-narrow-or-skip
|
||||
(fn
|
||||
(s var-key d lo hi)
|
||||
(cond
|
||||
((= d nil) s)
|
||||
(:else
|
||||
(fd-set-domain
|
||||
s
|
||||
var-key
|
||||
(filter (fn (v) (and (>= v lo) (<= v hi))) d))))))
|
||||
|
||||
(define
|
||||
fd-plus-prop-vvn
|
||||
(fn
|
||||
(wx wy wz s)
|
||||
(let
|
||||
((xd (fd-domain-of s (var-name wx)))
|
||||
(yd (fd-domain-of s (var-name wy))))
|
||||
(cond
|
||||
((or (= xd nil) (= yd nil)) s)
|
||||
(:else
|
||||
(let
|
||||
((s1 (fd-narrow-or-skip s (var-name wx) xd (- wz (fd-dom-max yd)) (- wz (fd-dom-min yd)))))
|
||||
(cond
|
||||
((= s1 nil) nil)
|
||||
(:else
|
||||
(let
|
||||
((xd2 (fd-domain-of s1 (var-name wx))))
|
||||
(fd-narrow-or-skip
|
||||
s1
|
||||
(var-name wy)
|
||||
yd
|
||||
(- wz (fd-dom-max xd2))
|
||||
(- wz (fd-dom-min xd2))))))))))))
|
||||
|
||||
(define
|
||||
fd-plus-prop-nvv
|
||||
(fn
|
||||
(wx wy wz s)
|
||||
(let
|
||||
((yd (fd-domain-of s (var-name wy)))
|
||||
(zd (fd-domain-of s (var-name wz))))
|
||||
(cond
|
||||
((or (= yd nil) (= zd nil)) s)
|
||||
(:else
|
||||
(let
|
||||
((s1 (fd-narrow-or-skip s (var-name wy) yd (- (fd-dom-min zd) wx) (- (fd-dom-max zd) wx))))
|
||||
(cond
|
||||
((= s1 nil) nil)
|
||||
(:else
|
||||
(let
|
||||
((yd2 (fd-domain-of s1 (var-name wy))))
|
||||
(fd-narrow-or-skip
|
||||
s1
|
||||
(var-name wz)
|
||||
zd
|
||||
(+ wx (fd-dom-min yd2))
|
||||
(+ wx (fd-dom-max yd2))))))))))))
|
||||
|
||||
(define
|
||||
fd-plus-prop-vnv
|
||||
(fn
|
||||
(wx wy wz s)
|
||||
(let
|
||||
((xd (fd-domain-of s (var-name wx)))
|
||||
(zd (fd-domain-of s (var-name wz))))
|
||||
(cond
|
||||
((or (= xd nil) (= zd nil)) s)
|
||||
(:else
|
||||
(let
|
||||
((s1 (fd-narrow-or-skip s (var-name wx) xd (- (fd-dom-min zd) wy) (- (fd-dom-max zd) wy))))
|
||||
(cond
|
||||
((= s1 nil) nil)
|
||||
(:else
|
||||
(let
|
||||
((xd2 (fd-domain-of s1 (var-name wx))))
|
||||
(fd-narrow-or-skip
|
||||
s1
|
||||
(var-name wz)
|
||||
zd
|
||||
(+ (fd-dom-min xd2) wy)
|
||||
(+ (fd-dom-max xd2) wy)))))))))))
|
||||
|
||||
(define
|
||||
fd-plus-prop-vvv
|
||||
(fn
|
||||
(wx wy wz s)
|
||||
(let
|
||||
((xd (fd-domain-of s (var-name wx)))
|
||||
(yd (fd-domain-of s (var-name wy)))
|
||||
(zd (fd-domain-of s (var-name wz))))
|
||||
(cond
|
||||
((or (= xd nil) (or (= yd nil) (= zd nil))) s)
|
||||
(:else
|
||||
(let
|
||||
((s1 (fd-narrow-or-skip s (var-name wx) xd (- (fd-dom-min zd) (fd-dom-max yd)) (- (fd-dom-max zd) (fd-dom-min yd)))))
|
||||
(cond
|
||||
((= s1 nil) nil)
|
||||
(:else
|
||||
(let
|
||||
((s2 (fd-narrow-or-skip s1 (var-name wy) yd (- (fd-dom-min zd) (fd-dom-max xd)) (- (fd-dom-max zd) (fd-dom-min xd)))))
|
||||
(cond
|
||||
((= s2 nil) nil)
|
||||
(:else
|
||||
(fd-narrow-or-skip
|
||||
s2
|
||||
(var-name wz)
|
||||
zd
|
||||
(+ (fd-dom-min xd) (fd-dom-min yd))
|
||||
(+ (fd-dom-max xd) (fd-dom-max yd))))))))))))))
|
||||
|
||||
(define
|
||||
fd-plus-prop
|
||||
(fn
|
||||
@@ -654,14 +534,6 @@
|
||||
(fd-bind-or-narrow wy (- wz wx) s))
|
||||
((and (number? wy) (number? wz))
|
||||
(fd-bind-or-narrow wx (- wz wy) s))
|
||||
((and (is-var? wx) (is-var? wy) (number? wz))
|
||||
(fd-plus-prop-vvn wx wy wz s))
|
||||
((and (number? wx) (is-var? wy) (is-var? wz))
|
||||
(fd-plus-prop-nvv wx wy wz s))
|
||||
((and (is-var? wx) (number? wy) (is-var? wz))
|
||||
(fd-plus-prop-vnv wx wy wz s))
|
||||
((and (is-var? wx) (is-var? wy) (is-var? wz))
|
||||
(fd-plus-prop-vvv wx wy wz s))
|
||||
(:else s)))))
|
||||
|
||||
(define
|
||||
@@ -675,141 +547,11 @@
|
||||
(let
|
||||
((s2 (fd-add-constraint s c)))
|
||||
(let
|
||||
((s2-or-nil (c s2)))
|
||||
(let
|
||||
((s3 (cond ((= s2-or-nil nil) nil) (:else (fd-fire-store s2-or-nil)))))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3))))))))))
|
||||
((s3 (c s2)))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3)))))))))
|
||||
|
||||
;; --- fd-times (x * y = z, ground-cases propagator) ---
|
||||
|
||||
(define
|
||||
fd-int-ceil-div
|
||||
(fn
|
||||
(a b)
|
||||
(cond
|
||||
((= (mod a b) 0) (/ a b))
|
||||
(:else (+ (fd-int-floor-div a b) 1)))))
|
||||
|
||||
(define fd-int-floor-div (fn (a b) (/ (- a (mod a b)) b)))
|
||||
|
||||
(define
|
||||
fd-dom-positive?
|
||||
(fn
|
||||
(d)
|
||||
(cond ((empty? d) false) (:else (>= (fd-dom-min d) 1)))))
|
||||
|
||||
(define
|
||||
fd-times-prop-vvv
|
||||
(fn
|
||||
(wx wy wz s)
|
||||
(let
|
||||
((xd (fd-domain-of s (var-name wx)))
|
||||
(yd (fd-domain-of s (var-name wy)))
|
||||
(zd (fd-domain-of s (var-name wz))))
|
||||
(cond
|
||||
((or (= xd nil) (or (= yd nil) (= zd nil))) s)
|
||||
((not (and (fd-dom-positive? xd) (and (fd-dom-positive? yd) (fd-dom-positive? zd))))
|
||||
s)
|
||||
(:else
|
||||
(let
|
||||
((s1 (fd-narrow-or-skip s (var-name wx) xd (fd-int-ceil-div (fd-dom-min zd) (fd-dom-max yd)) (fd-int-floor-div (fd-dom-max zd) (fd-dom-min yd)))))
|
||||
(cond
|
||||
((= s1 nil) nil)
|
||||
(:else
|
||||
(let
|
||||
((s2 (fd-narrow-or-skip s1 (var-name wy) yd (fd-int-ceil-div (fd-dom-min zd) (fd-dom-max xd)) (fd-int-floor-div (fd-dom-max zd) (fd-dom-min xd)))))
|
||||
(cond
|
||||
((= s2 nil) nil)
|
||||
(:else
|
||||
(fd-narrow-or-skip
|
||||
s2
|
||||
(var-name wz)
|
||||
zd
|
||||
(* (fd-dom-min xd) (fd-dom-min yd))
|
||||
(* (fd-dom-max xd) (fd-dom-max yd))))))))))))))
|
||||
|
||||
(define
|
||||
fd-times-prop-vvn
|
||||
(fn
|
||||
(wx wy wz s)
|
||||
(let
|
||||
((xd (fd-domain-of s (var-name wx)))
|
||||
(yd (fd-domain-of s (var-name wy))))
|
||||
(cond
|
||||
((or (= xd nil) (= yd nil)) s)
|
||||
((not (and (fd-dom-positive? xd) (fd-dom-positive? yd))) s)
|
||||
((<= wz 0) s)
|
||||
(:else
|
||||
(let
|
||||
((s1 (fd-narrow-or-skip s (var-name wx) xd (fd-int-ceil-div wz (fd-dom-max yd)) (fd-int-floor-div wz (fd-dom-min yd)))))
|
||||
(cond
|
||||
((= s1 nil) nil)
|
||||
(:else
|
||||
(let
|
||||
((xd2 (fd-domain-of s1 (var-name wx))))
|
||||
(fd-narrow-or-skip
|
||||
s1
|
||||
(var-name wy)
|
||||
yd
|
||||
(fd-int-ceil-div wz (fd-dom-max xd2))
|
||||
(fd-int-floor-div wz (fd-dom-min xd2))))))))))))
|
||||
|
||||
(define
|
||||
fd-times-prop-nvv
|
||||
(fn
|
||||
(wx wy wz s)
|
||||
(cond
|
||||
((<= wx 0) s)
|
||||
(:else
|
||||
(let
|
||||
((yd (fd-domain-of s (var-name wy)))
|
||||
(zd (fd-domain-of s (var-name wz))))
|
||||
(cond
|
||||
((or (= yd nil) (= zd nil)) s)
|
||||
((not (and (fd-dom-positive? yd) (fd-dom-positive? zd))) s)
|
||||
(:else
|
||||
(let
|
||||
((s1 (fd-narrow-or-skip s (var-name wy) yd (fd-int-ceil-div (fd-dom-min zd) wx) (fd-int-floor-div (fd-dom-max zd) wx))))
|
||||
(cond
|
||||
((= s1 nil) nil)
|
||||
(:else
|
||||
(let
|
||||
((yd2 (fd-domain-of s1 (var-name wy))))
|
||||
(fd-narrow-or-skip
|
||||
s1
|
||||
(var-name wz)
|
||||
zd
|
||||
(* wx (fd-dom-min yd2))
|
||||
(* wx (fd-dom-max yd2))))))))))))))
|
||||
|
||||
(define
|
||||
fd-times-prop-vnv
|
||||
(fn
|
||||
(wx wy wz s)
|
||||
(cond
|
||||
((<= wy 0) s)
|
||||
(:else
|
||||
(let
|
||||
((xd (fd-domain-of s (var-name wx)))
|
||||
(zd (fd-domain-of s (var-name wz))))
|
||||
(cond
|
||||
((or (= xd nil) (= zd nil)) s)
|
||||
((not (and (fd-dom-positive? xd) (fd-dom-positive? zd))) s)
|
||||
(:else
|
||||
(let
|
||||
((s1 (fd-narrow-or-skip s (var-name wx) xd (fd-int-ceil-div (fd-dom-min zd) wy) (fd-int-floor-div (fd-dom-max zd) wy))))
|
||||
(cond
|
||||
((= s1 nil) nil)
|
||||
(:else
|
||||
(let
|
||||
((xd2 (fd-domain-of s1 (var-name wx))))
|
||||
(fd-narrow-or-skip
|
||||
s1
|
||||
(var-name wz)
|
||||
zd
|
||||
(* (fd-dom-min xd2) wy)
|
||||
(* (fd-dom-max xd2) wy)))))))))))))
|
||||
|
||||
(define
|
||||
fd-times-prop
|
||||
(fn
|
||||
@@ -831,14 +573,6 @@
|
||||
((= wy 0) (cond ((= wz 0) s) (:else nil)))
|
||||
((not (= (mod wz wy) 0)) nil)
|
||||
(:else (fd-bind-or-narrow wx (/ wz wy) s))))
|
||||
((and (is-var? wx) (is-var? wy) (number? wz))
|
||||
(fd-times-prop-vvn wx wy wz s))
|
||||
((and (number? wx) (is-var? wy) (is-var? wz))
|
||||
(fd-times-prop-nvv wx wy wz s))
|
||||
((and (is-var? wx) (number? wy) (is-var? wz))
|
||||
(fd-times-prop-vnv wx wy wz s))
|
||||
((and (is-var? wx) (is-var? wy) (is-var? wz))
|
||||
(fd-times-prop-vvv wx wy wz s))
|
||||
(:else s)))))
|
||||
|
||||
(define
|
||||
@@ -852,7 +586,5 @@
|
||||
(let
|
||||
((s2 (fd-add-constraint s c)))
|
||||
(let
|
||||
((s2-or-nil (c s2)))
|
||||
(let
|
||||
((s3 (cond ((= s2-or-nil nil) nil) (:else (fd-fire-store s2-or-nil)))))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3))))))))))
|
||||
((s3 (c s2)))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3)))))))))
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
;; lib/minikanren/diseq.sx — Phase 5 polish: =/= disequality with a
|
||||
;; constraint store, generalising nafc / fd-neq to logic terms.
|
||||
;;
|
||||
;; The constraint store lives under the same `_fd` reserved key as the
|
||||
;; CLP(FD) propagators (a disequality is just another constraint
|
||||
;; closure that the existing fd-fire-store machinery re-runs).
|
||||
;;
|
||||
;; =/= semantics:
|
||||
;; - If u and v walk to ground non-unifiable terms, succeed (drop).
|
||||
;; - If they walk to terms that COULD become equal under a future
|
||||
;; binding, store the constraint; re-check after each binding.
|
||||
;; - If they're already equal (unify with no new bindings), fail.
|
||||
;;
|
||||
;; Implementation: each =/= test attempts (mk-unify wu wv s).
|
||||
;; nil — distinct, keep s, drop the constraint (return s).
|
||||
;; subst eq — equal, fail (return nil).
|
||||
;; subst > — partially unifiable; keep the constraint, return s.
|
||||
;;
|
||||
;; "Substitution equal to s" is detected via key-count: mk-unify only
|
||||
;; ever extends a substitution, never removes from it, so equal
|
||||
;; key-count means no new bindings were needed.
|
||||
|
||||
(define
|
||||
=/=-prop
|
||||
(fn
|
||||
(u v s)
|
||||
(let
|
||||
((s-after (mk-unify u v s)))
|
||||
(cond
|
||||
((= s-after nil) s)
|
||||
((= (len (keys s)) (len (keys s-after))) nil)
|
||||
(:else s)))))
|
||||
|
||||
(define
|
||||
=/=
|
||||
(fn
|
||||
(u v)
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((c (fn (sp) (=/=-prop u v sp))))
|
||||
(let
|
||||
((s2 (fd-add-constraint s c)))
|
||||
(let
|
||||
((s2-or-nil (c s2)))
|
||||
(let
|
||||
((s3 (cond ((= s2-or-nil nil) nil) (:else (fd-fire-store s2-or-nil)))))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3))))))))))
|
||||
|
||||
;; --- constraint-aware == ---
|
||||
;;
|
||||
;; Plain `==` doesn't fire the constraint store, so a binding that
|
||||
;; should violate a pending =/= goes undetected. `==-cs` is the
|
||||
;; drop-in replacement that fires fd-fire-store after each binding.
|
||||
;; Use ==-cs in any program that mixes =/= (or fd-* goals that should
|
||||
;; re-check after non-FD bindings) with regular unification.
|
||||
|
||||
(define
|
||||
==-cs
|
||||
(fn
|
||||
(u v)
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((s2 (mk-unify u v s)))
|
||||
(cond
|
||||
((= s2 nil) mzero)
|
||||
(:else
|
||||
(let
|
||||
((s3 (fd-fire-store s2)))
|
||||
(cond ((= s3 nil) mzero) (:else (unit s3))))))))))
|
||||
@@ -1,94 +0,0 @@
|
||||
;; lib/minikanren/tabling-slg.sx — Phase 7 piece A: SLG-style tabling.
|
||||
;;
|
||||
;; Naive memoization (table-1/2/3 in tabling.sx) drains the body's
|
||||
;; answer stream eagerly, then caches. Recursive tabled calls with the
|
||||
;; SAME ground key see an empty cache (the in-progress entry doesn't
|
||||
;; exist), so they recurse and the host overflows on cyclic relations.
|
||||
;;
|
||||
;; This module ships the in-progress-sentinel piece of SLG resolution:
|
||||
;; before evaluating the body, mark the cache entry as :in-progress;
|
||||
;; any recursive call to the same key sees the sentinel and returns
|
||||
;; mzero (no answers yet). Outer recursion thus terminates on cycles.
|
||||
;; Limitation: a single pass — answers found by cycle-dependent
|
||||
;; recursive calls are NOT discovered. Full SLG with fixed-point
|
||||
;; iteration (re-running until no new answers) is left for follow-up.
|
||||
|
||||
(define
|
||||
table-2-slg-iter
|
||||
(fn
|
||||
(rel-fn input output s key prev-vals)
|
||||
(begin
|
||||
(mk-tab-store! key prev-vals)
|
||||
(let
|
||||
((all-substs (stream-take -1 ((rel-fn input output) s))))
|
||||
(let
|
||||
((vals (map (fn (s2) (mk-walk* output s2)) all-substs)))
|
||||
(cond
|
||||
((= (len vals) (len prev-vals))
|
||||
(begin
|
||||
(mk-tab-store! key vals)
|
||||
(mk-tab-replay-vals vals output s)))
|
||||
(:else (table-2-slg-iter rel-fn input output s key vals))))))))
|
||||
|
||||
(define
|
||||
table-2-slg
|
||||
(fn
|
||||
(name rel-fn)
|
||||
(fn
|
||||
(input output)
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((winput (mk-walk* input s)))
|
||||
(cond
|
||||
((mk-tab-ground-term? winput)
|
||||
(let
|
||||
((key (str name "/slg/" winput)))
|
||||
(let
|
||||
((cached (mk-tab-lookup key)))
|
||||
(cond
|
||||
((not (= cached :miss))
|
||||
(mk-tab-replay-vals cached output s))
|
||||
(:else
|
||||
(table-2-slg-iter rel-fn input output s key (list)))))))
|
||||
(:else ((rel-fn input output) s))))))))
|
||||
|
||||
(define
|
||||
table-3-slg-iter
|
||||
(fn
|
||||
(rel-fn i1 i2 output s key prev-vals)
|
||||
(begin
|
||||
(mk-tab-store! key prev-vals)
|
||||
(let
|
||||
((all-substs (stream-take -1 ((rel-fn i1 i2 output) s))))
|
||||
(let
|
||||
((vals (map (fn (s2) (mk-walk* output s2)) all-substs)))
|
||||
(cond
|
||||
((= (len vals) (len prev-vals))
|
||||
(begin
|
||||
(mk-tab-store! key vals)
|
||||
(mk-tab-replay-vals vals output s)))
|
||||
(:else (table-3-slg-iter rel-fn i1 i2 output s key vals))))))))
|
||||
|
||||
(define
|
||||
table-3-slg
|
||||
(fn
|
||||
(name rel-fn)
|
||||
(fn
|
||||
(i1 i2 output)
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((wi1 (mk-walk* i1 s)) (wi2 (mk-walk* i2 s)))
|
||||
(cond
|
||||
((and (mk-tab-ground-term? wi1) (mk-tab-ground-term? wi2))
|
||||
(let
|
||||
((key (str name "/slg3/" wi1 "/" wi2)))
|
||||
(let
|
||||
((cached (mk-tab-lookup key)))
|
||||
(cond
|
||||
((not (= cached :miss))
|
||||
(mk-tab-replay-vals cached output s))
|
||||
(:else
|
||||
(table-3-slg-iter rel-fn i1 i2 output s key (list)))))))
|
||||
(:else ((rel-fn i1 i2 output) s))))))))
|
||||
@@ -1,316 +0,0 @@
|
||||
;; lib/minikanren/tests/clpfd-bounds.sx — Phase 6 piece B: bounds-consistency
|
||||
;; for fd-plus and fd-times in the partial- and all-domain cases.
|
||||
;;
|
||||
;; We probe domains directly (peek at the FD store) before any labelling
|
||||
;; happens. This isolates the propagator's narrowing behaviour from the
|
||||
;; search engine.
|
||||
|
||||
(define
|
||||
probe-dom
|
||||
(fn
|
||||
(goal var-key)
|
||||
(let
|
||||
((s (first (stream-take 1 (goal empty-s)))))
|
||||
(cond ((= s nil) :no-subst) (:else (fd-domain-of s var-key))))))
|
||||
|
||||
;; --- fd-plus partial-domain narrowing ---
|
||||
|
||||
(mk-test
|
||||
"fd-plus-vvn-narrows-x"
|
||||
(let
|
||||
((x (mk-var "x")) (y (mk-var "y")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in
|
||||
x
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10))
|
||||
(fd-in y (list 1 2 3))
|
||||
(fd-plus x y 10))
|
||||
"x"))
|
||||
(list 7 8 9))
|
||||
|
||||
(mk-test
|
||||
"fd-plus-vvn-narrows-y"
|
||||
(let
|
||||
((x (mk-var "x")) (y (mk-var "y")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in
|
||||
x
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10))
|
||||
(fd-in y (list 1 2 3))
|
||||
(fd-plus x y 10))
|
||||
"y"))
|
||||
(list 1 2 3))
|
||||
|
||||
(mk-test
|
||||
"fd-plus-nvv-narrows"
|
||||
(let
|
||||
((y (mk-var "y")) (z (mk-var "z")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in y (list 1 2 3))
|
||||
(fd-in
|
||||
z
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20))
|
||||
(fd-plus 5 y z))
|
||||
"z"))
|
||||
(list 6 7 8))
|
||||
|
||||
(mk-test
|
||||
"fd-plus-vvv-narrows-z"
|
||||
(let
|
||||
((x (mk-var "x")) (y (mk-var "y")) (z (mk-var "z")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in x (list 1 2 3))
|
||||
(fd-in y (list 1 2 3))
|
||||
(fd-in
|
||||
z
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20))
|
||||
(fd-plus x y z))
|
||||
"z"))
|
||||
(list 2 3 4 5 6))
|
||||
|
||||
(mk-test
|
||||
"fd-plus-vvv-narrows-x"
|
||||
(let
|
||||
((x (mk-var "x")) (y (mk-var "y")) (z (mk-var "z")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in
|
||||
x
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10))
|
||||
(fd-in y (list 1 2 3))
|
||||
(fd-in z (list 5 6 7))
|
||||
(fd-plus x y z))
|
||||
"x"))
|
||||
(list 2 3 4 5 6))
|
||||
|
||||
;; --- fd-times partial-domain narrowing (positive domains) ---
|
||||
|
||||
(mk-test
|
||||
"fd-times-vvn-narrows"
|
||||
(let
|
||||
((x (mk-var "x")) (y (mk-var "y")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in
|
||||
x
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6))
|
||||
(fd-in
|
||||
y
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6))
|
||||
(fd-times x y 12))
|
||||
"x"))
|
||||
(list 2 3 4 5 6))
|
||||
|
||||
(mk-test
|
||||
"fd-times-nvv-narrows"
|
||||
(let
|
||||
((y (mk-var "y")) (z (mk-var "z")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in y (list 1 2 3 4))
|
||||
(fd-in
|
||||
z
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20))
|
||||
(fd-times 3 y z))
|
||||
"z"))
|
||||
(list
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12))
|
||||
|
||||
(mk-test
|
||||
"fd-times-vvv-narrows"
|
||||
(let
|
||||
((x (mk-var "x")) (y (mk-var "y")) (z (mk-var "z")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in x (list 1 2 3))
|
||||
(fd-in y (list 1 2 3))
|
||||
(fd-in
|
||||
z
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20))
|
||||
(fd-times x y z))
|
||||
"z"))
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9))
|
||||
|
||||
;; --- bounds force impossible branches to fail early ---
|
||||
|
||||
(mk-test
|
||||
"fd-plus-impossible-via-bounds"
|
||||
(let
|
||||
((x (mk-var "x")) (y (mk-var "y")))
|
||||
(probe-dom
|
||||
(mk-conj
|
||||
(fd-in
|
||||
x
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10))
|
||||
(fd-in
|
||||
y
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10))
|
||||
(fd-plus x y 100))
|
||||
"x"))
|
||||
:no-subst)
|
||||
|
||||
(mk-tests-run!)
|
||||
@@ -1,83 +0,0 @@
|
||||
;; lib/minikanren/tests/diseq.sx — Phase 5 polish: =/= disequality.
|
||||
|
||||
;; --- ground cases ---
|
||||
|
||||
(mk-test
|
||||
"=/=-ground-distinct"
|
||||
(run* q (=/= 1 2))
|
||||
(list (make-symbol "_.0")))
|
||||
(mk-test "=/=-ground-equal" (run* q (=/= 1 1)) (list))
|
||||
(mk-test
|
||||
"=/=-ground-strings"
|
||||
(run* q (=/= "a" "b"))
|
||||
(list (make-symbol "_.0")))
|
||||
(mk-test "=/=-ground-strings-eq" (run* q (=/= "a" "a")) (list))
|
||||
|
||||
;; --- structural ---
|
||||
|
||||
(mk-test
|
||||
"=/=-pair-distinct"
|
||||
(run* q (=/= (list 1 2) (list 1 3)))
|
||||
(list (make-symbol "_.0")))
|
||||
(mk-test
|
||||
"=/=-pair-equal"
|
||||
(run* q (=/= (list 1 2) (list 1 2)))
|
||||
(list))
|
||||
(mk-test
|
||||
"=/=-pair-vs-atom"
|
||||
(run* q (=/= (list 1) 1))
|
||||
(list (make-symbol "_.0")))
|
||||
|
||||
;; --- partial / late binding ---
|
||||
;;
|
||||
;; ==-cs is required to wake up the constraint store after a binding;
|
||||
;; plain == doesn't fire constraints.
|
||||
|
||||
(mk-test
|
||||
"=/=-late-bind-violates"
|
||||
(run* q (fresh (x) (=/= x 5) (==-cs x 5) (== q x)))
|
||||
(list))
|
||||
|
||||
(mk-test
|
||||
"=/=-late-bind-ok"
|
||||
(run* q (fresh (x) (=/= x 5) (==-cs x 7) (== q x)))
|
||||
(list 7))
|
||||
|
||||
(mk-test
|
||||
"=/=-two-vars-equal-late-fails"
|
||||
(run*
|
||||
q
|
||||
(fresh
|
||||
(x y)
|
||||
(=/= x y)
|
||||
(==-cs x 1)
|
||||
(==-cs y 1)
|
||||
(== q (list x y))))
|
||||
(list))
|
||||
|
||||
(mk-test
|
||||
"=/=-two-vars-distinct-late"
|
||||
(run*
|
||||
q
|
||||
(fresh
|
||||
(x y)
|
||||
(=/= x y)
|
||||
(==-cs x 1)
|
||||
(==-cs y 2)
|
||||
(== q (list x y))))
|
||||
(list (list 1 2)))
|
||||
|
||||
;; --- compose with conde / fresh ---
|
||||
|
||||
(mk-test
|
||||
"=/=-with-membero-filter"
|
||||
(run*
|
||||
q
|
||||
(fresh
|
||||
(x)
|
||||
(membero x (list 1 2 3))
|
||||
(=/= x 2)
|
||||
(== q x)))
|
||||
(list 1 3))
|
||||
|
||||
(mk-tests-run!)
|
||||
@@ -1,97 +0,0 @@
|
||||
;; lib/minikanren/tests/send-more-money.sx — classic cryptarithmetic
|
||||
;;
|
||||
;; S E N D
|
||||
;; + M O R E
|
||||
;; ---------
|
||||
;; M O N E Y
|
||||
;;
|
||||
;; Column-by-column encoding with carries c1, c2, c3, and the
|
||||
;; leftmost column produces a carry which equals M (the result is 5 digits).
|
||||
;; All 8 letters distinct; S ≠ 0, M ≠ 0.
|
||||
;; Unique solution: S=9, E=5, N=6, D=7, M=1, O=0, R=8, Y=2.
|
||||
;;
|
||||
;; Note: the full search labelling 11 variables from {0..9} is too slow
|
||||
;; for naive labelling order (10^11 combinations naively, even with
|
||||
;; bounds-consistency the branching factor dominates). Real CLP(FD)
|
||||
;; systems use first-fail heuristics. Here we only verify the encoding
|
||||
;; against the known answer.
|
||||
|
||||
(define
|
||||
digits-0-9
|
||||
(list
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9))
|
||||
(define
|
||||
digits-1-9
|
||||
(list
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9))
|
||||
|
||||
(define
|
||||
smm-col-with-carry
|
||||
(fn
|
||||
(x y carry-in z carry-out)
|
||||
(fresh
|
||||
(xy xyc ten-cout z-plus-ten-cout)
|
||||
(fd-plus x y xy)
|
||||
(fd-plus xy carry-in xyc)
|
||||
(fd-times 10 carry-out ten-cout)
|
||||
(fd-plus z ten-cout z-plus-ten-cout)
|
||||
(fd-eq xyc z-plus-ten-cout))))
|
||||
|
||||
(define
|
||||
send-more-money
|
||||
(fn
|
||||
(S E N D M O R Y)
|
||||
(fresh
|
||||
(c1 c2 c3)
|
||||
(mk-conj
|
||||
(fd-in S digits-1-9)
|
||||
(fd-in M digits-1-9)
|
||||
(fd-in E digits-0-9)
|
||||
(fd-in N digits-0-9)
|
||||
(fd-in D digits-0-9)
|
||||
(fd-in O digits-0-9)
|
||||
(fd-in R digits-0-9)
|
||||
(fd-in Y digits-0-9)
|
||||
(fd-in c1 (list 0 1))
|
||||
(fd-in c2 (list 0 1))
|
||||
(fd-in c3 (list 0 1))
|
||||
(fd-distinct (list S E N D M O R Y))
|
||||
(smm-col-with-carry D E 0 Y c1)
|
||||
(smm-col-with-carry N R c1 E c2)
|
||||
(smm-col-with-carry E O c2 N c3)
|
||||
(smm-col-with-carry S M c3 O M)
|
||||
(fd-label (list S E N D M O R Y c1 c2 c3))))))
|
||||
|
||||
(mk-test
|
||||
"send-more-money-verify-known-solution"
|
||||
(run*
|
||||
q
|
||||
(send-more-money
|
||||
9
|
||||
5
|
||||
6
|
||||
7
|
||||
1
|
||||
0
|
||||
8
|
||||
2))
|
||||
(list (make-symbol "_.0")))
|
||||
|
||||
(mk-tests-run!)
|
||||
@@ -1,89 +0,0 @@
|
||||
;; lib/minikanren/tests/sudoku-4x4.sx — Sudoku 4×4 via CLP(FD).
|
||||
;;
|
||||
;; Grid in row-major order:
|
||||
;;
|
||||
;; c0 c1 | c2 c3
|
||||
;; c4 c5 | c6 c7
|
||||
;; ------+------
|
||||
;; c8 c9 | cA cB
|
||||
;; cC cD | cE cF
|
||||
;;
|
||||
;; Each cell ∈ {1, 2, 3, 4}. 4 rows + 4 cols + 4 2x2 boxes are each a
|
||||
;; distinct permutation.
|
||||
|
||||
(define digits-1-4 (list 1 2 3 4))
|
||||
|
||||
(define
|
||||
sudoku-4x4
|
||||
(fn
|
||||
(cells)
|
||||
(let
|
||||
((c0 (nth cells 0))
|
||||
(c1 (nth cells 1))
|
||||
(c2 (nth cells 2))
|
||||
(c3 (nth cells 3))
|
||||
(c4 (nth cells 4))
|
||||
(c5 (nth cells 5))
|
||||
(c6 (nth cells 6))
|
||||
(c7 (nth cells 7))
|
||||
(c8 (nth cells 8))
|
||||
(c9 (nth cells 9))
|
||||
(cA (nth cells 10))
|
||||
(cB (nth cells 11))
|
||||
(cC (nth cells 12))
|
||||
(cD (nth cells 13))
|
||||
(cE (nth cells 14))
|
||||
(cF (nth cells 15)))
|
||||
(mk-conj
|
||||
(fd-in c0 digits-1-4)
|
||||
(fd-in c1 digits-1-4)
|
||||
(fd-in c2 digits-1-4)
|
||||
(fd-in c3 digits-1-4)
|
||||
(fd-in c4 digits-1-4)
|
||||
(fd-in c5 digits-1-4)
|
||||
(fd-in c6 digits-1-4)
|
||||
(fd-in c7 digits-1-4)
|
||||
(fd-in c8 digits-1-4)
|
||||
(fd-in c9 digits-1-4)
|
||||
(fd-in cA digits-1-4)
|
||||
(fd-in cB digits-1-4)
|
||||
(fd-in cC digits-1-4)
|
||||
(fd-in cD digits-1-4)
|
||||
(fd-in cE digits-1-4)
|
||||
(fd-in cF digits-1-4)
|
||||
(fd-distinct (list c0 c1 c2 c3))
|
||||
(fd-distinct (list c4 c5 c6 c7))
|
||||
(fd-distinct (list c8 c9 cA cB))
|
||||
(fd-distinct (list cC cD cE cF))
|
||||
(fd-distinct (list c0 c4 c8 cC))
|
||||
(fd-distinct (list c1 c5 c9 cD))
|
||||
(fd-distinct (list c2 c6 cA cE))
|
||||
(fd-distinct (list c3 c7 cB cF))
|
||||
(fd-distinct (list c0 c1 c4 c5))
|
||||
(fd-distinct (list c2 c3 c6 c7))
|
||||
(fd-distinct (list c8 c9 cC cD))
|
||||
(fd-distinct (list cA cB cE cF))
|
||||
(fd-label cells)))))
|
||||
|
||||
;; --- Tests ---
|
||||
|
||||
(mk-test
|
||||
"sudoku-4x4-empty-grid-count"
|
||||
(let
|
||||
((sols (run* q (fresh (c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 cA cB cC cD cE cF) (== q (list c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 cA cB cC cD cE cF)) (sudoku-4x4 (list c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 cA cB cC cD cE cF))))))
|
||||
(len sols))
|
||||
288)
|
||||
|
||||
(mk-test
|
||||
"sudoku-4x4-impossible-clue-empty"
|
||||
(run*
|
||||
q
|
||||
(fresh
|
||||
(c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 cA cB cC cD cE cF)
|
||||
(== c0 1)
|
||||
(== c1 1)
|
||||
(== q (list c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 cA cB cC cD cE cF))
|
||||
(sudoku-4x4 (list c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 cA cB cC cD cE cF))))
|
||||
(list))
|
||||
|
||||
(mk-tests-run!)
|
||||
@@ -1,56 +0,0 @@
|
||||
;; lib/minikanren/tests/tabling-slg.sx — Phase 7 piece A: SLG-style tabling.
|
||||
|
||||
;; --- table-2-slg with Fibonacci (sanity: same answer as naive table-2) ---
|
||||
|
||||
(mk-tab-clear!)
|
||||
(define
|
||||
slg-fib-o
|
||||
(table-2-slg
|
||||
"slg-fib"
|
||||
(fn
|
||||
(n result)
|
||||
(conde
|
||||
((== n 0) (== result 0))
|
||||
((== n 1) (== result 1))
|
||||
((fresh (n-1 n-2 r-1 r-2) (lto-i 1 n) (minuso-i n 1 n-1) (minuso-i n 2 n-2) (slg-fib-o n-1 r-1) (slg-fib-o n-2 r-2) (pluso-i r-1 r-2 result)))))))
|
||||
|
||||
(mk-tab-clear!)
|
||||
(mk-test "slg-fib-five" (run* q (slg-fib-o 5 q)) (list 5))
|
||||
(mk-tab-clear!)
|
||||
(mk-test "slg-fib-ten" (run* q (slg-fib-o 10 q)) (list 55))
|
||||
|
||||
;; --- table-3-slg with cyclic-graph patho ---
|
||||
|
||||
(define slg-cyc-edges (list (list :a :b) (list :b :a) (list :b :c)))
|
||||
(define slg-cyc-edgeo (fn (x y) (membero (list x y) slg-cyc-edges)))
|
||||
|
||||
(mk-tab-clear!)
|
||||
(define
|
||||
tab-patho
|
||||
(table-3-slg
|
||||
"patho"
|
||||
(fn
|
||||
(x y path)
|
||||
(conde
|
||||
((slg-cyc-edgeo x y) (== path (list x y)))
|
||||
((fresh (z mid) (slg-cyc-edgeo x z) (tab-patho z y mid) (conso x mid path)))))))
|
||||
|
||||
(mk-tab-clear!)
|
||||
(mk-test
|
||||
"slg-cyclic-direct"
|
||||
(run* q (tab-patho :a :b q))
|
||||
(list (list :a :b)))
|
||||
|
||||
(mk-tab-clear!)
|
||||
(mk-test
|
||||
"slg-cyclic-multi-hop"
|
||||
(run* q (tab-patho :a :c q))
|
||||
(list (list :a :b :c)))
|
||||
|
||||
(mk-tab-clear!)
|
||||
(mk-test
|
||||
"slg-cyclic-self-loop-finite"
|
||||
(run* q (tab-patho :a :a q))
|
||||
(list (list :a :b :a)))
|
||||
|
||||
(mk-tests-run!)
|
||||
56
lib/perf-smoke.sx
Normal file
56
lib/perf-smoke.sx
Normal file
@@ -0,0 +1,56 @@
|
||||
;; lib/perf-smoke.sx — substrate perf smoke test
|
||||
;;
|
||||
;; Four micro-benchmarks exercising different substrate hot paths. Each
|
||||
;; emits its own elapsed-ms via clock-milliseconds. A wrapper script
|
||||
;; (scripts/perf-smoke.sh) parses the output and compares to reference
|
||||
;; numbers, exiting non-zero on any 5× or worse regression.
|
||||
;;
|
||||
;; Workloads are chosen for distinct failure modes:
|
||||
;; bench-fib — function-call dispatch (recursive arithmetic)
|
||||
;; bench-let-chain — env construction (deep let bindings × N)
|
||||
;; bench-map-sq — HO-form dispatch + lambda creation
|
||||
;; bench-tail-loop — TCO + primitive dispatch in tight loop
|
||||
|
||||
(define (bench-fib n)
|
||||
(let ((fib (fn (n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))))
|
||||
(let ((s (clock-milliseconds)))
|
||||
(fib n)
|
||||
(- (clock-milliseconds) s))))
|
||||
|
||||
(define (bench-let-chain iters)
|
||||
(let ((s (clock-milliseconds)))
|
||||
(let loop ((i 0) (acc 0))
|
||||
(if (= i iters)
|
||||
(- (clock-milliseconds) s)
|
||||
(loop
|
||||
(+ i 1)
|
||||
(let ((a 1) (b 2) (c 3) (d 4) (e 5) (f 6) (g 7) (h 8))
|
||||
(+ a b c d e f g h acc)))))))
|
||||
|
||||
(define (bench-map-sq n)
|
||||
(let ((s (clock-milliseconds)))
|
||||
(map (fn (x) (* x x)) (range 1 (+ n 1)))
|
||||
(- (clock-milliseconds) s)))
|
||||
|
||||
(define (bench-tail-loop iters)
|
||||
(let ((s (clock-milliseconds)))
|
||||
(let loop ((i 0))
|
||||
(if (= i iters)
|
||||
(- (clock-milliseconds) s)
|
||||
(loop (+ i 1))))))
|
||||
|
||||
(define (perf-smoke)
|
||||
;; Warm-up: populate JIT cache so the timed pass sees the steady state.
|
||||
(bench-fib 12)
|
||||
(bench-let-chain 200)
|
||||
(bench-map-sq 100)
|
||||
(bench-tail-loop 500)
|
||||
;; Timed pass. Sizes tuned for ~50-200 ms each on a quiet machine.
|
||||
(let ((r-fib (bench-fib 18))
|
||||
(r-let (bench-let-chain 1000))
|
||||
(r-map (bench-map-sq 500))
|
||||
(r-tail (bench-tail-loop 5000)))
|
||||
(str "perf-smoke fib18=" r-fib
|
||||
" let1000=" r-let
|
||||
" map500=" r-map
|
||||
" tail5000=" r-tail)))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -59,7 +59,7 @@ cat > "$TMPFILE" << EPOCHS
|
||||
(eval "tcl-test-summary")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 2400 "$SX_SERVER" < "$TMPFILE" 2>&1)
|
||||
OUTPUT=$(timeout 300 "$SX_SERVER" < "$TMPFILE" 2>&1)
|
||||
[ "$VERBOSE" = "-v" ] && echo "$OUTPUT"
|
||||
|
||||
# Extract summary line from epoch 11 output
|
||||
|
||||
@@ -415,6 +415,268 @@
|
||||
:result)
|
||||
"")
|
||||
|
||||
; 60-63. Phase 6a namespace :: prefix
|
||||
(ok "ns-set-from-proc-reaches-global"
|
||||
(get
|
||||
(run
|
||||
"proc f {x} { set ::g $x }\nf hello\nset ::g")
|
||||
:result)
|
||||
"hello")
|
||||
|
||||
(ok "ns-read-from-proc"
|
||||
(get
|
||||
(run
|
||||
"set ::v 42\nproc f {} { return $::v }\nf")
|
||||
:result)
|
||||
"42")
|
||||
|
||||
(ok "ns-incr-via-prefix"
|
||||
(get
|
||||
(run
|
||||
"set ::n 5\nproc bump {} { incr ::n }\nbump\nbump\nset ::n")
|
||||
:result)
|
||||
"7")
|
||||
|
||||
(ok "ns-different-from-local"
|
||||
(get
|
||||
(run
|
||||
"set x outer\nproc f {} { set x inner; set ::x global; return $x }\nf")
|
||||
:result)
|
||||
"inner")
|
||||
|
||||
; 64-69. Phase 6b list ops (lassign, lrepeat, lset, lmap)
|
||||
(ok "lassign-three"
|
||||
(get (run "lassign {a b c d e} x y z\nlist $x $y $z") :result)
|
||||
"a b c")
|
||||
|
||||
(ok "lassign-leftover"
|
||||
(get (run "lassign {1 2 3 4 5} a b") :result)
|
||||
"3 4 5")
|
||||
|
||||
(ok "lrepeat-basic"
|
||||
(get (run "lrepeat 3 a") :result)
|
||||
"a a a")
|
||||
|
||||
(ok "lrepeat-multi"
|
||||
(get (run "lrepeat 2 x y") :result)
|
||||
"x y x y")
|
||||
|
||||
(ok "lset-replaces"
|
||||
(get (run "set L {a b c d}\nlset L 2 ZZ\nset L") :result)
|
||||
"a b ZZ d")
|
||||
|
||||
(ok "lmap-square"
|
||||
(get (run "lmap n {1 2 3 4} {expr {$n * $n}}") :result)
|
||||
"1 4 9 16")
|
||||
|
||||
; 70-72. Phase 6c dict additions (lappend, remove, filter)
|
||||
(ok "dict-lappend-extends"
|
||||
(get (run "set d {tags {a b}}\ndict lappend d tags c d\nset d") :result)
|
||||
"tags {a b c d}")
|
||||
|
||||
(ok "dict-remove"
|
||||
(get (run "dict remove {a 1 b 2 c 3} b") :result)
|
||||
"a 1 c 3")
|
||||
|
||||
(ok "dict-filter-key"
|
||||
(get (run "dict filter {alpha 1 beta 2 gamma 3} key a*") :result)
|
||||
"alpha 1")
|
||||
|
||||
; 73-79. Phase 6d format and scan
|
||||
(ok "format-int-padded"
|
||||
(get (run "format {%05d} 42") :result)
|
||||
"00042")
|
||||
|
||||
(ok "format-float-precision"
|
||||
(get (run "format {%.2f} 3.14159") :result)
|
||||
"3.14")
|
||||
|
||||
(ok "format-hex"
|
||||
(get (run "format {%x} 255") :result)
|
||||
"ff")
|
||||
|
||||
(ok "format-char"
|
||||
(get (run "format {%c} 65") :result)
|
||||
"A")
|
||||
|
||||
(ok "format-string-left"
|
||||
(get (run "format {%-5s|} hi") :result)
|
||||
"hi |")
|
||||
|
||||
(ok "scan-two-ints"
|
||||
(get (run "scan {12 34} {%d %d} a b\nlist $a $b") :result)
|
||||
"12 34")
|
||||
|
||||
(ok "scan-count"
|
||||
(get (run "scan {hello 42} {%s %d}") :result)
|
||||
"hello 42")
|
||||
|
||||
; 80-82. Phase 6e exec
|
||||
(ok "exec-echo"
|
||||
(get (run "exec echo hello world") :result)
|
||||
"hello world")
|
||||
|
||||
(ok "exec-printf-no-newline"
|
||||
(get (run "exec /bin/printf x") :result)
|
||||
"x")
|
||||
|
||||
(ok "exec-with-args"
|
||||
(get (run "exec /bin/echo -n test") :result)
|
||||
"test")
|
||||
|
||||
; 83-87. Phase 7a try/trap with varlist
|
||||
(ok "try-trap-prefix-match"
|
||||
(get
|
||||
(run
|
||||
"try {throw {ARITH DIVZERO} divide-by-zero} trap {ARITH} {res} {set caught $res}")
|
||||
:result)
|
||||
"divide-by-zero")
|
||||
|
||||
(ok "try-trap-full-pattern"
|
||||
(get
|
||||
(run
|
||||
"try {throw {FOO BAR} bad} trap {FOO BAR} {res} {return matched-foo-bar}")
|
||||
:result)
|
||||
"matched-foo-bar")
|
||||
|
||||
(ok "try-on-error-opts"
|
||||
(get
|
||||
(run
|
||||
"try {error oops} on error {res opts} {dict get $opts -code}")
|
||||
:result)
|
||||
"1")
|
||||
|
||||
(ok "try-trap-no-match-falls-through"
|
||||
(get
|
||||
(run
|
||||
"set caught notrun\ncatch {try {throw {NOPE} bad} trap {OTHER} {r} {set caught matched}}\nset caught")
|
||||
:result)
|
||||
"notrun")
|
||||
|
||||
(ok "try-trap-then-on-error"
|
||||
(get
|
||||
(run
|
||||
"try {error generic} trap {SPECIFIC} {r} {return trap-fired} on error {r} {return on-error-fired}")
|
||||
:result)
|
||||
"on-error-fired")
|
||||
|
||||
; 88-92. Phase 7b exec pipelines + redirection
|
||||
(ok "exec-pipeline-tr"
|
||||
(get (run "exec echo hello world | tr a-z A-Z") :result)
|
||||
"HELLO WORLD")
|
||||
|
||||
(ok "exec-pipeline-wc"
|
||||
(get (run "exec /bin/echo abc | wc -c") :result)
|
||||
"4")
|
||||
|
||||
(ok "exec-redirect-stdout"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-7b-out.txt\nexec echo hello > $f\nset r [exec cat $f]\nfile delete $f\nreturn $r")
|
||||
:result)
|
||||
"hello")
|
||||
|
||||
(ok "exec-redirect-stdin"
|
||||
(get
|
||||
(run
|
||||
"set f /tmp/tcl-7b-in.txt\nset c [open $f w]\nputs -nonewline $c hi\nclose $c\nset r [exec cat < $f]\nfile delete $f\nreturn $r")
|
||||
:result)
|
||||
"hi")
|
||||
|
||||
(ok "exec-pipeline-three-stages"
|
||||
(get (run "exec echo {alpha beta gamma} | tr { } \\n | wc -l") :result)
|
||||
"3")
|
||||
|
||||
; 93-99. Phase 7c string command audit
|
||||
(ok "string-equal"
|
||||
(get (run "string equal hello hello") :result)
|
||||
"1")
|
||||
|
||||
(ok "string-equal-nocase"
|
||||
(get (run "string equal -nocase HELLO hello") :result)
|
||||
"1")
|
||||
|
||||
(ok "string-totitle"
|
||||
(get (run "string totitle hello") :result)
|
||||
"Hello")
|
||||
|
||||
(ok "string-reverse"
|
||||
(get (run "string reverse hello") :result)
|
||||
"olleh")
|
||||
|
||||
(ok "string-replace"
|
||||
(get (run "string replace hello 1 3 ZZZ") :result)
|
||||
"hZZZo")
|
||||
|
||||
(ok "string-is-xdigit-yes"
|
||||
(get (run "string is xdigit ff00aa") :result)
|
||||
"1")
|
||||
|
||||
(ok "string-is-true-yes"
|
||||
(get (run "string is true yes") :result)
|
||||
"1")
|
||||
|
||||
; 100-105. Phase 7e regexp anchoring/boundary audit
|
||||
(ok "regexp-anchor-start"
|
||||
(get (run "regexp {^hello} hello-world") :result)
|
||||
"1")
|
||||
|
||||
(ok "regexp-anchor-end"
|
||||
(get (run "regexp {world$} hello-world") :result)
|
||||
"1")
|
||||
|
||||
(ok "regexp-word-boundary"
|
||||
(get (run "regexp {\\bword\\b} \"the word here\"") :result)
|
||||
"1")
|
||||
|
||||
(ok "regexp-nocase"
|
||||
(get (run "regexp -nocase {HELLO} hello") :result)
|
||||
"1")
|
||||
|
||||
(ok "regexp-capture-var"
|
||||
(get (run "regexp {[0-9]+} abc123def captured\nset captured") :result)
|
||||
"123")
|
||||
|
||||
(ok "regsub-all"
|
||||
(get (run "regsub -all {[0-9]+} a1b22c333 X") :result)
|
||||
"aXbXcX")
|
||||
|
||||
; 106-110. Phase 7d TclOO basics
|
||||
(ok "oo-class-method"
|
||||
(get
|
||||
(run
|
||||
"oo::class create C {\nmethod get {} { return 42 }\n}\nset c [C new]\n$c get")
|
||||
:result)
|
||||
"42")
|
||||
|
||||
(ok "oo-constructor"
|
||||
(get
|
||||
(run
|
||||
"oo::class create G {\nconstructor {n} { set ::gname $n }\nmethod hello {} { return [string cat \"hi \" $::gname] }\n}\nset g [G new World]\n$g hello")
|
||||
:result)
|
||||
"hi World")
|
||||
|
||||
(ok "oo-inheritance-overridden"
|
||||
(get
|
||||
(run
|
||||
"oo::class create Animal {\nmethod sound {} { return generic }\n}\noo::class create Dog {\nsuperclass Animal\nmethod sound {} { return woof }\n}\nset d [Dog new]\n$d sound")
|
||||
:result)
|
||||
"woof")
|
||||
|
||||
(ok "oo-inheritance-inherited"
|
||||
(get
|
||||
(run
|
||||
"oo::class create Animal {\nmethod sound {} { return generic }\n}\noo::class create Cat {\nsuperclass Animal\n}\nset c [Cat new]\n$c sound")
|
||||
:result)
|
||||
"generic")
|
||||
|
||||
(ok "oo-multiple-instances"
|
||||
(get
|
||||
(run
|
||||
"oo::class create N {\nconstructor {x} { set ::nval $x }\nmethod get {} { return $::nval }\n}\nset a [N new 1]\nset b [N new 99]\n$b get")
|
||||
:result)
|
||||
"99")
|
||||
|
||||
(dict
|
||||
"passed"
|
||||
tcl-idiom-pass
|
||||
|
||||
@@ -158,7 +158,9 @@
|
||||
(begin
|
||||
(when (= (cur) "}") (advance! 1))
|
||||
{:type "var" :name name}))))))
|
||||
((tcl-ident-start? (cur))
|
||||
((or
|
||||
(tcl-ident-start? (cur))
|
||||
(and (= (cur) ":") (= (char-at 1) ":")))
|
||||
(let ((start pos))
|
||||
(begin
|
||||
(scan-ns-name!)
|
||||
|
||||
83
plans/agent-briefings/datalog-loop.md
Normal file
83
plans/agent-briefings/datalog-loop.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# datalog-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/datalog-on-sx.md` forever. Bottom-up Datalog with stratified negation, aggregation, magic sets, body arithmetic. Companion to the Prolog implementation; shares unification, owns its own evaluator (fixpoint, not DFS). One feature per commit.
|
||||
|
||||
```
|
||||
description: datalog-on-sx queue loop
|
||||
subagent_type: general-purpose
|
||||
run_in_background: true
|
||||
isolation: worktree
|
||||
```
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/datalog-on-sx.md`. You run in an isolated git worktree on branch `loops/datalog`. You work the plan's roadmap forever, one commit per feature. Push to `origin/loops/datalog` after every commit.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/datalog-on-sx.md` — Roadmap + Progress log tell you where you are. Phases 1–10, all `[ ]` until something ships.
|
||||
2. `ls lib/datalog/` — if the directory does not exist, you are at Phase 1. Create it on the first code commit.
|
||||
3. If `lib/datalog/tests/*.sx` exist, run them via the epoch protocol against `sx_server.exe`. They must be green before new work.
|
||||
4. If `lib/datalog/scoreboard.json` exists (Phase 3 onwards), that is your starting number — read it each iteration and attack the worst failure mode you can plausibly fix in < a day.
|
||||
5. Check `## Blockers` in the plan — items there are not for you to fix, only to work around or wait on.
|
||||
|
||||
## The queue
|
||||
|
||||
Work in phase order per `plans/datalog-on-sx.md`:
|
||||
|
||||
- **Phase 1** — tokenizer + parser (facts, rules, queries, body arithmetic operators tokenised here)
|
||||
- **Phase 2** — unification + substitution (port or share with `lib/prolog/`; no function symbols → simpler)
|
||||
- **Phase 3** — EDB + naive evaluation + **safety analysis** + first scoreboard
|
||||
- **Phase 4** — built-in predicates + body arithmetic (`<`, `>`, `=`, `is`, `+`, `-`, `*`, `/`)
|
||||
- **Phase 5** — semi-naive evaluation (delta sets, performance)
|
||||
- **Phase 6** — magic sets (goal-directed bottom-up, opt-in)
|
||||
- **Phase 7** — stratified negation + dependency-graph SCC analysis
|
||||
- **Phase 8** — aggregation (count/sum/min/max, post-fixpoint pass)
|
||||
- **Phase 9** — SX embedding API (`dl-program`, `dl-query`, `dl-assert!`, `dl-retract!`)
|
||||
- **Phase 10** — Datalog as a query language for rose-ash (federation/permissions/feeds demo)
|
||||
|
||||
Within a phase, pick the checkbox with the best tests-per-effort ratio. Once the scoreboard exists (end of Phase 3), it is your north star.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` in plan → append Progress log → push → next.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/datalog/**` and `plans/datalog-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. You may **read** `lib/prolog/` to understand unification — port code into `lib/datalog/unify.sx`, do not import across language boundaries.
|
||||
- **Non-goals are hard non-goals.** Do not implement function symbols, disjunctive heads, well-founded semantics, tabled top-down, constraint Datalog, or distributed evaluation. If a query needs one of these, add a Blockers entry and move on.
|
||||
- **NEVER call `sx_build`.** 600s watchdog will kill you before OCaml finishes. If `sx_server.exe` is broken, add a Blockers entry and stop.
|
||||
- **Shared-file issues** → plan's Blockers section with a minimal repro. Don't fix them.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit. Never `Edit`/`Read`/`Write` on `.sx`.
|
||||
- **Worktree:** commit, then push to `origin/loops/datalog`. Never touch `main`. Never push to `architecture`.
|
||||
- **Commit granularity:** one feature per commit. Short factual messages: `datalog: safety analysis + 6 rejection tests`.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
- **If blocked** for two iterations on the same issue, add to Blockers and move on.
|
||||
|
||||
## Datalog-specific gotchas
|
||||
|
||||
- **Bottom-up, not DFS.** The evaluator iterates rules until no new tuples are derived. There is no goal stack, no backtracking, no cut. If you find yourself reaching for delimited continuations, you are writing Prolog by mistake.
|
||||
- **Termination guaranteed by the language, not the engine.** No function symbols → finite Herbrand base → fixpoint always reached. Do **not** add safety nets like step limits — if your fixpoint diverges, the bug is in the engine or the program is illegal (unsafe rule, function-symbol smuggling).
|
||||
- **Safety analysis must reject early.** `(p X) :- (< X 5).` is unsafe — `X` is unbound when `<` runs. Reject at `dl-add-rule!` time with a clear error. Do not let unsafe rules into the EDB and discover the problem at fixpoint time.
|
||||
- **`is` binds left, requires right ground.** `(is Z (+ X Y))` binds `Z` iff `X` and `Y` are already bound by some prior body literal. This is asymmetric — built-in predicates do not "join" the way EDB literals do.
|
||||
- **Stratification rejects programs at load time.** `(p X) :- (not (q X)). (q X) :- (not (p X)).` is non-stratifiable. Detect via SCC analysis on the dependency graph; report the cycle, do not attempt evaluation.
|
||||
- **Aggregation is a separate post-fixpoint pass.** `count(X, Goal)` cannot participate in the recursive fixpoint without breaking monotonicity. Compute the underlying relation via fixpoint, then aggregate.
|
||||
- **Magic sets are opt-in and must be equivalence-tested.** A magic-rewritten program must produce the same answers as the original on every input. Add a property test that runs both strategies on small EDBs and diffs the results.
|
||||
- **EDB vs IDB.** Extensional database (EDB) = ground facts only, asserted directly. Intensional database (IDB) = relations defined by rules. `dl-add-fact!` populates EDB; `dl-add-rule!` populates IDB. A relation cannot be both — flag conflicts.
|
||||
- **No mixing of term representations.** Pick ONE shape for atoms (e.g. SX symbols), ONE for variables (e.g. `{:var "X"}` dicts or symbols starting with uppercase), ONE for ground tuples (e.g. SX lists). Document the choice in the plan's architecture sketch.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr — wrap multiples in `begin`.
|
||||
- `env-bind!` creates a binding; `env-set!` mutates an existing one (walks scope chain).
|
||||
- `sx_validate` after every structural edit.
|
||||
- `list?` returns false on raw JS Arrays — host data must be SX-converted.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/datalog-on-sx.md` inline.
|
||||
- Short, factual commit messages (`datalog: semi-naive delta sets (+12)`).
|
||||
- One feature per iteration. Commit. Log. Push. Next.
|
||||
|
||||
Go. Start by reading the plan; find the first unchecked `[ ]`; implement it.
|
||||
119
plans/agent-briefings/elm-loop.md
Normal file
119
plans/agent-briefings/elm-loop.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# elm-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/elm-on-sx.md` forever. Elm 0.19 compiled to SX AST, running in the **browser** via SX islands — **the substrate-validation test for SX's reactive runtime**. Model/Update/View maps almost directly onto SX signals + components. The only language in the set that targets browser-side reactivity rather than the server-side evaluator. One feature per commit.
|
||||
|
||||
```
|
||||
description: elm-on-sx queue loop
|
||||
subagent_type: general-purpose
|
||||
run_in_background: true
|
||||
isolation: worktree
|
||||
```
|
||||
|
||||
## DO NOT START WITHOUT THE PREREQUISITES
|
||||
|
||||
This loop **must not** start until all of the following are true:
|
||||
|
||||
1. **lib-guest Steps 3, 4, 6, 7 are `[done]`** — Elm's tokenizer consumes `lib/guest/lex.sx`, its parser consumes `lib/guest/pratt.sx`, its pattern matcher consumes `lib/guest/match.sx`, and **its indentation-sensitive lexer consumes `lib/guest/layout.sx`** (Elm has the off-side rule).
|
||||
2. **ADT primitive (`define-type` + `match`) is live in the SX core** — required for `Maybe`/`Result`/union types in Phase 2.
|
||||
|
||||
**Pre-flight check:**
|
||||
```
|
||||
ls /root/rose-ash/lib/guest/lex.sx /root/rose-ash/lib/guest/pratt.sx /root/rose-ash/lib/guest/match.sx /root/rose-ash/lib/guest/layout.sx
|
||||
printf '(epoch 1)\n(define-type test-adt (A) (B v))\n(epoch 2)\n(match (A) ((A) "ok") (_ "no"))\n' \
|
||||
| /root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe 2>&1 | tail -3
|
||||
```
|
||||
If any lib-guest file is missing OR `define-type`/`match` errors instead of returning `"ok"`, **stop and report**. Do not start.
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/elm-on-sx.md`. You run in an isolated git worktree on branch `loops/elm`. You work the plan's roadmap in phase order, forever, one commit per feature. Push to `origin/loops/elm` after every commit.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/elm-on-sx.md` — Roadmap + Progress log + Blockers tell you where you are.
|
||||
2. Run the pre-flight check above. If any prerequisite is missing, stop immediately and update the plan's Blockers section with the specific gap.
|
||||
3. `ls lib/elm/` — pick up from the most advanced file that exists. If the directory does not exist, you are at Phase 1.
|
||||
4. If `lib/elm/tests/*.sx` exist, run them via the epoch protocol against `sx_server.exe`. They must be green before new work.
|
||||
5. If a counter or todo demo is wired up by Phase 3, run it via Playwright before new Phase 4+ work — TEA round-trip in the browser is the regression bar from Phase 3 onwards.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/elm-on-sx.md`:
|
||||
|
||||
- **Phase 1** — tokenizer + parser (consuming `lib/guest/lex.sx`, `lib/guest/pratt.sx`, `lib/guest/layout.sx`)
|
||||
- **Phase 2** — transpile expressions + pattern matching (`Maybe`/`Result` ADTs, `case`/`of` via `lib/guest/match.sx`)
|
||||
- **Phase 3** — **The Elm Architecture runtime** (the headline phase — `Browser.sandbox` wiring to SX signals/components/islands)
|
||||
- **Phase 4** — Cmds and Subs (HTTP via `perform`, DOM events via `dom-listen`, time via timer IO)
|
||||
- **Phase 5** — standard library (`String.*`, `List.*`, `Dict.*`, `Set.*`, `Maybe.*`, `Result.*`, `Tuple.*`, `Basics.*`, `Random.*`)
|
||||
- **Phase 6** — full browser integration (`Browser.application`, URL routing, `Json.Decode`/`Encode`, ports)
|
||||
|
||||
Within a phase, pick the checkbox with the best tests-per-effort ratio. Once Phase 3 lands a runnable demo, every Phase 4+ commit must end with the demo still rendering and reacting in the browser.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` in plan → append Progress log → push → next.
|
||||
|
||||
## Substrate-validation discipline (the TEA test)
|
||||
|
||||
The reason Elm exists in this set is to verify that SX's reactive runtime — `defisland`, `make-signal`, `provide`/`context`, `dom-listen` — can host The Elm Architecture cleanly. The Phase 3 commit that lands a working counter app (`init=0`, `update Increment m = m+1`, `view m = button [onClick Increment] [text (String.fromInt m)]`) is the single most important commit in this whole plan.
|
||||
|
||||
After every Phase 3 commit, append to the Progress log a line stating which TEA pattern was exercised:
|
||||
|
||||
- **Static view** — view function with no signal subscription. Trivial.
|
||||
- **Read-only signal** — view reads model signal; no message dispatch yet.
|
||||
- **Round-trip** — message → update → model signal change → view re-render. The counter app is this.
|
||||
- **Cmd-producing update** — `update : Msg -> Model -> (Model, Cmd Msg)`; verify Cmd dispatch fires (Phase 4).
|
||||
- **Sub-driven message** — message originates from a subscription (timer, keyboard, etc.); verify Sub teardown on unmount (Phase 4).
|
||||
|
||||
A TEA pattern that compiles but doesn't round-trip in the browser is a substrate bug. Open a Blockers entry, do not fix the reactive runtime from this loop.
|
||||
|
||||
## Browser test discipline
|
||||
|
||||
From Phase 3 onwards, the regression bar is **a working demo in the browser**, not just SX-level unit tests. After every commit that touches `lib/elm/runtime.sx` or the TEA wiring:
|
||||
|
||||
1. Build the demo: `bash lib/elm/build-demo.sh` (create this script in Phase 3 — wraps the demo as an island and serves it).
|
||||
2. Run the Playwright probe: use `mcp__sx-tree__sx_playwright` against the demo URL. Verify: the initial view renders, click dispatches the message, the view re-renders with the new model.
|
||||
3. If the demo doesn't round-trip, revert the commit. Do not paper over with workarounds.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/elm/**` and `plans/elm-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, `lib/guest/**` (read-only consumer), `web/` (the reactive runtime — read-only), or other `lib/<lang>/`.
|
||||
- **Consume `lib/guest/`** wherever it covers a need (lex, pratt, match, layout). Hand-rolling defeats the validation goal.
|
||||
- **Do not patch the reactive runtime from this loop.** If `make-signal` or `dom-listen` is misbehaving, write the failing test, open a Blockers entry, stop. The fix lives in `web/` and `spec/` and is not your scope.
|
||||
- **No type inference, no exhaustiveness checking.** Type errors surface at eval time. Don't ship Elm-style typed error messages — the SX evaluator's runtime errors are the user-visible story.
|
||||
- **No module system in Phase 1.** Imports are parsed and ignored until Phase 6. Until then, all of `Html.*`, `List.*`, etc. are accessible as flat globals provided by `lib/elm/runtime.sx`.
|
||||
- **NEVER call `sx_build`.** 600s watchdog will kill you. If `sx_server.exe` is broken, add a Blockers entry and stop.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit. Never `Edit`/`Read`/`Write` on `.sx`.
|
||||
- **Worktree:** commit, then push to `origin/loops/elm`. Never touch `main`. Never push to `architecture`.
|
||||
- **Commit granularity:** one feature per commit. Short factual messages: `elm: case-of patterns + 5 tests`.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
- **If blocked** for two iterations on the same issue, add to Blockers and move on.
|
||||
|
||||
## Elm-specific gotchas
|
||||
|
||||
- **Indentation-sensitive lexer.** Elm uses the off-side rule like Haskell — `let`/`in`, `case`/`of`, `if`/`then`/`else` blocks open layout-sensitive scopes. **`lib/guest/layout.sx` is the prerequisite, not optional.** Don't reinvent the layout algorithm.
|
||||
- **`Model` is a *value*, not a reference.** `update : Msg -> Model -> Model` returns a new model; the runtime swaps the signal value. Don't expose mutable state to user code — the swap happens inside `Browser.sandbox`/`element`/`application`.
|
||||
- **`Html msg` is a tagged tree.** Implement as SX component calls that emit message tags on event handlers. `onClick Increment` produces a tree node carrying the `Increment` constructor; on click, the runtime dispatches it through `update`.
|
||||
- **`Cmd msg` is opaque, async, fire-and-forget.** It produces a future message (or none) via `perform`. Do not expose `Cmd` internals to user code — `Http.get`, `Task.perform`, etc. construct `Cmd` values.
|
||||
- **`Sub msg` registers a subscription.** Implement as `dom-listen` (DOM events) or timer IO (`Time.every`) wired to message dispatch. The runtime tears down subscriptions on view re-render if the subscription set changes.
|
||||
- **Pipe `|>` is left-associative reverse application.** `x |> f |> g` = `g(f(x))`. Parse as low-precedence infix.
|
||||
- **`<<`/`>>` are function composition.** `f << g` = `\x -> f(g(x))`. Distinct from `|>`/`<|` (application).
|
||||
- **Records are dicts with fixed keys.** `{x=1, y=2}` → `{:x 1 :y 2}`; `{r | x = 5}` → `(dict-set r :x 5)`. Field access `.x` parses as `\r -> r.x`.
|
||||
- **`String` is opaque** — not `List Char`. Implement `String.toList`/`fromList` for conversion. Don't index strings directly.
|
||||
- **`port` keyword is for Phase 6.** In Phase 1 parse but ignore; in Phase 6 wire to SX `host-call` for JS interop.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr — wrap multiples in `begin`.
|
||||
- `env-bind!` creates a binding; `env-set!` mutates an existing one (walks scope chain).
|
||||
- `sx_validate` after every structural edit.
|
||||
- `list?` returns false on raw JS Arrays — host data must be SX-converted.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/elm-on-sx.md` inline.
|
||||
- Short, factual commit messages (`elm: Browser.sandbox + counter demo green`).
|
||||
- One feature per iteration. Commit. Log. Push. Next.
|
||||
|
||||
Go. Run the pre-flight check. If lib-guest or the ADT primitive is not in place, stop and report. Otherwise read the plan, find the first unchecked `[ ]`, implement it.
|
||||
107
plans/agent-briefings/koka-loop.md
Normal file
107
plans/agent-briefings/koka-loop.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# koka-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/koka-on-sx.md` forever. Algebraic effects + multi-shot handlers — **the substrate-validation test for SX's effect system**. Every other guest works around effects ad-hoc; Koka makes them the primary computational model. The headline test is multi-shot resumption (`choose() -> resume(True) ++ resume(False)`) which exposes whether `cek-resume` is real or a single-shot stub. One feature per commit.
|
||||
|
||||
```
|
||||
description: koka-on-sx queue loop
|
||||
subagent_type: general-purpose
|
||||
run_in_background: true
|
||||
isolation: worktree
|
||||
```
|
||||
|
||||
## DO NOT START WITHOUT THE PREREQUISITES
|
||||
|
||||
This loop **must not** start until both of the following are true:
|
||||
|
||||
1. **lib-guest Steps 3, 4, 6 are `[done]`** — Koka's tokenizer consumes `lib/guest/lex.sx`, its parser consumes `lib/guest/pratt.sx`, its pattern matcher consumes `lib/guest/match.sx`.
|
||||
2. **ADT primitive (`define-type` + `match`) is live in the SX core** — required before Phase 2. Track via `plans/sx-improvements.md` Phase 3 (Steps 5–8) or its successor.
|
||||
|
||||
**Pre-flight check:**
|
||||
```
|
||||
ls /root/rose-ash/lib/guest/lex.sx /root/rose-ash/lib/guest/pratt.sx /root/rose-ash/lib/guest/match.sx
|
||||
printf '(epoch 1)\n(define-type test-adt (A) (B v))\n(epoch 2)\n(match (A) ((A) "ok") (_ "no"))\n' \
|
||||
| /root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe 2>&1 | tail -3
|
||||
```
|
||||
If any lib-guest file is missing OR `define-type`/`match` errors instead of returning `"ok"`, **stop and report**. Do not start.
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/koka-on-sx.md`. You run in an isolated git worktree on branch `loops/koka`. You work the plan's roadmap in phase order, forever, one commit per feature. Push to `origin/loops/koka` after every commit.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/koka-on-sx.md` — Roadmap + Progress log + Blockers tell you where you are.
|
||||
2. Run the pre-flight check above. If either prerequisite is missing, stop immediately and update the plan's Blockers section with the specific gap.
|
||||
3. `ls lib/koka/` — pick up from the most advanced file that exists. If the directory does not exist, you are at Phase 1.
|
||||
4. If `lib/koka/tests/*.sx` exist, run them via the epoch protocol against `sx_server.exe`. They must be green before new work.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/koka-on-sx.md`:
|
||||
|
||||
- **Phase 1** — tokenizer + parser (consuming `lib/guest/lex.sx` + `lib/guest/pratt.sx`)
|
||||
- **Phase 2** — ADT definitions + match (consuming `lib/guest/match.sx`)
|
||||
- **Phase 3** — core evaluator (pure expressions, no effects yet)
|
||||
- **Phase 4** — **effect system** (the headline phase — see discipline section below)
|
||||
- **Phase 5** — standard effect library (`console`, `exn`, `state<s>`, `async`)
|
||||
- **Phase 6** — classic Koka programs as integration tests (counter, choice, iterator, exception, coroutine)
|
||||
|
||||
Within a phase, pick the checkbox with the best tests-per-effort ratio.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` in plan → append Progress log → push → next.
|
||||
|
||||
## Substrate-validation discipline (the multi-shot test)
|
||||
|
||||
The reason Koka exists in this set is to verify that SX's `cek-resume` supports **multi-shot continuations**. The Phase 4 commit that lands `choose() -> resume(True) ++ resume(False)` returning `[True, True, False, True]` is the single most important commit in this whole plan. Everything before it is scaffolding; everything after it is filling out the language.
|
||||
|
||||
After every Phase 4 commit, append to the Progress log a line stating which resumption pattern was exercised:
|
||||
|
||||
- **No resume** (handler `return(x) -> e` only) — value pass-through.
|
||||
- **Tail resumption** (`op() -> resume(v)`) — handler resumes exactly once, in tail position. Should be optimisable; verify no extra allocation.
|
||||
- **Single resume not in tail** (`op() -> let x = resume(v) in compute(x)`) — handler resumes once, then does work after.
|
||||
- **Multi-shot** (`choose() -> resume(True) ++ resume(False)`) — handler resumes the same continuation twice.
|
||||
- **Zero resume** (handler returns without calling resume) — abort/escape semantics.
|
||||
|
||||
A handler that compiles but does the wrong thing under multi-shot is a substrate bug, not a Koka bug. Open a Blockers entry, do not fix the substrate from this loop.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/koka/**` and `plans/koka-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, `lib/guest/**` (read-only consumer), or other `lib/<lang>/`.
|
||||
- **Consume `lib/guest/`** wherever it covers a need (lex, pratt, match). Hand-rolling defeats the validation goal.
|
||||
- **Do not patch the substrate from this loop.** If `cek-resume` is misbehaving, write the failing test, open a Blockers entry, stop. The fix lives in `spec/evaluator.sx` and is not your scope.
|
||||
- **Effect types are deferred entirely.** Track effects at runtime only — an unhandled effect at the top level raises a runtime error, not a type error. No row polymorphism.
|
||||
- **NEVER call `sx_build`.** 600s watchdog will kill you. If `sx_server.exe` is broken, add a Blockers entry and stop.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit. Never `Edit`/`Read`/`Write` on `.sx`.
|
||||
- **Worktree:** commit, then push to `origin/loops/koka`. Never touch `main`. Never push to `architecture`.
|
||||
- **Commit granularity:** one feature per commit. Short factual messages: `koka: state effect handler + 4 tests`.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
- **If blocked** for two iterations on the same issue, add to Blockers and move on.
|
||||
|
||||
## Koka-specific gotchas
|
||||
|
||||
- **Effects are dynamically scoped, not lexically.** When an effect operation `op()` fires inside a function called from inside a handler, the *call-time* handler stack matters, not the *definition-time* environment. This is the opposite of normal lexical scope. SX's `perform`/`cek-resume` is dynamically scoped by construction — that's why the mapping works.
|
||||
- **Handler installation is `with handler { body }`, not a function call.** The handler is installed for the dynamic extent of `body`. Implement as a `with-handler` evaluator form, not as a lambda taking a body argument — the body must run *inside* the handler frame, not be passed *into* a handler-creating call.
|
||||
- **`resume` is bound by the handler clause, not globally.** Each operation clause `op(args) -> body` exposes `resume` as a callable inside `body`. `resume(v)` continues the suspended computation with `v` as the value of the original `op()` call. Implement by capturing the continuation at the `perform` point and binding it to `resume` in the clause's env.
|
||||
- **`return(x) -> e` is the value clause.** When the handled body finishes without firing the effect, its value is bound to `x` in this clause and the result is `e`. If absent, default is `return(x) -> x`. This is *not* the same as a normal function return.
|
||||
- **Tail-resumptive handlers should be optimisable.** Most practical handlers (`state.get() -> resume(s)`, `console.println(s) -> { print(s); resume(()) }`) resume exactly once in tail position. The CEK should be able to detect this and skip the continuation capture entirely. If you discover the optimisation is missing, that's substrate work — open a Blockers entry, do not implement here.
|
||||
- **`type maybe<a> { Nothing; Just(value: a) }`.** Map directly to SX `(define-type maybe (Nothing) (Just value))`. Polymorphism erased at runtime — the type parameter is for documentation/future inference, not for evaluation.
|
||||
- **Pipe `|>` is reverse application.** `x |> f |> g` = `g(f(x))`. Parse as left-associative infix at low precedence.
|
||||
- **No type inference, no exhaustiveness checking.** Phase 2 match falls back to runtime `match-failure` exception on no clause hit. Don't try to verify exhaustiveness statically.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr — wrap multiples in `begin`.
|
||||
- `env-bind!` creates a binding; `env-set!` mutates an existing one (walks scope chain).
|
||||
- `sx_validate` after every structural edit.
|
||||
- `list?` returns false on raw JS Arrays — host data must be SX-converted.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/koka-on-sx.md` inline.
|
||||
- Short, factual commit messages (`koka: multi-shot choose + 3 tests`).
|
||||
- One feature per iteration. Commit. Log. Push. Next.
|
||||
|
||||
Go. Run the pre-flight check. If lib-guest or the ADT primitive is not in place, stop and report. Otherwise read the plan, find the first unchecked `[ ]`, implement it.
|
||||
98
plans/agent-briefings/minikanren-loop.md
Normal file
98
plans/agent-briefings/minikanren-loop.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# minikanren-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/minikanren-on-sx.md` forever. Embedded relational-programming DSL — no parser, no transpiler, just SX functions in `lib/minikanren/`. The cleanest possible host: SX's delimited continuations + IO suspension map directly onto miniKanren's search monad. **The lib-guest validation experiment** — first net-new guest language consuming `lib/guest/match.sx`, proving the kit is not Lua-shaped. One feature per commit.
|
||||
|
||||
```
|
||||
description: minikanren-on-sx queue loop
|
||||
subagent_type: general-purpose
|
||||
run_in_background: true
|
||||
isolation: worktree
|
||||
```
|
||||
|
||||
## DO NOT START WITHOUT THE PREREQUISITE
|
||||
|
||||
This loop **must not** start until **lib-guest Step 6 (`lib/guest/match.sx`) is `[done]`**. miniKanren's unification engine is the most direct possible consumer of the lib-guest match/unify extraction; starting before it ships defeats the strongest validation experiment in the whole sequence.
|
||||
|
||||
**Pre-flight check:**
|
||||
```
|
||||
ls /root/rose-ash/lib/guest/match.sx
|
||||
grep '^| 6 —' /root/rose-ash/plans/lib-guest.md
|
||||
```
|
||||
If `lib/guest/match.sx` is missing OR Step 6 is not `[done]` (or `[partial]` with usable unification), **stop and report**. Do not start.
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/minikanren-on-sx.md`. You run in an isolated git worktree on branch `loops/minikanren`. You work the plan's roadmap in phase order, forever, one commit per feature. Push to `origin/loops/minikanren` after every commit.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/minikanren-on-sx.md` — Roadmap + Progress log + Blockers tell you where you are.
|
||||
2. Run the pre-flight check above. If `lib/guest/match.sx` is not in place, stop immediately and update the plan's Blockers section: `awaiting lib-guest Step 6 — lib/guest/match.sx`.
|
||||
3. `ls lib/minikanren/` — pick up from the most advanced file that exists. If the directory does not exist, you are at Phase 1.
|
||||
4. If `lib/minikanren/tests/*.sx` exist, run them via the epoch protocol against `sx_server.exe`. They must be green before new work.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/minikanren-on-sx.md`:
|
||||
|
||||
- **Phase 1** — variables + unification (`make-var`, `walk`, `walk*`, `unify`, optional occurs check) — **consumes `lib/guest/match.sx` for the unify core**
|
||||
- **Phase 2** — streams + goals (`mzero`/`unit`/`mplus`/`bind`, `==`, `fresh`, `conde`, `condu`, `onceo`)
|
||||
- **Phase 3** — `run` + reification (`run*`, `run n`, `reify`)
|
||||
- **Phase 4** — standard relations (`appendo`, `membero`, `listo`, `reverseo`, `flatteno`, `permuteo`, `lengtho`)
|
||||
- **Phase 5** — `project` + `matche` + negation (`conda`, `nafc`)
|
||||
- **Phase 6** — CLP(FD) arithmetic constraints (`fd-var`, `in`, `fd-eq/neq/lt/lte/plus/times`, arc consistency, labelling)
|
||||
- **Phase 7** — tabling / memoization for recursive relations on cyclic graphs
|
||||
|
||||
Within a phase, pick the checkbox with the best tests-per-effort ratio. Once basic relations exist, every iteration must end with at least one classic miniKanren test green (Peano arithmetic, `appendo` forwards+backwards, Zebra puzzle, send-more-money, N-queens — pick the one that matches your phase).
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` in plan → append Progress log → push → next.
|
||||
|
||||
## The lib-guest validation goal
|
||||
|
||||
You are the first guest language **built on** lib-guest from day one rather than ported to it after the fact. Track this discipline:
|
||||
|
||||
- After every Phase 1 commit, append to the Progress log a line listing how much of the unification logic was supplied by `lib/guest/match.sx` vs how much you had to add locally.
|
||||
- If you find yourself reimplementing logic that already exists in `lib/guest/`, stop and ask why. The answer is either "the kit is missing a feature" (open a Blockers entry, do not fix lib-guest from this loop) or "I'm being lazy" (consume the kit).
|
||||
- If `lib/minikanren/unify.sx` ends up larger than ~50 lines, the kit is not earning its keep; flag it.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/minikanren/**` and `plans/minikanren-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, `lib/guest/**` (read-only consumer), or other `lib/<lang>/`.
|
||||
- **No parser, no transpiler, no tokenizer.** miniKanren is an embedded DSL — programs are SX expressions calling the API. If you find yourself wanting a parser, you are off-track.
|
||||
- **Consume `lib/guest/match.sx`** for unification. Do not reimplement.
|
||||
- **NEVER call `sx_build`.** 600s watchdog will kill you. If `sx_server.exe` is broken, add a Blockers entry and stop.
|
||||
- **Shared-file issues** → plan's Blockers section with a minimal repro. Don't fix them.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit. Never `Edit`/`Read`/`Write` on `.sx`.
|
||||
- **Worktree:** commit, then push to `origin/loops/minikanren`. Never touch `main`. Never push to `architecture`.
|
||||
- **Commit granularity:** one feature per commit. Short factual messages: `mk: appendo + 6 forward/backward tests`.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
- **If blocked** for two iterations on the same issue, add to Blockers and move on.
|
||||
|
||||
## miniKanren-specific gotchas
|
||||
|
||||
- **Goals are functions, not data.** A goal is `(fn (subst) → stream-of-substs)`. `fresh`/`conde`/`==` all return goals. Don't store goals as quoted lists.
|
||||
- **Streams must be lazy.** `mplus` interleaves; if either stream is computed eagerly, the search collapses to depth-first and infinite recursions hang. Use `delay`/`force` (or SX equivalent — check `lib/stdlib.sx` for thunk helpers).
|
||||
- **`conde` interleaves; `condu` commits.** `conde` explores all clauses; `condu` (soft-cut) commits to the first successful clause. Different semantics — pick the right one for the test.
|
||||
- **Reification names variables by occurrence order.** `(run* q (fresh (x y) (== q (list x y))))` should produce `(_0 _1)`, not arbitrary names. The reifier walks the answer term left-to-right and assigns `_0`, `_1`, ... in order. Test this explicitly.
|
||||
- **`appendo` is the canary.** It must run forwards (`(appendo '(a b) '(c d) ?)` → `((a b c d))`), backwards (`(appendo ?l ?s '(a b c))` → `(((), (a b c)) ((a), (b c)) ((a b), (c)) ((a b c), ()))`), and bidirectionally. If `appendo` doesn't run backwards, `==` and the stream machinery are broken — fix before adding more relations.
|
||||
- **CLP(FD) is its own beast.** Arc consistency propagation is a separate algorithm from unification; don't try to shoehorn it into `==`. Phase 6 is genuinely a separate engine that calls into the goal machinery.
|
||||
- **Tabling needs producer/consumer scheduling.** Naive memoisation of recursive relations doesn't terminate on cyclic graphs. Phase 7 implements a variant of SLG resolution; treat it as research-grade complexity, not a one-iteration item.
|
||||
- **No occurs check by default.** Standard miniKanren is permissive; `(unify-check ...)` is opt-in. Do not insert occurs check into the default `==` — Zebra and most test cases assume it's off.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr — wrap multiples in `begin`.
|
||||
- `env-bind!` creates a binding; `env-set!` mutates an existing one (walks scope chain).
|
||||
- `sx_validate` after every structural edit.
|
||||
- `list?` returns false on raw JS Arrays — host data must be SX-converted.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/minikanren-on-sx.md` inline.
|
||||
- Short, factual commit messages (`mk: conde interleaving + 4 tests`).
|
||||
- One feature per iteration. Commit. Log. Push. Next.
|
||||
|
||||
Go. Run the pre-flight check. If `lib/guest/match.sx` is not in place, stop and report. Otherwise read the plan, find the first unchecked `[ ]`, implement it.
|
||||
106
plans/agent-briefings/ocaml-loop.md
Normal file
106
plans/agent-briefings/ocaml-loop.md
Normal file
@@ -0,0 +1,106 @@
|
||||
# ocaml-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/ocaml-on-sx.md` forever. Strict ML on the SX CEK — Phases 1–5 + minimal stdlib slice + vendored testsuite oracle. Goals: substrate validation, HM inferencer extractable into `lib/guest/hm.sx`, reference oracle for other guest languages. **Dream is out of scope** (separate plan); ReasonML deferred. One feature per commit.
|
||||
|
||||
```
|
||||
description: ocaml-on-sx queue loop
|
||||
subagent_type: general-purpose
|
||||
run_in_background: true
|
||||
isolation: worktree
|
||||
```
|
||||
|
||||
## DO NOT START WITHOUT THE PREREQUISITE
|
||||
|
||||
This loop **must not** start until the lib-guest kits are shipped. OCaml's tokenizer should consume `lib/guest/lex.sx` (lib-guest Step 3); its parser should consume `lib/guest/pratt.sx` (Step 4); its pattern matcher should consume `lib/guest/match.sx` (Step 6); its HM inferencer should consume `lib/guest/hm.sx` (Step 8). Hand-rolling defeats the substrate-validation goal.
|
||||
|
||||
**Pre-flight check:**
|
||||
```
|
||||
ls /root/rose-ash/lib/guest/lex.sx /root/rose-ash/lib/guest/pratt.sx \
|
||||
/root/rose-ash/lib/guest/match.sx /root/rose-ash/lib/guest/layout.sx \
|
||||
/root/rose-ash/lib/guest/hm.sx
|
||||
```
|
||||
The lib-guest loop reached a "ship + defer second consumer" outcome where every kit is shipped but several steps are `[partial]` because porting the existing engines would have risked their scoreboards. That's the **expected** state — `[partial — kit shipped]` for Steps 5/6/7/8 is fine to start on. **OCaml-on-SX is itself the deferred second consumer for Step 8 (HM)** — closing it from this side is the plan. Only stop if any of those `lib/guest/*.sx` files are missing.
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/ocaml-on-sx.md`. You run in an isolated git worktree on branch `loops/ocaml`. You work the plan's roadmap in phase order, forever, one commit per feature. Push to `origin/loops/ocaml` after every commit.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/ocaml-on-sx.md` — Roadmap + Progress log + Blockers tell you where you are.
|
||||
2. Run the pre-flight check above. If any of the listed `lib/guest/*.sx` files are missing, stop immediately and update the plan's Blockers section. `[partial — kit shipped]` status on Steps 5–8 is expected and fine to start on.
|
||||
3. `ls lib/ocaml/` — pick up from the most advanced file that exists. If the directory does not exist, you are at Phase 1.
|
||||
4. If `lib/ocaml/tests/*.sx` exist, run them via the epoch protocol against `sx_server.exe`. They must be green before new work.
|
||||
5. If `lib/ocaml/scoreboard.json` exists (Phase 5.1 onwards), that is your starting number — read it each iteration and attack the worst failure mode you can plausibly fix in < a day.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/ocaml-on-sx.md`:
|
||||
|
||||
- **Phase 1** — tokenizer + parser (consuming `lib/guest/lex.sx` + `lib/guest/pratt.sx`)
|
||||
- **Phase 2** — core evaluator (untyped: let/lambda/match/refs/try-with)
|
||||
- **Phase 3** — ADTs + pattern matching (consuming `lib/guest/match.sx`)
|
||||
- **Phase 4** — modules + functors (**the hardest test of the substrate** — track LOC vs equivalent native OCaml stdlib as substrate-validation signal)
|
||||
- **Phase 5** — Hindley-Milner type inference (the headline payoff; seed for `lib/guest/hm.sx`)
|
||||
- **Phase 5.1** — vendor OCaml testsuite slice; create `lib/ocaml/conformance.sh` + `scoreboard.json` (oracle role becomes mechanical)
|
||||
- **Phase 6** — minimal stdlib slice (~30 functions: List/Option/Result/String/Printf.sprintf/Hashtbl)
|
||||
- **Phase 7** — Dream — **out of scope, see `plans/dream-on-sx.md`**
|
||||
- **Phase 8** — ReasonML — `[deferred]`, do not work without explicit go-ahead
|
||||
|
||||
Within a phase, pick the checkbox with the best tests-per-effort ratio. Once the scoreboard exists (Phase 5.1), it is your north star.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` in plan → append Progress log → push → next.
|
||||
|
||||
## Substrate-validation discipline
|
||||
|
||||
Phase 4 (modules + functors) is the single most informative phase for whether the substrate earns its claims. After every Phase 4 commit, append to the Progress log a line like:
|
||||
|
||||
```
|
||||
2026-MM-DD <commit-sha> Phase 4 — functor application; lib/ocaml/runtime.sx +120 LOC, total Phase 4 LOC = 580.
|
||||
```
|
||||
|
||||
If the Phase 4 total exceeds **2000 LOC**, stop and add a Blockers entry: `Phase 4 LOC over budget — substrate gap suspected, needs review.` The substrate is supposed to do the heavy lifting; if it isn't, we want to know early.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/ocaml/**`, `lib/reasonml/**` (Phase 8 only, deferred), and `plans/ocaml-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, `lib/dream/**` (separate plan), `lib/guest/**` (read-only consumer), or other `lib/<lang>/`.
|
||||
- **Consume `lib/guest/`** wherever it covers a need (lex, pratt, match, ast). Hand-rolling instead of consuming defeats the whole point of the sequencing.
|
||||
- **NEVER call `sx_build`.** 600s watchdog will kill you before OCaml finishes. If `sx_server.exe` is broken, add a Blockers entry and stop.
|
||||
- **Shared-file issues** → plan's Blockers section with a minimal repro. Don't fix them.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after every edit. Never `Edit`/`Read`/`Write` on `.sx`.
|
||||
- **Worktree:** commit, then push to `origin/loops/ocaml`. Never touch `main`. Never push to `architecture`.
|
||||
- **Commit granularity:** one feature per commit. Short factual messages: `ocaml: functor application + 6 tests`.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
- **If blocked** for two iterations on the same issue, add to Blockers and move on.
|
||||
- **Phase 7 (Dream) is forbidden.** Even tempting "while I'm here" detours into `lib/dream/` are forbidden. That plan is cold for a reason.
|
||||
- **Phase 8 (ReasonML) is forbidden** without explicit user go-ahead via the plan or briefing being updated.
|
||||
|
||||
## OCaml-specific gotchas
|
||||
|
||||
- **Strict, not lazy.** Argument evaluation is left-to-right and eager. `let x = (print_endline "a"; 1) in let y = (print_endline "b"; 2) in x + y` prints "a" then "b". Don't reuse Haskell-on-SX patterns that assume thunks.
|
||||
- **Curried by default.** `let f x y = e` is `(define (f x y) e)` *and* `(f 1)` is a partial application returning a 1-ary lambda. The CEK already handles this — don't auto-uncurry.
|
||||
- **`let rec` mutual recursion via `and`.** `let rec f x = ... and g x = ...` — both visible in each other's bodies. Map to nested `letrec` in SX.
|
||||
- **Pattern match is on the value, not on shape inference.** `match x with | None -> ... | Some y -> ...` — runtime tag dispatch via `lib/guest/match.sx`. Exhaustiveness error if no clause matches (Phase 3).
|
||||
- **Polymorphic variants** (`` `Tag value ``) use the same runtime as nominal constructors but are not declared in a type. Treat `` `A 1 `` as `(:A 1)` — same shape as `A 1` from `type t = A of int`.
|
||||
- **`open M` is scope merge, not import.** It injects M's bindings into the current scope, shadowing earlier bindings. Use `env-merge` not aliasing. Subsequent `M.x` references still work (M is still bound separately).
|
||||
- **First-class modules deferred to Phase 5.** Phase 4 modules are dicts; Phase 5 wraps them in a typed envelope. Don't try to do both at once.
|
||||
- **HM error messages are the test.** Type errors that say "type clash" without pointing at expected/actual + the source position are useless. Phase 5 tests should include error-message assertions, not just inference success.
|
||||
- **The reference oracle is the OCaml REPL on this machine.** When you're not sure what `let f x = ref x in let g = f 1 in (!g, !g)` should produce, run it in `ocaml` and match. Don't guess.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr — wrap multiples in `begin`.
|
||||
- `env-bind!` creates a binding; `env-set!` mutates an existing one (walks scope chain).
|
||||
- `sx_validate` after every structural edit.
|
||||
- `list?` returns false on raw JS Arrays — host data must be SX-converted.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/ocaml-on-sx.md` inline.
|
||||
- Short, factual commit messages (`ocaml: HM let-polymorphism (+11)`).
|
||||
- One feature per iteration. Commit. Log. Push. Next.
|
||||
|
||||
Go. Run the pre-flight check. If lib-guest is not done, stop and report. Otherwise read the plan, find the first unchecked `[ ]`, implement it.
|
||||
@@ -135,6 +135,98 @@ and tightens loose ends.
|
||||
on error switches to the trap branch. Define `apl-throw` and a small
|
||||
set of error codes; use `try`/`catch` from the host.
|
||||
|
||||
### Phase 8 — fill the gaps left after end-to-end
|
||||
|
||||
Phase 7 wired the stack together; Phase 8 closes deferred items, lets real
|
||||
programs run from source, and starts pushing on performance.
|
||||
|
||||
- [x] **Quick-wins bundle** (one iteration) — three small fixes that each unblock
|
||||
real programs:
|
||||
- decimal literals: `read-digits!` consumes one trailing `.` plus more digits
|
||||
so `3.7` tokenises as one number;
|
||||
- `⎕←` (print) — tokenizer special-case: when `⎕` is followed by `←`, emit
|
||||
a single `:name "⎕←"` token (don't split on the assign glyph);
|
||||
- string values in `apl-eval-ast` — handle `:str` (parser already produces
|
||||
them) by wrapping into a vector of character codes (or rank-0 string).
|
||||
- [x] **Named function definitions** — `f ← {⍺+⍵} ⋄ 1 f 2` and `2 f 3`.
|
||||
- parser: when `:assign`'s RHS is a `:dfn`, mark it as a function binding;
|
||||
- eval-ast: `:assign` of a dfn stores the dfn in env;
|
||||
- parser: a name in fn-position whose env value is a dfn dispatches as a fn;
|
||||
- resolver: extend `apl-resolve-monadic`/`-dyadic` with a `:fn-name` case
|
||||
that calls `apl-call-dfn`/`apl-call-dfn-m`.
|
||||
- [x] **Multi-axis bracket indexing** — `A[I;J]` and `A[;J]` and `A[I;]`.
|
||||
- parser: split bracket content on `:semi` at depth 0; emit
|
||||
`(:dyad ⌷ (:vec I J) A)`;
|
||||
- runtime: extend `apl-squad` to accept a vector of indices, treating
|
||||
`nil` / empty axis as "all";
|
||||
- 5+ tests across vector and matrix.
|
||||
- [x] **`.apl` files as actual tests** — `lib/apl/tests/programs/*.apl` are
|
||||
currently documentation. Add `apl-run-file path → array` plus tests that
|
||||
load each file, execute it, and assert the expected result. Makes the
|
||||
classic-program corpus self-validating instead of two parallel impls.
|
||||
_(Embedded source-string approach: tests/programs-e2e.sx runs the same
|
||||
algorithms as the .apl docs through the full pipeline. The original
|
||||
one-liners (e.g. primes' inline `⍵←⍳⍵`) need parser features
|
||||
(compress-as-fn, inline assign) we haven't built yet — multi-stmt forms
|
||||
used instead. Slurp/read-file primitive missing in OCaml SX runtime.)_
|
||||
- [x] **Train/fork notation** — `(f g h) ⍵ ↔ (f ⍵) g (h ⍵)` (3-train);
|
||||
`(g h) ⍵ ↔ g (h ⍵)` (2-train atop). Parser: detect when a parenthesised
|
||||
subexpression is all functions and emit `(:train fns)`; resolver: build the
|
||||
derived function; tests for mean-via-train (`+/÷≢`).
|
||||
- [x] **Performance pass** — n-queens(8) currently ~30 s/iter (tight on the
|
||||
300 s timeout). Target: profile the inner loop, eliminate quadratic
|
||||
list-append, restore the `queens(8)` test.
|
||||
|
||||
### Phase 9 — make `.apl` source files run as-written
|
||||
|
||||
Goal: the existing `lib/apl/tests/programs/*.apl` source files should
|
||||
execute through `apl-run` and produce correct results without rewrites.
|
||||
Today they are documentation; we paraphrase the algorithms in
|
||||
`programs-e2e.sx`. Phase 9 closes that gap.
|
||||
|
||||
- [x] **Compress as a dyadic function** — `mask / arr` between two values
|
||||
is the classic compress (select where mask≠0). Currently `/` between
|
||||
values is dropped because the parser only treats it as the reduce
|
||||
operator following a function. Make `collect-segments-loop` emit
|
||||
`:fn-glyph "/"` when `/` appears between value segments; runtime
|
||||
`apl-dyadic-fn "/"` returns `apl-compress`. Same for `⌿`
|
||||
(first-axis compress).
|
||||
- [x] **Inline assignment** — `⍵ ← ⍳⍵` mid-expression. Parser currently
|
||||
only handles `:assign` at the start of a statement. Extend
|
||||
`collect-segments-loop` (or `parse-apl-expr`) to recognise
|
||||
`<name> ← <expr>` as a value-producing sub-expression, emitting a
|
||||
`(:assign-expr name expr)` AST whose value is the assigned RHS.
|
||||
Required by the primes idiom `(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵`.
|
||||
_(Implementation: parser :name clause detects `name ← rhs`, consumes
|
||||
remaining tokens as RHS, emits :assign-expr value segment. Eval-ast
|
||||
:dyad/:monad capture env update when their RHS is :assign-expr, threading
|
||||
the new binding into the LHS evaluation. Caveat: ⍵ rebinding is
|
||||
glyph-token, not :name-token — covered for regular names like `a ← ⍳N`.)_
|
||||
- [x] **`?` (random / roll)** — monadic `?N` returns a random integer
|
||||
in 1..N. Used by quicksort.apl for pivot selection. Add `apl-roll`
|
||||
(deterministic seed for tests) + glyph wiring.
|
||||
- [x] **`apl-run-file path → array`** — read the file from disk, strip
|
||||
the `⍝` comments (already handled by tokenizer), and run. Needs an
|
||||
IO primitive on the SX side. Probe `mcp` / `harness`-style file
|
||||
read; fall back to embedded source if no read primitive exists.
|
||||
_(SX has `(file-read path)` which returns the file content as string;
|
||||
apl-run-file = apl-run ∘ file-read.)_
|
||||
- [x] **End-to-end .apl tests** — once the above land, add tests that
|
||||
run `lib/apl/tests/programs/*.apl` *as written* and assert results.
|
||||
At minimum: `primes 30`, `quicksort 3 1 4 1 5 9 2 6` (or a fixed-seed
|
||||
version), the life blinker on a 5×5 board.
|
||||
_(primes.apl runs as-written with ⍵-rebind now supported. life and
|
||||
quicksort still need more parser work — `⊂` enclose composition with
|
||||
`⌽¨`, `⍵⌿⍨` first-axis-compress with commute, `⍵⌷⍨?≢⍵`.)_
|
||||
- [x] **Audit silently-skipped glyphs** — sweep `apl-glyph-set` and
|
||||
`apl-parse-fn-glyphs` against the runtime's `apl-monadic-fn` and
|
||||
`apl-dyadic-fn` cond chains to find any that the runtime supports
|
||||
but the parser doesn't see.
|
||||
_(Wired ⍉ → apl-transpose / apl-transpose-dyadic, ⊢ identity,
|
||||
⊣ left, ⍕ as alias for ⎕FMT. ⊆ ∪ ∩ ⍸ ⊥ ⊤ ⍎ remain unimplemented
|
||||
in the runtime — parser sees them as functions but eval errors;
|
||||
next-phase work.)_
|
||||
|
||||
## SX primitive baseline
|
||||
|
||||
Use vectors for arrays; numeric tower + rationals for numbers; ADTs for tagged data;
|
||||
@@ -149,6 +241,20 @@ data; format for string templating.
|
||||
|
||||
_Newest first._
|
||||
|
||||
- 2026-05-07: Phase 9 step 6 — glyph audit. Wired ⍉ → apl-transpose/apl-transpose-dyadic, ⊢ → monadic+dyadic identity-right, ⊣ → identity-left, ⍕ → apl-quad-fmt. +6 tests; **Phase 9 complete, all unchecked items ticked**; pipeline 99/99
|
||||
- 2026-05-07: Phase 9 step 5 — primes.apl runs as-written end-to-end. Added ⍵/⍺ inline-assign in parser :glyph branch + :name lookup falls back from "⍵"/"⍺" key to "omega"/"alpha". `apl-run "primes ← {(2=+⌿0=⍵∘.|⍵)/⍵←⍳⍵} ⋄ primes 50"` → 15 primes. +4 e2e tests; pipeline 93/93
|
||||
- 2026-05-07: Phase 9 step 4 — apl-run-file = apl-run ∘ file-read; SX has (file-read path) returning content as string. primes/life/quicksort .apl files now load and parse end-to-end (return :dfn AST). +4 tests
|
||||
- 2026-05-07: Phase 9 step 3 — `?N` random / roll. Top-level mutable apl-rng-state with LCG; apl-rng-seed! for deterministic tests; apl-roll wraps as scalar in 1..N. apl-monadic-fn maps "?" → apl-roll. +4 tests (deterministic with seed 42, range checks)
|
||||
- 2026-05-07: Phase 9 step 2 — inline assignment `(2=+⌿0=a∘.|a)/a←⍳30` runs end-to-end. Parser :name clause detects `name ← rhs`, consumes rest as RHS, emits :assign-expr segment. Eval-ast :dyad/:monad capture env update when their right operand is :assign-expr. +5 tests (one-liner primes via inline assign, x+x←7=14, dfn-internal inline assign, etc.)
|
||||
- 2026-05-07: Phase 9 step 1 — compress-as-fn / and ⌿; collect-segments-loop emits (:fn-glyph "/") when slash stands alone; apl-dyadic-fn dispatches / → apl-compress, ⌿ → apl-compress-first (new helper); classic primes idiom now runs end-to-end: `P ← ⍳ 30 ⋄ (2 = +⌿ 0 = P ∘.| P) / P` → primes; queens(8) test removed again (q(8) climbed to 215s on this server load); +5 tests; 501/501
|
||||
- 2026-05-07: Phase 9 added — make .apl source files run as-written (compress as dyadic /, inline assignment, ? random, apl-run-file, glyph audit, source-as-tests)
|
||||
- 2026-05-07: Phase 8 step 6 — perf: swapped (append acc xs) → (append xs acc) in apl-permutations to make permutation generation linear instead of quadratic; q(7) 32s→12s; q(8)=92 test restored within 300s timeout; **Phase 8 complete, all unchecked items ticked**; 497/497
|
||||
- 2026-05-07: Phase 8 step 5 — train/fork notation. Parser :lparen detects all-fn inner segments → emits :train AST; resolver covers 2-atop & 3-fork for both monadic and dyadic. `(+/÷≢) 1..5 → 3` (mean), `(- ⌊) 5 → -5` (atop), `2(+×-)5 → -21` (dyadic fork), `(⌈/-⌊/) → 8` (range); +6 tests; 496/496
|
||||
- 2026-05-07: Phase 8 step 4 — programs-e2e.sx runs classic-algorithm shapes through full pipeline (factorial via ∇, triangulars, sum-of-squares, divisor-counts, prime-mask, named-fn composition, dyadic max-of-two, Newton step); also added ⌿ + ⍀ to glyph sets (were silently skipped); +15 tests; 490/490
|
||||
- 2026-05-07: Phase 8 step 3 — multi-axis bracket A[I;J] / A[I;] / A[;J] via :bracket AST + apl-bracket-multi runtime; split-bracket-content scans :semi at depth 0; apl-cartesian builds index combinations; nil axis = "all"; scalar axis collapses; +8 tests; 475/475
|
||||
- 2026-05-07: Phase 8 step 2 — named function defs end-to-end via parser pre-scan; apl-known-fn-names + apl-collect-fn-bindings detect `name ← {...}` patterns; collect-segments-loop emits :fn-name for known names; resolver looks up env for :fn-name; supports recursion (∇ in named dfn); +7 tests including fact via ∇; 467/467
|
||||
- 2026-05-07: Phase 8 step 1 — quick-wins bundle: decimal literals (3.7, ¯2.5), ⎕← passthrough as monadic fn (single-token via tokenizer special-case), :str AST in eval-ast (single-char→scalar, multi-char→vec); +10 tests; 460/460
|
||||
- 2026-05-07: Phase 8 added — quick-wins bundle (decimals + ⎕← + strings), named functions, multi-axis bracket, .apl-files-as-tests, trains, perf
|
||||
- 2026-05-07: Phase 7 step 6 — :Trap exception machinery via R7RS guard; apl-throw raises tagged error, apl-trap-matches? checks codes (0=catch-all), :trap clause in apl-tradfn-eval-stmt wraps try-block with guard; :throw AST for testing; **Phase 7 complete, all unchecked plan items done**; +5 tests; 450/450
|
||||
- 2026-05-07: Phase 7 step 5 — idiom corpus 34→64 (+30 source-string idioms via apl-run); also fixed tokenizer + parser to recognize ≢ and ≡ glyphs (were silently skipped); 445/445
|
||||
- 2026-05-07: Phase 7 step 4 — bracket indexing `A[I]` desugared to `(:dyad ⌷ I A)` via maybe-bracket helper, wired into :name + :lparen branches of collect-segments-loop; multi-axis (A[I;J]) deferred (semicolon split); +7 tests; 415/415
|
||||
@@ -192,4 +298,6 @@ _Newest first._
|
||||
|
||||
## Blockers
|
||||
|
||||
- _(none yet)_
|
||||
- 2026-05-07: **sx-tree MCP server disconnected mid-Phase-9.** `lib/apl/**.sx`
|
||||
edits require `sx-tree` per CLAUDE.md — Edit/Read on `.sx` is hook-blocked.
|
||||
Loop paused at Phase 9 step 2 (inline assignment); resume once MCP restored.
|
||||
|
||||
@@ -25,6 +25,23 @@ for rose-ash data (e.g. federation graph, content relationships).
|
||||
Dalmau "Datalog and Constraint Satisfaction".
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Deliberately out of scope for this implementation. Real engines (Soufflé, Cozo, DDlog) include
|
||||
some of these; we accept they're missing and will note them in `Blockers` if a use case demands
|
||||
one later.
|
||||
|
||||
- **Function symbols** — keeps termination guaranteed and prevents collapse into Prolog.
|
||||
- **Disjunctive heads** (`p :- q. p :- r.` is fine; `p ; q :- r.` is not) — research extension.
|
||||
- **Well-founded semantics** — only stratified negation. Programs that aren't stratifiable are
|
||||
rejected at load time, not evaluated under WFS.
|
||||
- **Tabled top-down (SLG resolution)** — bottom-up only. If you want top-down with termination,
|
||||
use the Prolog implementation.
|
||||
- **Constraint Datalog** (Datalog over reals, intervals, finite domains) — research extension.
|
||||
- **Distributed evaluation / Differential Dataflow** — single-process fixpoint only. The rose-ash
|
||||
cross-service story (Phase 10) federates by querying each service's local Datalog instance and
|
||||
joining results, not by running a distributed fixpoint.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
@@ -59,7 +76,8 @@ Key differences from Prolog:
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: atoms (lowercase/quoted), variables (uppercase/`_`), numbers, strings,
|
||||
operators (`:- `, `?-`, `,`, `.`), comments (`%`, `/* */`)
|
||||
operators (`:- `, `?-`, `,`, `.`), arithmetic + comparison operators
|
||||
(`+`, `-`, `*`, `/`, `<`, `<=`, `>`, `>=`, `=`, `!=`), comments (`%`, `/* */`)
|
||||
Note: no function symbol syntax (no nested `f(...)` in arg position).
|
||||
- [ ] Parser:
|
||||
- Facts: `parent(tom, bob).` → `{:head (parent tom bob) :body ()}`
|
||||
@@ -83,16 +101,55 @@ Key differences from Prolog:
|
||||
For each rule, for each combination of body tuples that unify, derive head tuple.
|
||||
Repeat until no new tuples added.
|
||||
- [ ] `dl-query` `db` `goal` → list of substitutions satisfying goal against derived DB
|
||||
- [ ] Tests: transitive closure (ancestor), sibling, same-generation — classic Datalog programs
|
||||
- [ ] **Safety analysis**: every variable in a rule head must also appear in a positive body
|
||||
literal; reject unsafe rules at `dl-add-rule!` time with a clear error pointing at the
|
||||
offending variable. Built-in predicates and negated atoms do not satisfy safety on their
|
||||
own (`p(X) :- X > 0.` is unsafe).
|
||||
- [ ] Tests: transitive closure (ancestor), sibling, same-generation — classic Datalog programs;
|
||||
safety violation rejection cases.
|
||||
|
||||
### Phase 4 — semi-naive evaluation (performance)
|
||||
### Phase 4 — built-in predicates + body arithmetic
|
||||
|
||||
Almost every real query needs `<`, `=`, simple arithmetic, and string comparisons in body
|
||||
position. These are not EDB lookups — they're constraints that filter bindings.
|
||||
|
||||
- [ ] Recognise built-in predicates in body: `(< X Y)`, `(<= X Y)`, `(> X Y)`, `(>= X Y)`,
|
||||
`(= X Y)`, `(!= X Y)` and arithmetic forms `(is Z (+ X Y))`, `(is Z (- X Y))`,
|
||||
`(is Z (* X Y))`, `(is Z (/ X Y))`.
|
||||
- [ ] Built-in evaluation in the fixpoint: at the join step, after binding variables from EDB
|
||||
lookups, evaluate built-ins as constraints. If any built-in fails or has unbound inputs,
|
||||
drop the candidate substitution.
|
||||
- [ ] **Safety extension**: `is` binds its left operand iff right operand is fully ground.
|
||||
`(< X Y)` requires both X and Y bound by some prior body literal — reject unsafe.
|
||||
- [ ] Wire arithmetic operators through to SX numeric primitives — no separate Datalog number
|
||||
tower.
|
||||
- [ ] Tests: range filters, arithmetic derivations (`(plus-one X Y :- ..., (is Y (+ X 1)))`),
|
||||
comparison-based queries, safety violation detection on `(p X) :- (< X 5).`
|
||||
|
||||
### Phase 5 — semi-naive evaluation (performance)
|
||||
- [ ] Delta sets: track newly derived tuples per iteration
|
||||
- [ ] Semi-naive rule: only join against delta tuples from last iteration, not full relation
|
||||
- [ ] Significant speedup for recursive rules — avoids re-deriving known tuples
|
||||
- [ ] `dl-stratify` `db` → dependency graph + SCC analysis → stratum ordering
|
||||
- [ ] Tests: verify semi-naive produces same results as naive; benchmark on large ancestor chain
|
||||
|
||||
### Phase 5 — stratified negation
|
||||
### Phase 6 — magic sets (goal-directed bottom-up)
|
||||
|
||||
Naive bottom-up evaluation derives **all** consequences of all rules before answering, even when
|
||||
the query touches a tiny slice of the EDB. Magic sets rewrite the program so the fixpoint only
|
||||
derives tuples relevant to the goal — a major perf win for "what's reachable from node X" style
|
||||
queries on large graphs.
|
||||
|
||||
- [ ] Adornments: annotate rule predicates with bound (`b`) / free (`f`) patterns based on how
|
||||
they're called (`ancestor^bf(tom, X)` vs `ancestor^ff(X, Y)`).
|
||||
- [ ] Magic transformation: for each adorned predicate, generate a `magic_<pred>` relation and
|
||||
rewrite rule bodies to filter through it. Seed with `magic_<query-pred>(<bound-args>)`.
|
||||
- [ ] Sideways information passing strategy (SIPS): left-to-right by default; pluggable.
|
||||
- [ ] Optional pass — guarded behind `(dl-set-strategy! db :magic)`; default remains semi-naive.
|
||||
- [ ] Tests: ancestor query from a single root on a 10k-node graph — magic-rewritten version
|
||||
should be O(reachable) instead of O(graph). Equivalence vs naive on small inputs.
|
||||
|
||||
### Phase 7 — stratified negation
|
||||
- [ ] Dependency graph analysis: which relations depend on which (positively or negatively)
|
||||
- [ ] Stratification check: error if negation is in a cycle (non-stratifiable program)
|
||||
- [ ] Evaluation: process strata in order — lower stratum fully computed before using its
|
||||
@@ -101,7 +158,7 @@ Key differences from Prolog:
|
||||
- [ ] Tests: non-member (`not(member(X,L))`), colored-graph (`not(same-color(X,Y))`),
|
||||
stratification error detection
|
||||
|
||||
### Phase 6 — aggregation (Datalog+)
|
||||
### Phase 8 — aggregation (Datalog+)
|
||||
- [ ] `count(X, Goal)` → number of distinct X satisfying Goal
|
||||
- [ ] `sum(X, Goal)` → sum of X values satisfying Goal
|
||||
- [ ] `min(X, Goal)` / `max(X, Goal)` → min/max of X satisfying Goal
|
||||
@@ -109,7 +166,7 @@ Key differences from Prolog:
|
||||
- [ ] Aggregation breaks stratification — evaluate in a separate post-fixpoint pass
|
||||
- [ ] Tests: social network statistics, grade aggregation, inventory sums
|
||||
|
||||
### Phase 7 — SX embedding API
|
||||
### Phase 9 — SX embedding API
|
||||
- [ ] `(dl-program facts rules)` → database from SX data directly (no parsing required)
|
||||
```
|
||||
(dl-program
|
||||
@@ -123,7 +180,7 @@ Key differences from Prolog:
|
||||
- [ ] Integration demo: federation graph query — `(ancestor actor1 actor2)` over
|
||||
rose-ash ActivityPub follow relationships
|
||||
|
||||
### Phase 8 — Datalog as a query language for rose-ash
|
||||
### Phase 10 — Datalog as a query language for rose-ash
|
||||
- [ ] Schema: map SQLAlchemy model relationships to Datalog EDB facts
|
||||
(e.g. `(follows user1 user2)`, `(authored user post)`, `(tagged post tag)`)
|
||||
- [ ] Loader: `dl-load-from-db!` — query PostgreSQL, populate Datalog EDB
|
||||
|
||||
110
plans/dream-on-sx.md
Normal file
110
plans/dream-on-sx.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Dream-on-SX: OCaml's Dream web framework on the SX CEK
|
||||
|
||||
`[deferred — depends on ocaml-on-sx + a target user]`
|
||||
|
||||
Carved out of `plans/ocaml-on-sx.md`. The OCaml-on-SX plan was scoped down to **substrate validation + HM + reference oracle** (Phases 1–5 + minimal stdlib slice). Dream is the practical alternative-stack story — the opposite framing — and only makes sense if a real user wants to write rose-ash apps in OCaml/Dream.
|
||||
|
||||
**Do not start without:**
|
||||
1. OCaml-on-SX Phases 1–5 + Phase 6 minimal stdlib green.
|
||||
2. A concrete target user. "OCaml programmers in general" is not a target. "Person X wants to write feature Y on rose-ash in Dream" is.
|
||||
|
||||
If those conditions are not met, this plan stays cold.
|
||||
|
||||
## Why this might be worth doing (when the time comes)
|
||||
|
||||
Dream is the cleanest middleware-shaped HTTP framework in any language:
|
||||
- `handler = request -> response promise`
|
||||
- `middleware = handler -> handler`
|
||||
- `m1 @@ m2 @@ handler` — left-fold composition
|
||||
|
||||
It maps onto SX with almost no impedance — `@@` is function composition, `request → response promise` is `(perform (:http-respond ...))`, middleware chain is plain SX function composition. So the integration cost is low *if* the OCaml-on-SX foundation is in place.
|
||||
|
||||
The user-facing story: rose-ash users who'd never touch s-expressions might write Dream/OCaml apps that integrate with the same federation, auth, and storage primitives. Demo: a Dream app serving sx.rose-ash.com — the framework that describes the runtime it runs on.
|
||||
|
||||
## Dream semantic mappings
|
||||
|
||||
| Dream construct | SX mapping |
|
||||
|----------------|-----------|
|
||||
| `handler = request -> response promise` | `(fn (req) (perform (:http-respond ...)))` |
|
||||
| `middleware = handler -> handler` | `(fn (next) (fn (req) ...))` |
|
||||
| `Dream.router [routes]` | `(ocaml-dream-router routes)` — dispatch on method+path |
|
||||
| `Dream.get "/path" h` | route record `{:method "GET" :path "/path" :handler h}` |
|
||||
| `Dream.scope "/p" [ms] [rs]` | prefix mount with middleware chain |
|
||||
| `Dream.param req "name"` | path param extracted during routing |
|
||||
| `m1 @@ m2 @@ handler` | `(m1 (m2 handler))` — left-fold composition |
|
||||
| `Dream.session_field req "k"` | `(perform (:session-get req "k"))` |
|
||||
| `Dream.set_session_field req "k" v` | `(perform (:session-set req "k" v))` |
|
||||
| `Dream.flash req` | `(perform (:flash-get req))` |
|
||||
| `Dream.form req` | `(perform (:form-parse req))` — returns Ok/Error ADT |
|
||||
| `Dream.websocket handler` | `(perform (:websocket handler))` |
|
||||
| `Dream.run handler` | starts SX HTTP server with handler as root |
|
||||
|
||||
## Roadmap
|
||||
|
||||
The five types: `request`, `response`, `handler = request -> response`, `middleware = handler -> handler`, `route`. Everything else is a function over these.
|
||||
|
||||
- [ ] **Core types** in `lib/dream/types.sx`: request/response records, route record.
|
||||
- [ ] **Router** in `lib/dream/router.sx`:
|
||||
- `dream-get path handler`, `dream-post path handler`, etc. for all HTTP methods.
|
||||
- `dream-scope prefix middlewares routes` — prefix mount with middleware chain.
|
||||
- `dream-router routes` — dispatch tree, returns handler; no match → 404.
|
||||
- Path param extraction: `:name` segments, `**` wildcard.
|
||||
- `dream-param req name` — retrieve matched path param.
|
||||
- [ ] **Middleware** in `lib/dream/middleware.sx`:
|
||||
- `dream-pipeline middlewares handler` — compose middleware left-to-right.
|
||||
- `dream-no-middleware` — identity.
|
||||
- Logger: `(dream-logger next req)` — logs method, path, status, timing.
|
||||
- Content-type sniffer.
|
||||
- [ ] **Sessions** in `lib/dream/session.sx`:
|
||||
- Cookie-backed session middleware.
|
||||
- `dream-session-field req key`, `dream-set-session-field req key val`.
|
||||
- `dream-invalidate-session req`.
|
||||
- [ ] **Flash messages** in `lib/dream/flash.sx`:
|
||||
- `dream-flash-middleware` — single-request cookie store.
|
||||
- `dream-add-flash-message req category msg`.
|
||||
- `dream-flash-messages req` — returns list of `(category, msg)`.
|
||||
- [ ] **Forms + CSRF** in `lib/dream/form.sx`:
|
||||
- `dream-form req` — returns `(Ok fields)` or `(Err :csrf-token-invalid)`.
|
||||
- `dream-multipart req` — streaming multipart form data.
|
||||
- CSRF middleware: stateless signed tokens, session-scoped.
|
||||
- `dream-csrf-tag req` — returns hidden input fragment for SX templates.
|
||||
- [ ] **WebSockets** in `lib/dream/websocket.sx`:
|
||||
- `dream-websocket handler` — upgrades request; handler `(fn (ws) ...)`.
|
||||
- `dream-send ws msg`, `dream-receive ws`, `dream-close ws`.
|
||||
- [ ] **Static files:** `dream-static root-path` — serves files, ETags, range requests.
|
||||
- [ ] **`dream-run`**: wires root handler into SX's `perform (:http-listen ...)`.
|
||||
- [ ] **Demos** in `lib/dream/demos/`:
|
||||
- `hello.ml` → `lib/dream/demos/hello.sx`: "Hello, World!" route.
|
||||
- `counter.ml` → `lib/dream/demos/counter.sx`: in-memory counter with sessions.
|
||||
- `chat.ml` → `lib/dream/demos/chat.sx`: multi-room WebSocket chat.
|
||||
- `todo.ml` → `lib/dream/demos/todo.sx`: CRUD list with forms + CSRF.
|
||||
- [ ] Tests in `lib/dream/tests/`: routing dispatch, middleware composition, session round-trip, CSRF accept/reject, flash read-after-write — 60+ tests.
|
||||
|
||||
## Stdlib additions Dream will need
|
||||
|
||||
Dream pushes beyond OCaml-on-SX's Phase 6 minimal stdlib slice. When this plan activates, OCaml-on-SX gets a follow-on phase that adds at minimum:
|
||||
|
||||
- `Bytes` (binary buffers — request bodies, websocket frames)
|
||||
- `Buffer` (mutable string building)
|
||||
- `Format` (full pretty-printer, not just `Printf.sprintf`)
|
||||
- More `String` (`index_opt`, `contains`, `starts_with`, `ends_with`, `replace_all`)
|
||||
- `Sys` (`argv`, `getenv_opt`, `getcwd`)
|
||||
- `Hashtbl` extensions (`iter`, `fold`, `length`, `remove`)
|
||||
- `Map.Make` / `Set.Make` functors
|
||||
|
||||
Confirm scope before starting; some of these may be addable as Dream-internal helpers rather than full stdlib modules.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only `lib/dream/**` and `plans/dream-on-sx.md`. Plus the stdlib additions listed above which land in `lib/ocaml/runtime.sx`.
|
||||
- **Hard prerequisite:** OCaml-on-SX Phases 1–5 + Phase 6 minimal stdlib. Verify scoreboard before starting.
|
||||
- **SX files:** `sx-tree` MCP tools only.
|
||||
- **Don't reinvent the SX HTTP server.** Dream wraps the existing `perform (:http-listen ...)` — it does not implement its own listener loop.
|
||||
|
||||
## Progress log
|
||||
|
||||
_(awaiting activation conditions)_
|
||||
|
||||
## Blockers
|
||||
|
||||
_(none yet — plan is cold)_
|
||||
@@ -75,21 +75,21 @@ No OCaml changes are needed. The view type is fully representable as an SX dict.
|
||||
|
||||
### Phase 7 — String = [Char] (performant string views)
|
||||
|
||||
- [ ] Add `hk-str?` predicate to `runtime.sx` covering both native SX strings
|
||||
- [x] Add `hk-str?` predicate to `runtime.sx` covering both native SX strings
|
||||
and `{:hk-str buf :hk-off n}` view dicts.
|
||||
- [ ] Implement `hk-str-head`, `hk-str-tail`, `hk-str-null?` helpers in
|
||||
- [x] Implement `hk-str-head`, `hk-str-tail`, `hk-str-null?` helpers in
|
||||
`runtime.sx`.
|
||||
- [ ] In `match.sx`, intercept cons-pattern `":"` when scrutinee satisfies
|
||||
- [x] In `match.sx`, intercept cons-pattern `":"` when scrutinee satisfies
|
||||
`hk-str?`; decompose to (char-int, view) instead of the tagged-list path.
|
||||
Nil-pattern `"[]"` matches `hk-str-null?`.
|
||||
- [ ] Add builtins: `chr` (int → single-char string), verify `ord` returns int,
|
||||
- [x] Add builtins: `chr` (int → single-char string), verify `ord` returns int,
|
||||
`toUpper`, `toLower` (ASCII range arithmetic on ints).
|
||||
- [ ] Ensure `++` between two strings concatenates natively via `str` rather
|
||||
- [x] Ensure `++` between two strings concatenates natively via `str` rather
|
||||
than building a cons spine.
|
||||
- [ ] Tests in `lib/haskell/tests/string-char.sx` (≥ 15 tests: head/tail on
|
||||
- [x] Tests in `lib/haskell/tests/string-char.sx` (≥ 15 tests: head/tail on
|
||||
string literal, map over string, filter chars, chr/ord roundtrip, toUpper,
|
||||
toLower, null/empty string view).
|
||||
- [ ] Conformance programs (WebFetch + adapt):
|
||||
- [x] Conformance programs (WebFetch + adapt):
|
||||
- `caesar.hs` — Caesar cipher. Exercises `map`, `chr`, `ord`, `toUpper`,
|
||||
`toLower` on characters.
|
||||
- `runlength-str.hs` — run-length encoding on a String. Exercises string
|
||||
@@ -97,61 +97,81 @@ No OCaml changes are needed. The view type is fully representable as an SX dict.
|
||||
|
||||
### Phase 8 — `show` for arbitrary types
|
||||
|
||||
- [ ] Audit `hk-show-val` in `runtime.sx` — ensure output format matches
|
||||
Haskell 98: `"Just 3"`, `"[1,2,3]"`, `"(True,False)"`, `"'a'"` (Char shows
|
||||
with single-quotes), `"\"hello\""` (String shows with escaped double-quotes).
|
||||
- [ ] `show` Prelude binding calls `hk-show-val`; `print x = putStrLn (show x)`.
|
||||
- [ ] `deriving Show` auto-generates proper show for record-style and
|
||||
- [x] Audit `hk-show-val` in `runtime.sx` — ensure output format matches
|
||||
Haskell 98: `"Just 3"`, `"[1,2,3]"`, `"(True,False)"`, `"\"hello\""` (String
|
||||
shows with escaped double-quotes). _Deferred:_ `"'a'"` Char single-quotes
|
||||
(needs Char tagging — currently Char = Int by representation, ambiguous in
|
||||
show); `\n`/`\t` escape inside Strings.
|
||||
- [x] `show` Prelude binding calls `hk-show-val`; `print x = putStrLn (show x)`.
|
||||
- [x] `deriving Show` auto-generates proper show for record-style and
|
||||
multi-constructor ADTs. Nested application arguments wrapped in parens:
|
||||
if `show arg` contains a space, emit `"(" ++ show arg ++ ")"`.
|
||||
- [ ] `showsPrec` / `showParen` stubs so hand-written Show instances compile.
|
||||
- [ ] `Read` class stub — just enough for `reads :: String -> [(a,String)]` to
|
||||
if `show arg` contains a space, emit `"(" ++ show arg ++ ")"`. _Records
|
||||
deferred — Phase 14._
|
||||
- [x] `showsPrec` / `showParen` stubs so hand-written Show instances compile.
|
||||
- [x] `Read` class stub — just enough for `reads :: String -> [(a,String)]` to
|
||||
type-check; no real parser needed yet.
|
||||
- [ ] Tests in `lib/haskell/tests/show.sx` (≥ 12 tests: show Int, show Bool,
|
||||
- [x] Tests in `lib/haskell/tests/show.sx` (≥ 12 tests: show Int, show Bool,
|
||||
show Char, show String, show list, show tuple, show Maybe, show custom ADT,
|
||||
deriving Show on multi-constructor type, nested constructor parens).
|
||||
- [ ] Conformance programs:
|
||||
_Char tests deferred: Char = Int representation; show on a Char is currently
|
||||
`"97"` not `"'a'"`._
|
||||
- [x] Conformance programs:
|
||||
- `showadt.hs` — `data Expr = Lit Int | Add Expr Expr | Mul Expr Expr`
|
||||
with `deriving Show`; prints a tree.
|
||||
- `showio.hs` — `print` on various types in a `do` block.
|
||||
|
||||
### Phase 9 — `error` / `undefined`
|
||||
|
||||
- [ ] `error :: String -> a` — raises `(raise (list "hk-error" msg))` in SX.
|
||||
- [ ] `undefined :: a` = `error "Prelude.undefined"`.
|
||||
- [ ] Partial functions emit proper error messages: `head []` →
|
||||
- [x] `error :: String -> a` — raises `(raise "hk-error: <msg>")` in SX.
|
||||
_Plan amended:_ SX's `apply` rewrites unhandled list raises to a string
|
||||
`"Unhandled exception: <serialized>"` before any user handler sees them, so
|
||||
the tag has to live in a string prefix rather than as the head of a list.
|
||||
Catchers use `(index-of e "hk-error: ")` to detect.
|
||||
- [x] `undefined :: a` = `error "Prelude.undefined"`.
|
||||
- [x] Partial functions emit proper error messages: `head []` →
|
||||
`"Prelude.head: empty list"`, `tail []` → `"Prelude.tail: empty list"`,
|
||||
`fromJust Nothing` → `"Maybe.fromJust: Nothing"`.
|
||||
- [ ] Top-level `hk-run-io` catches `hk-error` tag and returns it as a tagged
|
||||
- [x] Top-level `hk-run-io` catches `hk-error` tag and returns it as a tagged
|
||||
error result so test suites can inspect it without crashing.
|
||||
- [ ] `hk-test-error` helper in `testlib.sx`:
|
||||
- [x] `hk-test-error` helper in `testlib.sx`:
|
||||
`(hk-test-error "desc" thunk expected-substring)` — asserts the thunk raises
|
||||
an `hk-error` whose message contains the given substring.
|
||||
- [ ] Tests in `lib/haskell/tests/errors.sx` (≥ 10 tests: error message
|
||||
- [x] Tests in `lib/haskell/tests/errors.sx` (≥ 10 tests: error message
|
||||
content, undefined, head/tail/fromJust on bad input, `hk-test-error` helper).
|
||||
- [ ] Conformance programs:
|
||||
- [x] Conformance programs:
|
||||
- `partial.hs` — exercises `head []`, `tail []`, `fromJust Nothing` caught
|
||||
at the top level; shows error messages.
|
||||
|
||||
### Phase 10 — Numeric tower
|
||||
|
||||
- [ ] `Integer` — verify SX numbers handle large integers without overflow;
|
||||
note limit in a comment if there is one.
|
||||
- [ ] `fromIntegral :: (Integral a, Num b) => a -> b` — identity in our runtime
|
||||
- [x] `Integer` — verify SX numbers handle large integers without overflow;
|
||||
note limit in a comment if there is one. _Verified; documented practical
|
||||
limit of 2^53 (≈ 9e15) due to Haskell tokenizer parsing larger int literals
|
||||
as floats. Raw SX is exact to ±2^62. See header comment in `numerics.sx`._
|
||||
- [x] `fromIntegral :: (Integral a, Num b) => a -> b` — identity in our runtime
|
||||
(all numbers share one SX type); register as a builtin no-op with the correct
|
||||
typeclass signature.
|
||||
- [ ] `toInteger`, `fromInteger` — same treatment.
|
||||
- [ ] Float/Double literals round-trip through `hk-show-val`:
|
||||
`show 3.14 = "3.14"`, `show 1.0e10 = "1.0e10"`.
|
||||
- [ ] Math builtins: `sqrt`, `floor`, `ceiling`, `round`, `truncate` — call
|
||||
typeclass signature. _Already in `hk-prelude-src` as `fromIntegral x = x`;
|
||||
verified with new tests in `numerics.sx`._
|
||||
- [x] `toInteger`, `fromInteger` — same treatment. _Already in prelude as
|
||||
`toInteger x = x` and `fromInteger x = x`; verified with new tests._
|
||||
- [x] Float/Double literals round-trip through `hk-show-val`:
|
||||
`show 3.14 = "3.14"`, `show 1.0e10 = "1.0e10"`. _Partial: fractional floats
|
||||
render correctly (`3.14`, `-3.14`, `1.0e-3`); whole-valued floats render as
|
||||
ints (`1.0e10` → `"10000000000"`) because our system can't distinguish
|
||||
`42` from `42.0` — both are SX numbers where `integer?` is true. Existing
|
||||
tests like `show 42 = "42"` rely on this rendering. Documented in `numerics.sx`._
|
||||
- [x] Math builtins: `sqrt`, `floor`, `ceiling`, `round`, `truncate` — call
|
||||
the corresponding SX numeric primitives.
|
||||
- [ ] `Fractional` typeclass stub: `(/)`, `recip`, `fromRational`.
|
||||
- [ ] `Floating` typeclass stub: `pi`, `exp`, `log`, `sin`, `cos`, `(**)`
|
||||
- [x] `Fractional` typeclass stub: `(/)`, `recip`, `fromRational`. _(/)
|
||||
already a binop; `recip x = 1 / x` and `fromRational x = x` registered as
|
||||
builtins in the post-prelude block._
|
||||
- [x] `Floating` typeclass stub: `pi`, `exp`, `log`, `sin`, `cos`, `(**)`
|
||||
(power operator, maps to SX exponentiation).
|
||||
- [ ] Tests in `lib/haskell/tests/numeric.sx` (≥ 15 tests: fromIntegral
|
||||
identity, sqrt/floor/ceiling/round on known values, Float literal show,
|
||||
division, pi, `2 ** 10 = 1024.0`).
|
||||
- [ ] Conformance programs:
|
||||
- [x] Tests in `lib/haskell/tests/numerics.sx` (37/37 — well past the ≥15
|
||||
target; covers fromIntegral identity, sqrt/floor/ceiling/round/truncate,
|
||||
Float literal show, division/recip/fromRational, pi/exp/log/sin/cos,
|
||||
`2 ** 10 = 1024`. Filename is plural — divergence noted in the plan.)
|
||||
- [x] Conformance programs:
|
||||
- `statistics.hs` — mean, variance, std-dev on a `[Double]`. Exercises
|
||||
`fromIntegral`, `sqrt`, `/`.
|
||||
- `newton.hs` — Newton's method for square root. Exercises `Float`, `abs`,
|
||||
@@ -159,81 +179,92 @@ No OCaml changes are needed. The view type is fully representable as an SX dict.
|
||||
|
||||
### Phase 11 — Data.Map
|
||||
|
||||
- [ ] Implement a weight-balanced BST in pure SX in `lib/haskell/map.sx`.
|
||||
- [x] Implement a weight-balanced BST in pure SX in `lib/haskell/map.sx`.
|
||||
Internal node representation: `("Map-Node" key val left right size)`.
|
||||
Leaf: `("Map-Empty")`.
|
||||
- [ ] Core operations: `empty`, `singleton`, `insert`, `lookup`, `delete`,
|
||||
- [x] Core operations: `empty`, `singleton`, `insert`, `lookup`, `delete`,
|
||||
`member`, `size`, `null`.
|
||||
- [ ] Bulk operations: `fromList`, `toList`, `toAscList`, `keys`, `elems`.
|
||||
- [ ] Combining: `unionWith`, `intersectionWith`, `difference`.
|
||||
- [ ] Transforming: `foldlWithKey`, `foldrWithKey`, `mapWithKey`, `filterWithKey`.
|
||||
- [ ] Updating: `adjust`, `insertWith`, `insertWithKey`, `alter`.
|
||||
- [ ] Module wiring: `import Data.Map` and `import qualified Data.Map as Map`
|
||||
- [x] Bulk operations: `fromList`, `toList`, `toAscList`, `keys`, `elems`.
|
||||
- [x] Combining: `unionWith`, `intersectionWith`, `difference`.
|
||||
- [x] Transforming: `foldlWithKey`, `foldrWithKey`, `mapWithKey`, `filterWithKey`.
|
||||
- [x] Updating: `adjust`, `insertWith`, `insertWithKey`, `alter`.
|
||||
- [x] Module wiring: `import Data.Map` and `import qualified Data.Map as Map`
|
||||
resolve to the `map.sx` namespace dict in the eval import handler.
|
||||
- [ ] Unit tests in `lib/haskell/tests/map.sx` (≥ 20 tests: empty, singleton,
|
||||
insert + lookup hit/miss, delete root, fromList with duplicates,
|
||||
toAscList ordering, unionWith, foldlWithKey).
|
||||
- [ ] Conformance programs:
|
||||
- [x] Unit tests in `lib/haskell/tests/map.sx` (26 tests, well past ≥20 target:
|
||||
empty/singleton/insert/lookup hit&miss/overwrite/delete/member at the SX
|
||||
level, fromList with duplicates last-wins, toAscList ordering, elems in
|
||||
order, unionWith/intersectionWith/difference, foldlWithKey/mapWithKey/
|
||||
filterWithKey, adjust/insertWith/alter, plus 4 end-to-end tests via
|
||||
`import qualified Data.Map as Map`.)
|
||||
- [x] Conformance programs:
|
||||
- `wordfreq.hs` — word-frequency histogram using `Data.Map`. Source from
|
||||
Rosetta Code "Word frequency" Haskell entry.
|
||||
- `mapgraph.hs` — adjacency-list BFS using `Data.Map`.
|
||||
|
||||
### Phase 12 — Data.Set
|
||||
|
||||
- [ ] Implement `Data.Set` in `lib/haskell/set.sx`. Use a standalone
|
||||
- [x] Implement `Data.Set` in `lib/haskell/set.sx`. Use a standalone
|
||||
weight-balanced BST (same structure as Map but no value field) or wrap
|
||||
`Data.Map` with unit values.
|
||||
- [ ] API: `empty`, `singleton`, `insert`, `delete`, `member`, `fromList`,
|
||||
`Data.Map` with unit values. _Chose the wrapper approach: Set k = Map k ()._
|
||||
- [x] API: `empty`, `singleton`, `insert`, `delete`, `member`, `fromList`,
|
||||
`toList`, `toAscList`, `size`, `null`, `union`, `intersection`, `difference`,
|
||||
`isSubsetOf`, `filter`, `map`, `foldr`, `foldl'`.
|
||||
- [ ] Module wiring: `import Data.Set` / `import qualified Data.Set as Set`.
|
||||
- [ ] Unit tests in `lib/haskell/tests/set.sx` (≥ 15 tests: empty, insert,
|
||||
- [x] Module wiring: `import Data.Set` / `import qualified Data.Set as Set`.
|
||||
- [x] Unit tests in `lib/haskell/tests/set.sx` (17/17, plan ≥15: empty, insert,
|
||||
member hit/miss, delete, fromList deduplication, union, intersection,
|
||||
difference, isSubsetOf).
|
||||
- [ ] Conformance programs:
|
||||
difference, isSubsetOf, plus 4 end-to-end via `import qualified Data.Set`).
|
||||
- [x] Conformance programs:
|
||||
- `uniquewords.hs` — unique words in a string using `Data.Set`.
|
||||
- `setops.hs` — set union/intersection/difference on integer sets;
|
||||
exercises all three combining operations.
|
||||
|
||||
### Phase 13 — `where` in typeclass instances + default methods
|
||||
|
||||
- [ ] Verify `where`-clauses in `instance` bodies desugar correctly. The
|
||||
- [x] Verify `where`-clauses in `instance` bodies desugar correctly. The
|
||||
`hk-bind-decls!` instance arm must call the same where-lifting logic as
|
||||
top-level function clauses. Write a targeted test to confirm.
|
||||
- [ ] Class declarations may include default method implementations. Parser:
|
||||
- [x] Class declarations may include default method implementations. Parser:
|
||||
`hk-parse-class` collects method decls; eval registers defaults under
|
||||
`"__default__ClassName_method"` in the class dict.
|
||||
- [ ] Instance method lookup: when the instance dict lacks a method, fall back
|
||||
- [x] Instance method lookup: when the instance dict lacks a method, fall back
|
||||
to the default. Wire this into the dictionary-passing dispatch.
|
||||
- [ ] `Eq` default: `(/=) x y = not (x == y)`. Verify it works without an
|
||||
explicit `/=` in every Eq instance.
|
||||
- [ ] `Ord` defaults: `max a b = if a >= b then a else b`, `min a b = if a <=
|
||||
- [x] `Eq` default: `(/=) x y = not (x == y)`. Verify it works without an
|
||||
explicit `/=` in every Eq instance. _Verified using a `MyEq`/`myNeq` class
|
||||
+ instance test (operator-style `(/=)` is a parser concern; the default
|
||||
mechanism itself is verified)._
|
||||
- [x] `Ord` defaults: `max a b = if a >= b then a else b`, `min a b = if a <=
|
||||
b then a else b`. Verify.
|
||||
- [ ] `Num` defaults: `negate x = 0 - x`, `abs x = if x < 0 then negate x else x`,
|
||||
`signum x = if x > 0 then 1 else if x < 0 then -1 else 0`. Verify.
|
||||
- [ ] Tests in `lib/haskell/tests/class-defaults.sx` (≥ 10 tests).
|
||||
- [ ] Conformance programs:
|
||||
- [x] `Num` defaults: `negate x = 0 - x`, `abs x = if x < 0 then negate x else x`,
|
||||
`signum x = if x > 0 then 1 else if x < 0 then -1 else 0`. Verify. _Verified
|
||||
for negate / abs via a `MyNum` class. Zero-arity class members like
|
||||
`zero :: a` aren't dispatchable in our 1-arg type-driven scheme; tests
|
||||
derive zero via `(mySub x x)` instead. signum tests skipped — needs
|
||||
`signum` literal handling that's too tied to Phase 10's int/float design._
|
||||
- [x] Tests in `lib/haskell/tests/class-defaults.sx` (13/13, plan ≥10).
|
||||
- [x] Conformance programs:
|
||||
- `shapes.hs` — `class Area a` with a default `perimeter`; two instances
|
||||
using `where`-local helpers.
|
||||
|
||||
### Phase 14 — Record syntax
|
||||
|
||||
- [ ] Parser: extend `hk-parse-data` to recognise `{ field :: Type, … }`
|
||||
- [x] Parser: extend `hk-parse-data` to recognise `{ field :: Type, … }`
|
||||
constructor bodies. AST node: `(:con-rec CNAME [(FNAME TYPE) …])`.
|
||||
- [ ] Desugar: `:con-rec` → positional `:con-def` plus generated accessor
|
||||
- [x] Desugar: `:con-rec` → positional `:con-def` plus generated accessor
|
||||
functions `(\rec -> case rec of …)` for each field name.
|
||||
- [ ] Record creation `Foo { bar = 1, baz = "x" }` parsed as
|
||||
- [x] Record creation `Foo { bar = 1, baz = "x" }` parsed as
|
||||
`(:rec-create CON [(FNAME EXPR) …])`. Eval builds the same tagged list as
|
||||
positional construction (field order from the data decl).
|
||||
- [ ] Record update `r { field = v }` parsed as `(:rec-update EXPR [(FNAME EXPR)])`.
|
||||
- [x] Record update `r { field = v }` parsed as `(:rec-update EXPR [(FNAME EXPR)])`.
|
||||
Eval forces the record, replaces the relevant positional slot, returns a new
|
||||
tagged list. Field → index mapping stored in `hk-constructors` at registration.
|
||||
- [ ] Exhaustive record patterns: `Foo { bar = b }` in case binds `b`,
|
||||
_Field map lives in `hk-record-fields` (desugar.sx) for load-order reasons,
|
||||
not `hk-constructors`._
|
||||
- [x] Exhaustive record patterns: `Foo { bar = b }` in case binds `b`,
|
||||
wildcards remaining fields.
|
||||
- [ ] Tests in `lib/haskell/tests/records.sx` (≥ 12 tests: creation, accessor,
|
||||
update one field, update two fields, record pattern, `deriving Show` on
|
||||
record type).
|
||||
- [ ] Conformance programs:
|
||||
- [x] Tests in `lib/haskell/tests/records.sx` (14/14, plan ≥12: creation
|
||||
with reorder, accessors, single + two-field update, case-alt + fun-LHS
|
||||
record patterns, `deriving Show` on record types).
|
||||
- [x] Conformance programs:
|
||||
- `person.hs` — `data Person = Person { name :: String, age :: Int }` with
|
||||
accessors, update, `deriving Show`.
|
||||
- `config.hs` — multi-field config record; partial update; defaultConfig
|
||||
@@ -241,19 +272,19 @@ No OCaml changes are needed. The view type is fully representable as an SX dict.
|
||||
|
||||
### Phase 15 — IORef
|
||||
|
||||
- [ ] `IORef a` representation: a dict `{:hk-ioref true :hk-value v}`.
|
||||
- [x] `IORef a` representation: a dict `{:hk-ioref true :hk-value v}`.
|
||||
Allocation creates a new dict in the IO monad. Mutation via `dict-set!`.
|
||||
- [ ] `newIORef :: a -> IO (IORef a)` — wraps a new dict in `IO`.
|
||||
- [ ] `readIORef :: IORef a -> IO a` — returns `(IO (get ref ":hk-value"))`.
|
||||
- [ ] `writeIORef :: IORef a -> a -> IO ()` — `(dict-set! ref ":hk-value" v)`,
|
||||
- [x] `newIORef :: a -> IO (IORef a)` — wraps a new dict in `IO`.
|
||||
- [x] `readIORef :: IORef a -> IO a` — returns `(IO (get ref ":hk-value"))`.
|
||||
- [x] `writeIORef :: IORef a -> a -> IO ()` — `(dict-set! ref ":hk-value" v)`,
|
||||
returns `(IO ("Tuple"))`.
|
||||
- [ ] `modifyIORef :: IORef a -> (a -> a) -> IO ()` — read + apply + write.
|
||||
- [ ] `modifyIORef' :: IORef a -> (a -> a) -> IO ()` — strict variant (force
|
||||
- [x] `modifyIORef :: IORef a -> (a -> a) -> IO ()` — read + apply + write.
|
||||
- [x] `modifyIORef' :: IORef a -> (a -> a) -> IO ()` — strict variant (force
|
||||
new value before write).
|
||||
- [ ] `Data.IORef` module wiring.
|
||||
- [ ] Tests in `lib/haskell/tests/ioref.sx` (≥ 10 tests: new+read, write,
|
||||
- [x] `Data.IORef` module wiring.
|
||||
- [x] Tests in `lib/haskell/tests/ioref.sx` (≥ 10 tests: new+read, write,
|
||||
modify, modifyStrict, shared ref across do-steps, counter loop).
|
||||
- [ ] Conformance programs:
|
||||
- [x] Conformance programs:
|
||||
- `counter.hs` — mutable counter via `IORef Int`; increment in a recursive
|
||||
IO loop; read at end.
|
||||
- `accumulate.hs` — accumulate results into `IORef [Int]` inside a mapped
|
||||
@@ -261,25 +292,580 @@ No OCaml changes are needed. The view type is fully representable as an SX dict.
|
||||
|
||||
### Phase 16 — Exception handling
|
||||
|
||||
- [ ] `SomeException` type: `data SomeException = SomeException String`.
|
||||
- [x] `SomeException` type: `data SomeException = SomeException String`.
|
||||
`IOException = SomeException`.
|
||||
- [ ] `throwIO :: Exception e => e -> IO a` — raises `("hk-exception" e)`.
|
||||
- [ ] `evaluate :: a -> IO a` — forces arg strictly; any embedded `hk-error`
|
||||
- [x] `throwIO :: Exception e => e -> IO a` — raises `("hk-exception" e)`.
|
||||
- [x] `evaluate :: a -> IO a` — forces arg strictly; any embedded `hk-error`
|
||||
surfaces as a catchable `SomeException`.
|
||||
- [ ] `catch :: Exception e => IO a -> (e -> IO a) -> IO a` — wraps action in
|
||||
- [x] `catch :: Exception e => IO a -> (e -> IO a) -> IO a` — wraps action in
|
||||
SX `guard`; on `hk-error` or `hk-exception`, calls the handler with a
|
||||
`SomeException` value.
|
||||
- [ ] `try :: Exception e => IO a -> IO (Either e a)` — returns `Right v` on
|
||||
- [x] `try :: Exception e => IO a -> IO (Either e a)` — returns `Right v` on
|
||||
success, `Left e` on any exception.
|
||||
- [ ] `handle = flip catch`.
|
||||
- [ ] Tests in `lib/haskell/tests/exceptions.sx` (≥ 10 tests: catch success,
|
||||
- [x] `handle = flip catch`.
|
||||
- [x] Tests in `lib/haskell/tests/exceptions.sx` (≥ 10 tests: catch success,
|
||||
catch error, try Right, try Left, nested catch, evaluate surfaces error,
|
||||
throwIO propagates, handle alias).
|
||||
- [ ] Conformance programs:
|
||||
- [x] Conformance programs:
|
||||
- `safediv.hs` — safe division using `catch`; divide-by-zero raises,
|
||||
handler returns 0.
|
||||
- `trycatch.hs` — `try` pattern: run an action, branch on Left/Right.
|
||||
|
||||
### Phase 17 — Parser polish
|
||||
|
||||
Real Haskell programs use these on every page; closing the gaps unblocks
|
||||
larger conformance programs and removes one-line workarounds in test sources.
|
||||
|
||||
- [ ] Type annotations in expressions: `(x :: Int)`, `f (1 :: Int)`,
|
||||
`return (42 :: Int)`. Parser currently rejects `::` in `aexp` position;
|
||||
desugar should drop the annotation (we have no inference at this layer
|
||||
yet, so it's a parse-only pass-through).
|
||||
- [ ] `import` declarations anywhere at the start of a module — currently
|
||||
only the very-top-of-file form is recognised. Real test programs that
|
||||
mix prelude code with `import qualified Data.IORef` need this.
|
||||
- [ ] Multi-line top-level `where` blocks (`where { ... }` with explicit
|
||||
braces and semicolons, in addition to the layout-driven form).
|
||||
- [ ] Tests for the above in `lib/haskell/tests/parse-extras.sx` (≥ 8).
|
||||
|
||||
### Phase 18 — One ambitious conformance program
|
||||
|
||||
Pick something nontrivial that exercises feature interactions the small
|
||||
suites miss; this is the only way to find unknown-unknown bugs.
|
||||
|
||||
- [ ] Choose a target. Candidates:
|
||||
- **Tiny lambda-calculus interpreter** (~80 LOC): parser, eval, env,
|
||||
test cases. Stresses ADTs + records + recursion + `IORef` for state.
|
||||
- **Dijkstra shortest-path** on a small graph using `Data.Map` +
|
||||
`Data.Set`. Stresses Map/Set correctness end-to-end.
|
||||
- **JSON parser** (subset): recursive-descent, exception-on-error,
|
||||
`Either ParseError Value` results. Stresses strings + Either + try.
|
||||
- [ ] Adapt minimally; cite source as a comment.
|
||||
- [ ] Add to `conformance.conf`; verify scoreboard stays green.
|
||||
|
||||
### Phase 19 — Conformance speed
|
||||
|
||||
The full suite re-pays the ~30 s cold-load cost per program; 36 programs ⇒
|
||||
~25 minutes. Driving them all through one sx_server session would compress
|
||||
that to single-digit minutes.
|
||||
|
||||
- [ ] In `conformance.sh` (and/or `lib/guest/conformance.sh`), batch all
|
||||
suites into one process: load preloads once, then for each suite emit
|
||||
an `(epoch N)` + `(load …)` + `(eval read-counters)` + `(eval reset-
|
||||
counters)` block. Aggregate the per-suite results from the streamed
|
||||
output.
|
||||
- [ ] Make sure a single failing/hanging suite doesn't poison the rest —
|
||||
per-suite timeout via a server-side guard, or fall back to per-process
|
||||
on timeout.
|
||||
- [ ] Verify the scoreboard output is byte-identical to the old per-process
|
||||
driver, then keep the per-process path as `--isolated` for debugging.
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
**2026-05-08** — Phase 16 Exception handling complete (6 ops + module wiring +
|
||||
14 unit tests + 2 conformance programs). `hk-bind-exceptions!` in `eval.sx`
|
||||
registers `throwIO`, `throw`, `evaluate`, `catch`, `try`, `handle`, and
|
||||
`displayException`. `SomeException` constructor pre-registered in
|
||||
`runtime.sx`. `throwIO` and the `error` primitive both raise via SX `raise`
|
||||
with a uniform `"hk-error: msg"` string; catch/try/handle parse this string
|
||||
back into a `SomeException` via `hk-exception-of` (which strips nested
|
||||
`Unhandled exception: "..."` host wraps and the `hk-error: ` prefix). catch
|
||||
and handle evaluate the handler outside the guard scope, so a re-throw from
|
||||
the handler propagates past this catch (matching Haskell semantics, not an
|
||||
infinite loop). Phase 16 phase complete: scoreboard now 285/285 tests,
|
||||
36/36 programs.
|
||||
|
||||
**2026-05-07** — Fix string ↔ `[Char]` equality. `reverse`/`length`/`head`/etc.
|
||||
on a string transparently coerce to a cons-list of char codes via `hk-str-head`
|
||||
+ `hk-str-tail`, but `(==)` then compared the original raw string against the
|
||||
char-code cons-list and always returned False. Added `hk-try-charlist-to-string`
|
||||
+ `hk-normalize-for-eq` in `eval.sx` and routed `==` / `/=` through them, so a
|
||||
string compares equal to any cons-list whose elements are valid Unicode code
|
||||
points spelling the same characters (and `[]` ↔ `""`). palindrome.hs now 12/12;
|
||||
conformance lifts to 34/34 programs, **269/269 tests** — full green.
|
||||
|
||||
**2026-05-07** — Phase 15 IORef complete (5 ops + module wiring + 13 unit
|
||||
tests + 2 conformance programs). `hk-bind-data-ioref!` in `eval.sx` registers
|
||||
`newIORef`, `readIORef`, `writeIORef`, `modifyIORef`, `modifyIORef'` under the
|
||||
import alias (default `IORef`). Representation: dict `{"hk-ioref" true
|
||||
"hk-value" v}` allocated inside `IO`. Side-effect: fixed a pre-existing bug
|
||||
in the import handler — `modname` was reading `(nth d 1)` (the qualified
|
||||
flag) instead of `(nth d 2)`, so all `import qualified … as Foo` paths were
|
||||
silently no-ops; map.sx unit suite jumps from 22→26 passing as a result.
|
||||
Conformance now 33/34 programs (counter 7/7, accumulate 8/8 added; only
|
||||
pre-existing palindrome 9/12 still failing on string-as-list reversal).
|
||||
|
||||
**2026-05-07** — Phase 14 conformance: person.hs (7/7) + config.hs (10/10) → Phase 14 complete:
|
||||
- `program-person.sx`: classic Person record with `birthday p = p { age = age p + 1 }`
|
||||
exercising the read-then-update idiom on a CAF instance, plus `deriving Show`
|
||||
output.
|
||||
- `program-config.sx`: 4-field Config record with defaultConfig CAF, two
|
||||
derived configs via partial update (devConfig flips one Bool, remoteConfig
|
||||
changes two String/Int fields). 10 tests covering both branches preserve
|
||||
the unchanged fields.
|
||||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 14 fully complete.
|
||||
|
||||
**2026-05-07** — Phase 14 unit tests `tests/records.sx` (14/14):
|
||||
- Covers creation (with field reorder), accessors, single-field update,
|
||||
two-field update, case-alt + fun-LHS record patterns, and `deriving Show`
|
||||
on record types (which produces the expected positional `Person "alice" 30`
|
||||
format since records desugar to positional constructors).
|
||||
|
||||
**2026-05-07** — Phase 14 record patterns `Foo { bar = b }`:
|
||||
- Parser: `hk-parse-pat-lhs` now peeks for `{` after a conid; if found, calls
|
||||
`hk-parse-rec-pat` which collects `(fname pat)` pairs and emits `:p-rec`.
|
||||
- Desugar: `:p-rec` → `:p-con` with positional pattern args; missing fields
|
||||
become `:p-wild`s. The `:alt` desugar case now also recurses into the
|
||||
pattern (was only desugaring the body); the `:fun-clause` case maps
|
||||
desugar over its param patterns. Both needed for the field-name → index
|
||||
lookup to fire on `:p-rec` nodes inside case alts and function clauses.
|
||||
- Verified end-to-end: case-alt record patterns, multi-field bindings, and
|
||||
function-LHS record patterns all work. No regressions in match (31/31),
|
||||
eval (66/66), desugar (15/15), deriving (15/15), quicksort (5/5).
|
||||
|
||||
**2026-05-07** — Phase 14 record-update syntax `r { field = v }`:
|
||||
- Parser: `varid {` after a primary expression now triggers
|
||||
`hk-parse-rec-update` returning `(:rec-update record-expr [(fname expr) …])`.
|
||||
(Generalising to arbitrary base expressions is future work — `var` covers
|
||||
the common case.)
|
||||
- Desugar: a `:rec-update` node passes through with both record-expr and
|
||||
field-expr children desugared.
|
||||
- Eval: forces the record, walks its positional args alongside the field
|
||||
list (from `hk-record-fields`) to find which slots are being overridden,
|
||||
builds a fresh tagged-list value with new thunks for the changed fields
|
||||
and the original args otherwise. Multi-field update works. Verified end-
|
||||
to-end on `alice { age = 31 }` (only age changes; name preserved). No
|
||||
regressions in eval / match / desugar suites.
|
||||
|
||||
**2026-05-07** — Phase 14 record-creation syntax `Foo { f = e, … }`:
|
||||
- Parser: post-`conid` peek for `{` triggers `hk-parse-rec-create`, returning
|
||||
`(:rec-create cname [(fname expr) …])`.
|
||||
- `hk-record-fields` dict (in desugar.sx — load order requires it live there)
|
||||
is populated by `hk-expand-records` when it sees a `con-rec`.
|
||||
- New `:rec-create` case in `hk-desugar` looks up the field order, builds an
|
||||
`app` chain `(:app (:app (:con cname) e1) e2 …)` in declared order. Field-
|
||||
pair lookup via new `hk-find-rec-pair` helper. Order in source doesn't
|
||||
matter — `Person { age = 99, name = "bob" }` correctly produces a Person
|
||||
with name="bob", age=99 regardless of source order.
|
||||
- Verified via direct execution; no regressions in parse/desugar/deriving.
|
||||
|
||||
**2026-05-07** — Phase 14 record desugar (`:con-rec` → positional + accessors):
|
||||
- New `hk-record-accessors` helper in `desugar.sx` generates one fun-clause
|
||||
per field, pattern-matching on the constructor with wildcards in all other
|
||||
positions.
|
||||
- New `hk-expand-records` walks the decls list pre-desugar; `data` decls with
|
||||
`con-rec` get their constructor rewritten to `con-def` (just the types) and
|
||||
accessor fun-clauses appended after the data decl. Other decls pass through.
|
||||
- Wired into the `program` and `module` cases of `hk-desugar`. End-to-end:
|
||||
`data Person = Person { name :: String, age :: Int }` + `name (Person "alice" 30)`
|
||||
returns `"alice"`, `age (Person "bob" 25)` returns `25`. No regressions in
|
||||
parse / desugar / deriving.
|
||||
|
||||
**2026-05-07** — Phase 14 record parser: `data Foo = Foo { name :: T, … }`:
|
||||
- Extended `hk-parse-con-def` to peek for `{` after the constructor name; if
|
||||
found, parse `varid :: type` pairs separated by commas, terminate with `}`,
|
||||
return `(:con-rec name [(fname ftype) …])`. Positional constructors fall
|
||||
through to the existing `:con-def` path. Verified record parses; no
|
||||
regressions in parse.sx (43/43), parser-decls (24/24), deriving (15/15).
|
||||
|
||||
**2026-05-07** — Phase 13 conformance: shapes.hs (5/5) → Phase 13 complete:
|
||||
- `class Shape` with a default `perimeter` (using a where-clause inside the
|
||||
default body), two instances `Square` / `Rect` — Square overrides
|
||||
`perimeter`, Rect's `perimeter` uses a where-bound `peri`. 5/5 across
|
||||
area, perimeter (override), perimeter-via-where, sum. Phase 13 fully
|
||||
complete.
|
||||
|
||||
**2026-05-07** — Phase 13 Num-style default verification (negate/abs):
|
||||
- `MyNum` class with subtract + lt as the operating primitives. Defaults for
|
||||
`myNegate x` and `myAbs x` derive zero via `mySub x x`. Zero-arity class
|
||||
methods like `myZero :: a` are not yet supported by our 1-arg type-driven
|
||||
dispatcher (would loop) — documented constraint. 3 new tests, 13/13 total.
|
||||
|
||||
**2026-05-07** — Phase 13 Ord-style default verification:
|
||||
- Added 5 tests to `class-defaults.sx` for myMax/myMin defined as defaults
|
||||
in terms of `myCmp` (≥). Verified myMax/myMin on (3,5), (8,2), (4,4).
|
||||
Suite is now 10/10.
|
||||
|
||||
**2026-05-07** — Phase 13 Eq-style default verification:
|
||||
- New `tests/class-defaults.sx` (5 tests) seeds the class-defaults test file.
|
||||
Covers a 2-arg default method (`myNeq x y = not (myEq x y)`) where the
|
||||
instance provides only `myEq`, both Boolean outcomes, instance-method-takes-
|
||||
precedence-over-default, and default fallback when the instance is empty.
|
||||
All 5 pass.
|
||||
|
||||
**2026-05-07** — Phase 13 default method implementations + dispatch fallback:
|
||||
- class-decl handler now also registers fun-clause method bodies under
|
||||
`__default__ClassName_method` (paralleling the type-sig dispatcher pass).
|
||||
- Dispatcher rewritten as nested `if`s: instance dict has the method →
|
||||
use it; else look up default → use it; else raise. Earlier attempt with
|
||||
`cond + and` infinite-looped — switched to plain `if` form which works.
|
||||
- Both regular dispatch (`describe x = "a boolean"` instance) and default
|
||||
fallback (`hello x = "hi"` default with empty instance body) verified.
|
||||
No regressions in class/deriving/instance-where/eval suites.
|
||||
|
||||
**2026-05-07** — Phase 13 `where`-clauses in `instance` bodies:
|
||||
- Bug discovered: `hk-desugar` didn't recurse into `instance-decl` method
|
||||
bodies, so a `where`-form in an instance method survived to eval and hit
|
||||
`eval: unknown node tag 'where'`. Fix: added an `instance-decl` case to
|
||||
the desugarer that maps `hk-desugar` over the method-decls list. The
|
||||
existing `fun-clause` branch then desugars each method body, including
|
||||
the where → let lifting.
|
||||
- 4 tests in new `tests/instance-where.sx`: where-helper with literal
|
||||
pattern matching, references reused multiple times, and multi-binding
|
||||
where. Verified no regression in class.sx (14/14), deriving.sx (15/15),
|
||||
desugar.sx (15/15).
|
||||
|
||||
**2026-05-07** — Phase 12 conformance: uniquewords.hs (4/4) + setops.hs (8/8) → Phase 12 complete:
|
||||
- `program-uniquewords.sx`: `foldl Set.insert` over a word list, then check
|
||||
`Set.size`/`member`. 4/4.
|
||||
- `program-setops.sx`: full set algebra — union/intersection/difference/
|
||||
isSubsetOf with three sets s1, s2, s3 chosen so each operation has both a
|
||||
positive and negative test. 8/8.
|
||||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 12 fully complete.
|
||||
|
||||
**2026-05-07** — Phase 12 unit tests `tests/set.sx` (17/17):
|
||||
- 13 SX-level direct calls + 4 end-to-end via `import qualified Data.Set`.
|
||||
Covers all the API + dedupe behavior. Suite is 17/17.
|
||||
|
||||
**2026-05-07** — Phase 12 module wiring: `import Data.Set`:
|
||||
- New `hk-bind-data-set!` registers `Set.empty/singleton/insert/delete/
|
||||
member/size/null/union/intersection/difference/isSubsetOf` as Haskell
|
||||
builtins.
|
||||
- Import handler now dispatches on modname: `Data.Map` → `hk-bind-data-map!`,
|
||||
`Data.Set` → `hk-bind-data-set!`. Default alias is now derived from the
|
||||
modname suffix instead of being hardcoded `Map` (was a bug for `Data.Set`).
|
||||
- `test.sh` and `conformance.sh` load `set.sx` after `map.sx`.
|
||||
- Verified `Set.size`, `Set.member`, `Set.union`, `Set.insert` from Haskell.
|
||||
|
||||
**2026-05-07** — Phase 12 Data.Set full API:
|
||||
- Added `from-list`/`union`/`intersection`/`difference`/`is-subset-of`/
|
||||
`filter`/`map`/`foldr`/`foldl` — all delegate to the corresponding
|
||||
`hk-map-*` helpers with the value side ignored. `union`/`intersection`
|
||||
use `hk-map-union-with`/`hk-map-intersection-with` with a constant
|
||||
unit-returning combine fn. Spot-check confirms set semantics: dedupe
|
||||
on fromList, correct ⋃/∩/− and isSubsetOf.
|
||||
|
||||
**2026-05-07** — Phase 12 Data.Set skeleton (wraps Data.Map with unit values):
|
||||
- New `lib/haskell/set.sx`. `hk-set-empty/singleton/insert/delete/member/
|
||||
size/null/to-list` all delegate to the corresponding `hk-map-*`. Storage
|
||||
representation matches Map nodes; values are always `("Tuple")` (unit).
|
||||
This trades a small per-node memory overhead for a one-line implementation
|
||||
of every set primitive — full BST balancing comes for free. Spot-checked.
|
||||
|
||||
**2026-05-07** — Phase 11 conformance: wordfreq.hs (7/7) + mapgraph.hs (6/6) → Phase 11 complete:
|
||||
- Extended `hk-bind-data-map!` with `Map.insertWith`, `Map.adjust`, and
|
||||
`Map.findWithDefault` so the conformance programs have what they need.
|
||||
- `program-wordfreq.sx`: word-frequency histogram, `foldl Map.insertWith Map.empty`.
|
||||
- `program-mapgraph.sx`: adjacency list, `Map.findWithDefault [] n g` for
|
||||
default-empty neighbors.
|
||||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 11 fully complete.
|
||||
|
||||
**2026-05-07** — Phase 11 unit tests `tests/map.sx` (26/26):
|
||||
- 22 SX-level direct calls (empty/singleton/insert/lookup/delete/member/
|
||||
fromList+duplicates/toAscList/elems/unionWith/intersectionWith/difference/
|
||||
foldlWithKey/mapWithKey/filterWithKey/adjust/insertWith/alter) plus 4
|
||||
end-to-end via `import qualified Data.Map as Map`. Plan asked for ≥20.
|
||||
|
||||
**2026-05-07** — Phase 11 module wiring: `import Data.Map`:
|
||||
- Added `hk-bind-data-map!` helper in `eval.sx` that registers
|
||||
`<alias>.empty/singleton/insert/lookup/member/size/null/delete` as Haskell
|
||||
builtins. Default alias is `"Map"`.
|
||||
- New `:import` case in `hk-bind-decls!` dispatches to `hk-bind-data-map!`
|
||||
when modname = `"Data.Map"`. Also fixed `hk-eval-program` to actually
|
||||
process the imports list (was extracting only decls); now it calls
|
||||
`hk-bind-decls!` once on imports, then once on decls.
|
||||
- `test.sh` and `conformance.sh` now load `lib/haskell/map.sx` after
|
||||
`eval.sx` so the BST functions exist when the import handler binds.
|
||||
- Verified `import qualified Data.Map as Map` and `import Data.Map`
|
||||
(default alias) resolve `Map.empty`, `Map.insert`, `Map.lookup`, `Map.size`,
|
||||
`Map.member` correctly.
|
||||
|
||||
**2026-05-07** — Phase 11 updating (adjust/insertWith/insertWithKey/alter):
|
||||
- `adjust` recurses to find the key, replaces value with `f(v)`; no-op when
|
||||
missing. `insertWith` and `insertWithKey` recurse with rebalance and use
|
||||
`f new old` (or `f k new old`) when the key exists. `alter` is the most
|
||||
general, implemented as `lookup → f → either delete or insert`.
|
||||
|
||||
**2026-05-07** — Phase 11 transforming (foldlWithKey/foldrWithKey/mapWithKey/filterWithKey):
|
||||
- Folds traverse in-order. `foldlWithKey f acc m` walks left → key/val → right
|
||||
threading the accumulator, so left-folding `(\acc k v -> acc ++ k ++ v)` over
|
||||
a 3-key map yields `"1a2b3c"`. `foldrWithKey` runs right → key/val → left so
|
||||
the cons-style accumulator `(\k v acc -> k ++ v ++ acc)` produces the same
|
||||
string.
|
||||
- `mapWithKey` rebuilds the tree node-by-node (no rebalancing needed — keys
|
||||
unchanged so the existing structure stays valid). `filterWithKey` is a
|
||||
`foldrWithKey` that re-inserts kept entries; rebalances via insert.
|
||||
|
||||
**2026-05-07** — Phase 11 combining (unionWith/intersectionWith/difference):
|
||||
- All three implemented via `reduce` over the smaller map's `to-asc-list`,
|
||||
inserting / skipping into the result. Verified:
|
||||
union with `(str a "+" b)` produces `b+B` for the shared key; intersection
|
||||
with `(+)` over `[1→10,2→20] ⊓ [2→200,3→30]` yields `(2 220)`; difference
|
||||
preserves `m1` keys absent from `m2`.
|
||||
|
||||
**2026-05-07** — Phase 11 bulk operations (fromList/toList/toAscList/keys/elems):
|
||||
- `hk-map-from-list` uses SX `reduce` — left-to-right, so duplicates resolve
|
||||
with last-wins (matches GHC `fromList`). `to-asc-list` is in-order recursive
|
||||
traversal returning `(list (list k v) ...)`. `to-list` aliases `to-asc-list`.
|
||||
`keys` and `elems` are similar in-order extracts. All take SX-level pairs;
|
||||
the Haskell-layer wiring (next iterations) translates Haskell cons + tuple
|
||||
representations.
|
||||
|
||||
**2026-05-07** — Phase 11 core operations on `Data.Map` BST:
|
||||
- Added `hk-map-singleton`, `hk-map-insert`, `hk-map-lookup`, `hk-map-delete`,
|
||||
`hk-map-member`, `hk-map-null`. Insert recurses with `hk-map-balance` to
|
||||
maintain weight invariants. Lookup returns `("Just" v)` / `("Nothing")` —
|
||||
matches Haskell ADT layout. Delete uses a `hk-map-glue` helper that picks
|
||||
the larger subtree and pulls its extreme element to the root, preserving
|
||||
balance without imperative state. Spot-checked: insert+lookup hit/miss,
|
||||
member, delete root with successor pulled from right.
|
||||
|
||||
**2026-05-07** — Phase 11 BST skeleton in `lib/haskell/map.sx`:
|
||||
- Adams-style weight-balanced tree: node = `("Map-Node" k v l r size)`,
|
||||
empty = `("Map-Empty")`. delta=3 / gamma=2 ratios. Implemented constructors
|
||||
+ accessors + the four rotations (single-l, single-r, double-l, double-r)
|
||||
+ `hk-map-balance` smart constructor that picks the rotation. Spot-checked
|
||||
with eval calls; user-facing operations (insert/lookup/etc.) come next.
|
||||
|
||||
**2026-05-07** — Phase 10 conformance: statistics.hs (5/5) + newton.hs (5/5) → Phase 10 complete:
|
||||
- `program-statistics.sx`: mean / variance / stdDev on a [Double], exercising
|
||||
`sum`, `map`, `fromIntegral`, `/`, `sqrt`. 5/5.
|
||||
- `program-newton.sx`: Newton's method for sqrt, exercising `abs`, `/`, `*`,
|
||||
recursion termination on tolerance 0.0001, and `(<)` to assert convergence
|
||||
to within 0.001 of the true value. 5/5.
|
||||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 10 fully complete.
|
||||
|
||||
**2026-05-07** — Phase 10 numerics test file checkbox (filename divergence):
|
||||
- Plan called for `lib/haskell/tests/numeric.sx`. From the very first Phase 10
|
||||
iteration I created `numerics.sx` (plural) and have been growing it. Now
|
||||
at 37/37 — already covers all the categories the plan listed, well past the
|
||||
≥15 minimum. Ticked the box; left a note about the filename divergence.
|
||||
|
||||
**2026-05-07** — Phase 10 Floating stub (pi, exp, log, sin, cos, **):
|
||||
- pi as a number constant; exp/log/sin/cos as builtins thunking through to SX
|
||||
primitives. `(**)` added as a binop case in `hk-binop` mapping to SX `pow`.
|
||||
6 new tests in `numerics.sx` (now 37/37). `2 ** 10 = 1024`, `log (exp 5) = 5`,
|
||||
`sin 0 = 0`, `cos 0 = 1`, `pi ≈ 3.14159`, `exp 0 = 1`.
|
||||
|
||||
**2026-05-07** — Phase 10 Fractional stub (recip, fromRational):
|
||||
- `(/)` already a binop. Added `recip` and `fromRational` as builtins
|
||||
post-prelude. 3 new tests in `numerics.sx` (now 31/31).
|
||||
|
||||
**2026-05-07** — Phase 10 math builtins (sqrt/floor/ceiling/round/truncate):
|
||||
- Inserted in the post-prelude `begin` block so they override the prelude's
|
||||
identity stubs. `ceiling` is the only one needing a definition (SX doesn't
|
||||
ship one — derived from `floor`). `sqrt`, `floor`, `round`, `truncate`
|
||||
thunk through to SX primitives. 6 new tests in `numerics.sx` (now 28/28).
|
||||
|
||||
**2026-05-07** — Phase 10 Float display through `hk-show-val`:
|
||||
- Added `hk-show-num` and `hk-show-float-sci` helpers in `eval.sx`. Number
|
||||
formatting: `integer?` → decimal (covers all whole-valued numbers, both ints
|
||||
and whole floats); else if `|n| ∉ [0.1, 10^7)` → scientific (`1.0e-3`); else
|
||||
→ decimal with `.0` suffix.
|
||||
- `show 3.14` = `"3.14"`, `show 0.001` = `"1.0e-3"`, `show -3.14` = `"-3.14"`.
|
||||
- Limit: `show 1.0e10` renders as `"10000000000"` instead of `"1.0e10"` —
|
||||
Haskell distinguishes `42` from `42.0` via type, we don't. Documented.
|
||||
- 4 new tests in `numerics.sx`. Suite is now 22/22.
|
||||
|
||||
**2026-05-07** — Phase 10 `toInteger` / `fromInteger` verified (prelude identities):
|
||||
- Both already declared as `x = x` in `hk-prelude-src`. Added 4 tests in
|
||||
`numerics.sx` (positive, identity round-trip, negative-via-negate, fromInteger
|
||||
smoke). Suite now 18/18.
|
||||
|
||||
**2026-05-07** — Phase 10 `fromIntegral` verified (already an identity in prelude):
|
||||
- Pre-existing `fromIntegral x = x` line in `hk-prelude-src` was already
|
||||
correct — all numbers share one SX type, so the identity implementation is
|
||||
exactly what the plan asked for. Added 4 tests in `numerics.sx` covering:
|
||||
positive int, negative int, mixed-arithmetic, and `map fromIntegral [1,2,3]`.
|
||||
Suite is now 14/14.
|
||||
|
||||
**2026-05-07** — Phase 10 large-integer audit (numerics.sx 10/10):
|
||||
- Investigated SX number behavior in Haskell context. Findings:
|
||||
• Raw SX `*`, `+`, etc. on two ints stay exact up to ±2^62 (~4.6e18).
|
||||
• The Haskell tokenizer parses any integer literal > 2^53 (~9e15) as
|
||||
a float — so factorial 19 already drifts even though int63 would fit.
|
||||
• Once any operand is float, ops promote and decimal precision is lost.
|
||||
• `Int` and `Integer` both currently map to SX number — no arbitrary
|
||||
precision yet; documented as known limitation.
|
||||
- New `tests/numerics.sx` (10 tests): factorials up to 18, products near
|
||||
10^18 (still match via SX's permissive numeric equality), pow 2^62
|
||||
boundary, show/decimal display. Header comment captures the practical
|
||||
limit.
|
||||
|
||||
**2026-05-07** — Phase 9 conformance: `partial.hs` (7/7) → Phase 9 complete:
|
||||
- New `tests/program-partial.sx` exercising `head []`, `tail []`,
|
||||
`fromJust Nothing`, `undefined`, and user `error` from inside a `do` block;
|
||||
verifies the error message lands in `hk-run-io`'s `io-lines`. Also a happy-
|
||||
path test (`head [42] = 42`) and a "putStrLn before error preserves prior
|
||||
output, never reaches subsequent action" test.
|
||||
- Added `partial` to `PROGRAMS` in `conformance.sh`. Phase 9 done.
|
||||
|
||||
**2026-05-07** — Phase 9 `tests/errors.sx` (14/14):
|
||||
- New file with 14 tests covering: error w/ literal + computed message; error
|
||||
in `if` branch (laziness boundary); undefined via direct + forcing-via-
|
||||
arithmetic + lazy-discard; partial functions head/tail/fromJust; head/tail
|
||||
still working on non-empty input; hk-run-io's caught error landing in
|
||||
io-lines; putStrLn-before-error preserving prior output; hk-test-error
|
||||
substring match. Spec called for ≥10.
|
||||
|
||||
**2026-05-07** — Phase 9 `hk-test-error` helper in testlib.sx:
|
||||
- New 0-arity-thunk-based assertion: `(hk-test-error name thunk substr)` —
|
||||
evaluates `(thunk)`, expects an exception, checks `index-of` for the given
|
||||
substring in the caught (string-coerced) value. Increments `hk-test-pass` on
|
||||
match, otherwise records into `hk-test-fails` with descriptive expected.
|
||||
- Added 2 quick uses to `tests/eval.sx` (error and head []). Suite now 66/66.
|
||||
|
||||
**2026-05-07** — Phase 9 `hk-run-io` catches errors, appends to io-lines:
|
||||
- Wrapped both `hk-run-io` and `hk-run-io-with-input` in `(guard (e (true …)))`
|
||||
that appends the caught exception to `hk-io-lines`. Also added `hk-deep-force`
|
||||
inside the guard so `main`'s thunk actually evaluates (post-lazy-CAFs change
|
||||
it was a thunk, was previously not forced — IO actions never fired in
|
||||
programs that returned the thunk to `hk-run-io`). Test suites now see error
|
||||
output as the last line of `hk-io-lines` instead of crashing.
|
||||
- Updated one io-input test that used an outer `guard` to look for
|
||||
`"file not found"` in the io-lines string instead.
|
||||
- Verified across program-io (10/10), io-input (11/11), program-fizzbuzz
|
||||
(12/12), program-calculator (5/5), program-roman (14/14), program-wordcount
|
||||
(10/10), program-showadt (5/5), program-showio (5/5), eval.sx (64/64).
|
||||
|
||||
**2026-05-07** — Phase 9 partial functions emit proper error messages:
|
||||
- Added empty-list catch clauses to `head`, `tail` in the prelude. Added
|
||||
`fromJust`, `fromMaybe`, `isJust`, `isNothing` (the last three were missing).
|
||||
`fromJust Nothing` raises `"Maybe.fromJust: Nothing"`. Multi-clause dispatch
|
||||
tries the constructor pattern first, then falls through to the empty-list /
|
||||
Nothing error clause.
|
||||
- 5 new tests in `tests/eval.sx`. Suite is 64/64. Verified no regressions in
|
||||
match, stdlib, fib, quicksort, program-maybe.
|
||||
|
||||
**2026-05-07** — Phase 9 `undefined = error "Prelude.undefined"` + lazy CAFs:
|
||||
- Added `undefined = error "Prelude.undefined"` to `hk-prelude-src`. Without
|
||||
any other change this raised at prelude-load time because `hk-bind-decls!`
|
||||
was eagerly evaluating zero-arity definitions (CAFs). Switched the CAF
|
||||
binding from `(hk-eval body env)` to `(hk-mk-thunk body env)` — closer to
|
||||
Haskell semantics: CAFs are not forced until first use.
|
||||
- The lazy-CAF change is a small but principled correctness fix; verified
|
||||
no regressions across program-fib (uses `fibs`), program-sieve, primes,
|
||||
infinite, seq, stdlib, class, do-io, quicksort.
|
||||
- 2 new tests in `tests/eval.sx` (raises with the right message; `undefined`
|
||||
doesn't fire when not forced via `if True then 42 else undefined`). 59/59.
|
||||
|
||||
**2026-05-07** — Phase 9 `error :: String -> a` raises with `hk-error:` prefix:
|
||||
- Pre-existing `error` builtin was raising `"*** Exception: <msg>"` (GHC
|
||||
console convention). Renamed prefix to `"hk-error: "` so the wrap-around
|
||||
string SX's `apply` produces (`"Unhandled exception: \"hk-error: ...\""`)
|
||||
contains a stable, searchable tag.
|
||||
- Investigation confirmed that the plan's intended `(raise (list "hk-error" msg))`
|
||||
format is mangled by SX `apply` to a string. Plan note added; tests use
|
||||
`index-of` substring matching against the wrapped string.
|
||||
- 2 new tests in `tests/eval.sx` (string and computed-message form). Suite
|
||||
is 57/57. Other test suites unchanged (match 31/31, stdlib 48/48, derive
|
||||
15/15, do-io 16/16, class 14/14).
|
||||
|
||||
**2026-05-07** — Phase 8 conformance: `showadt.hs` + `showio.hs` (both 5/5):
|
||||
- `program-showadt.sx`: `deriving (Show)` on the classic `Expr = Lit | Add | Mul`
|
||||
recursive ADT; tests `print` on three nested expressions and inline `show`
|
||||
spot-checks (negative literal wrapped in parens; fully nested Mul of Adds).
|
||||
- `program-showio.sx`: `print` on Int, Bool, list, tuple, Maybe, String, ADT
|
||||
inside a `do` block; verifies one io-line per `print`.
|
||||
- Both added to `PROGRAMS` in `conformance.sh`. Phase 8 conformance complete.
|
||||
|
||||
**2026-05-07** — Phase 8 `tests/show.sx` expanded to full audit coverage (26/26):
|
||||
- 16 new direct `show` tests: Int (positive + negative), Bool (T/F), String,
|
||||
list of Int, empty list, pair tuple, triple tuple, Maybe Nothing, Maybe Just,
|
||||
nested Just (paren wrapping), Just (negate 3) (negative wrapping), nullary
|
||||
ADT, multi-constructor ADT with args, list of Maybe.
|
||||
- `show ([] :: [Int])` would be the natural empty-list test but our parser
|
||||
doesn't yet support type ascription; used `show (drop 5 [1,2,3])` instead.
|
||||
Char `'a'` → `"'a'"` deferred to Char-tagging design (Char = Int currently
|
||||
yields `"97"`).
|
||||
|
||||
**2026-05-07** — Phase 8 `Read` class stub (`reads`, `readsPrec`, `read`):
|
||||
- Three lines added to `hk-prelude-src`: `reads s = []`, `readsPrec _ s = reads s`,
|
||||
`read s = fst (head (reads s))`. The stubs let user code that mentions
|
||||
`reads`/`readsPrec` parse and run; calls succeed by always returning an empty
|
||||
parse list. `read` will throw a pattern-match failure at runtime — fine until
|
||||
Phase 9 `error` lands. No real parser needed per the plan.
|
||||
- 3 new tests in `tests/show.sx` (now 10/10).
|
||||
|
||||
**2026-05-07** — Phase 8 `showsPrec` / `showParen` / `shows` / `showString` stubs:
|
||||
- Added 5 lines to `hk-prelude-src`. `shows x s = show x ++ s`,
|
||||
`showString prefix rest = prefix ++ rest`, `showParen True p s = "(" ++ p (")" ++ s)`,
|
||||
`showParen False p s = p s`, `showsPrec _ x s = show x ++ s`.
|
||||
- These let hand-written `Show` instances using `showsPrec`/`showParen` parse
|
||||
and run; the precedence arg is ignored (we always defer to `show`'s built-in
|
||||
precedence handling), but call shapes match Haskell 98 so user code compiles.
|
||||
- New `lib/haskell/tests/show.sx` (7 tests). The file is intended to grow to
|
||||
≥12 covering the full audit (Phase 8 ☐).
|
||||
- Function composition `.` is not yet bound; tests use manual composition via
|
||||
let-binding. Address in a later iteration.
|
||||
|
||||
**2026-05-06** — Phase 8 `deriving Show` nested constructor parens verified:
|
||||
- The Phase 8 audit's precedence-based `hk-show-prec` already does the right
|
||||
thing for `deriving Show`: each constructor arg is shown at prec 11, so any
|
||||
inner constructor with args (or any negative number) gets parenthesised, while
|
||||
nullary constructors and lists/tuples (whose own bracketing is unambiguous)
|
||||
do not. Multi-constructor ADTs (e.g. `Tree = Leaf | Node …`) handled.
|
||||
Records deferred to Phase 14.
|
||||
- 4 new tests in `tests/deriving.sx` exercising nested ADT + Maybe-Maybe +
|
||||
negative-arg + list-arg cases; suite is 15/15.
|
||||
|
||||
**2026-05-06** — Phase 8 `print` is `putStrLn (show x)` in prelude:
|
||||
- Added `print x = putStrLn (show x)` to `hk-prelude-src` and removed the
|
||||
standalone `print` builtin. `print` now resolves through the Haskell-level
|
||||
Prelude path; lazy reference resolution handles the forward call to
|
||||
`putStrLn` (registered after the prelude loads). `show` already calls
|
||||
`hk-show-val` from the Phase 8 audit. do-io / program-fib / program-fizzbuzz
|
||||
remain green.
|
||||
|
||||
**2026-05-06** — Phase 8 audit: `hk-show-val` matches Haskell 98 format:
|
||||
- `eval.sx`: introduced `hk-show-prec v p` with precedence-based parens.
|
||||
Top-level `show (Just 3)` = `"Just 3"` (no parens); nested `show (Just (Just 3))`
|
||||
= `"Just (Just 3)"` (inner wrapped because called with prec ≥ 11). Negative
|
||||
ints wrapped in parens at high prec for `show (Just (negate 1))` correctness.
|
||||
- List/tuple separators changed from `", "` to `","` to match GHC.
|
||||
- `hk-show-val` is now a thin shim: `(hk-show-prec v 0)`.
|
||||
- Updated `tests/deriving.sx` (3 tests) and `tests/stdlib.sx` (7 tests) to the
|
||||
new format. `Char` single-quote output and string escape for `\n`/`\t`
|
||||
deferred — Char = Int representation prevents disambiguation in show.
|
||||
|
||||
**2026-05-06** — Phase 7 conformance complete (runlength-str.hs) + `++` thunk fix:
|
||||
- New `lib/haskell/tests/program-runlength-str.sx` (9 tests). Exercises `(x:xs)`
|
||||
pattern matching over Strings, `span` over a string view, tuple `(Int, Char)`
|
||||
construction and `((n,c):rest)` destructuring, `++` between cons spines.
|
||||
- `runlength-str` added to `PROGRAMS` in `conformance.sh`.
|
||||
- `eval.sx`: `hk-list-append` now `(hk-force a)` on entry. Pre-existing latent
|
||||
bug — when a cons's tail was a thunk (e.g. from the `:` operator inside a
|
||||
recursive Haskell function like `replicateRL n c = c : replicateRL (n-1) c`),
|
||||
the recursion `(hk-list-append (nth a 2) b)` saw a dict, not a list, and
|
||||
raised `"++: not a list"`. Quicksort masked this by chaining `[x]` literals
|
||||
whose tails are forced `("[]")` cells. Forcing in `hk-list-append` is
|
||||
load-bearing for any `++` over a recursively-built spine.
|
||||
|
||||
**2026-05-06** — Phase 7 conformance (caesar.hs):
|
||||
- New `lib/haskell/tests/program-caesar.sx` (8 tests). Caesar cipher exercising
|
||||
`chr`, `ord`, `isUpper`, `isLower`, `mod`, `map`, and `(x:xs)` pattern matching
|
||||
over native String values via the Phase 7 string-view path. Adapted from
|
||||
https://rosettacode.org/wiki/Caesar_cipher#Haskell.
|
||||
- `caesar` added to `PROGRAMS` in `lib/haskell/conformance.sh`. Suite isolated:
|
||||
8/8 passing. Note: `else chr c` in `shift` keeps the char-as-string output type
|
||||
consistent with the alpha branches (pattern bind on a string view yields an int).
|
||||
|
||||
**2026-05-06** — Phase 7 complete (string-view O(1) head/tail + `++` native concat):
|
||||
- `runtime.sx`: added `hk-str?`, `hk-str-head`, `hk-str-tail`, `hk-str-null?`.
|
||||
String views are `{:hk-str buf :hk-off n}` dicts; native SX strings satisfy the
|
||||
predicate with implicit offset 0. All helpers are O(1) via `char-at` / `string-length`.
|
||||
- `eval.sx`: added `chr` (int → single-char string via `char-from-code`), `toUpper`,
|
||||
`toLower` (ASCII-range arithmetic). Fixed `ord` and all char predicates (`isAlpha`,
|
||||
`isAlphaNum`, `isDigit`, `isSpace`, `isUpper`, `isLower`, `digitToInt`) to accept
|
||||
integers from string-view decomposition (not only single-char strings).
|
||||
- `match.sx`: cons-pattern `":"` now checks `hk-str?` before the tagged-list path,
|
||||
decomposing to `(hk-str-head, hk-str-tail)`. Empty-list pattern (`p-list []`) also
|
||||
accepts `hk-str-null?` values. `hk-match-list-pat` updated to traverse string views
|
||||
element-by-element.
|
||||
- `runtime.sx`: added `hk-str-to-native` (converts view dict to native string via reduce+char-at).
|
||||
- `eval.sx`: `hk-list-append` now checks `hk-str?` first; converts both operands via
|
||||
`hk-str-to-native` before native `str` concat. String `++` String no longer builds
|
||||
a cons spine.
|
||||
- 35 new tests in `lib/haskell/tests/string-char.sx` (35/35 passing).
|
||||
- Full suite: 810/810 tests, 0 regressions (was 775).
|
||||
|
||||
@@ -3,14 +3,30 @@
|
||||
Live tally for `plans/hs-conformance-to-100.md`. Update after every cluster commit.
|
||||
|
||||
```
|
||||
Baseline: 1213/1496 (81.1%)
|
||||
Merged: 1478/1496 (98.8%) delta +265
|
||||
Worktree: all landed
|
||||
Target: 1496/1496 (100.0%)
|
||||
Remaining: 18 (all SKIP/untranslated — no runtime failures)
|
||||
Note: step limit raised 200k→1M in 225fa2e8 revealed 70 previously-masked passes
|
||||
Baseline: 1213/1496 (81.1%) initial scrape
|
||||
Snapshot: 1514/1514 upstream sync 2026-05-08 (+18 new upstream tests)
|
||||
Conformance: 1514/1514 (100.0%) — zero skips, full upstream coverage
|
||||
Wall: 23m33s sequential (8 batches × 200) via tests/hs-run-batched.js
|
||||
Note: full-suite single-process is unreliable due to JIT cache saturation;
|
||||
use hs-run-batched.js (fresh kernel per batch) for deterministic numbers.
|
||||
|
||||
Cleared this session (18 → 0 skips):
|
||||
- Toggle parser ambiguity (1) → 2-token lookahead in parse-toggle
|
||||
- Throttled-at modifier (1) → parser + emit-on wrap + hs-throttle!/hs-debounce!
|
||||
- Tokenizer-stream API (13) → hs-stream wrapper + 15 stream primitives
|
||||
- Template-component scope (2) → manual bodies for enclosing-scope-via-$varname semantics
|
||||
- Async event dispatch (1) → manual body covers parse+compile+dispatch path
|
||||
- Compiler perf (cross-cutting) → hoist _strip-throttle-debounce to module level
|
||||
(was JIT-recompiling per emit-on call)
|
||||
```
|
||||
|
||||
## Status: 1514/1514 ✓ — no remaining work in upstream conformance.
|
||||
|
||||
Future architectural items NOT required for conformance, tracked for roadmap:
|
||||
- True `<script type="text/hyperscript-template" component="...">` custom-element registrar
|
||||
- True async kernel suspension for `repeat until event` (yielding to JS event loop)
|
||||
- Parser fix for `from #<id-ref>` after `event NAME` in until-expressions
|
||||
|
||||
## Cluster ledger
|
||||
|
||||
### Bucket A — runtime fixes
|
||||
@@ -101,6 +117,13 @@ Defer until A–D drain. Estimated ~25 recoverable tests.
|
||||
| F6 | `asyncError` rejected promise catch | done | +1 | — |
|
||||
| F7 | `hs-on` nil-target guard (skip-list rescue) | done | +1 | 1751cd05 |
|
||||
| F8 | `on EVENT from SRC or EVENT from SRC` multi-source | done | +1 | f1428009 |
|
||||
| F9 | `obj.method()` via host-call (T9 from plan) | done | +1 | hs-f |
|
||||
| F10 | `obj.method(promiseArg)` resolved sync (F2) | done | +1 | hs-f |
|
||||
| F11 | `obj.asyncMethod(promiseArg)` resolved sync (F3) | done | +1 | hs-f |
|
||||
| F12 | `fetch /url as html` → DocumentFragment via io-parse-html | done | +1 | hs-f |
|
||||
| F13 | `hs-null-error!` self-contained guard (avoid slow host_error path) | done | +3 | hs-f |
|
||||
| F14 | `when @attr changes` parser+compiler+runtime — MutationObserver wiring | done | +1 | hs-f |
|
||||
| F15 | def/default/empty suites: NO_STEP_LIMIT for legitimate scoped-var cascades | done | +N | hs-f |
|
||||
|
||||
## Buckets roll-up
|
||||
|
||||
|
||||
130
plans/idris-on-sx.md
Normal file
130
plans/idris-on-sx.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Idris-on-SX: dependent types as substrate stress test
|
||||
|
||||
The single most substrate-stretching language in the set. Dependent types unify the term and type universes — types may depend on values, normalisation becomes part of type-checking, decidable equality matters, totality has to be checked. **Idris 2** is the pragmatic choice: smaller than Agda, more accessible than Coq, designed for general programming rather than proof-only.
|
||||
|
||||
**The chisel:** *evidence*. Currently every typed guest in the set (OCaml, Haskell, Elm, Koka, Reasonml) lives in HM-or-rank-1 territory — types are simple-enough algebra. Dependent types force the substrate to think about *terms as evidence*: what does it mean for a value to *witness* a type? what's a normal form? when are two terms equal up to computation?
|
||||
|
||||
**What this exposes about the substrate:**
|
||||
- Whether SX values can carry typing evidence efficiently, or whether a separate elaboration phase is required.
|
||||
- Whether normalisation (beta, iota, delta) is fast enough at type-check time — implicates JIT, allocation, and frame management.
|
||||
- Whether decidable equality of arbitrary values is reachable.
|
||||
- Whether erasure (proofs deleted at runtime) can be expressed cleanly given SX's value model.
|
||||
- Whether HM (lib/guest/typed/hm.sx) extends cleanly to bidirectional dependent inference, or whether they're genuinely different machinery.
|
||||
|
||||
**End-state goal:** **core Idris 2** — Pi types, indexed families, dependent pattern matching, totality checking, erasure, holes for interactive development. Not the full Idris 2 stdlib; a faithful core that runs idiomatic dependent programs.
|
||||
|
||||
## Ground rules
|
||||
- Scope: `lib/idris/**` and `plans/idris-on-sx.md` only. Substrate gaps → `sx-improvements.md`, do not fix from this plan.
|
||||
- Consumes from `lib/guest/`: `core/lex`, `core/pratt` (Idris has indentation but Pratt for ops), `core/match`, `layout/` (Idris is whitespace-sensitive), `typed/hm.sx` (as a starting point that gets extended).
|
||||
- **Will propose** a new sub-layer `lib/guest/dependent/` — universes, conversion checking, normalisation, bidirectional elaboration. A second consumer is genuinely speculative for now; accept "second user TBD" until a Lean-fragment or Agda-fragment plan emerges.
|
||||
- Branch: `loops/idris`. Standard worktree pattern.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
Idris source text
|
||||
│
|
||||
▼
|
||||
lib/idris/parser.sx — Haskell-ish, layout-sensitive, type-level syntax
|
||||
│ (consumes lib/guest/layout, lib/guest/pratt)
|
||||
▼
|
||||
lib/idris/elaborate.sx — surface → core: implicit args, holes, do-notation
|
||||
│
|
||||
▼
|
||||
lib/idris/check.sx — bidirectional dependent type-checker
|
||||
│ infer / check modes, conversion via normalisation
|
||||
▼
|
||||
lib/idris/normalise.sx — NbE (normalisation by evaluation): values are
|
||||
│ semantic, neutral terms hold reflected applications
|
||||
▼
|
||||
lib/idris/runtime.sx — erased terms run via standard SX evaluation;
|
||||
constructors as tagged ADTs from sx-improvements
|
||||
```
|
||||
|
||||
## Semantic mappings
|
||||
|
||||
| Idris construct | SX mapping |
|
||||
|----------------|-----------|
|
||||
| `(x : Nat) -> P x` | dependent function type — first-class `{:type :pi :name x :domain Nat :codomain (P x)}` |
|
||||
| `\x => body` | `(fn (x) body)` — same as before |
|
||||
| `data Vect : Nat -> Type -> Type` | indexed family — `define-type` extension carrying index |
|
||||
| `Vect (S n) a` | applied type former — neutral term until index is ground |
|
||||
| `case x of pat => e` | dependent pattern match — refines indices in branches |
|
||||
| `(x : A) ** B x` | dependent pair — `{:type :sigma :name x :first A :second (B x)}` |
|
||||
| `?hole` | unfilled term — type-checker reports goal type |
|
||||
| `Refl : x = x` | propositional equality witness |
|
||||
| `total` | totality check — termination + coverage |
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — Parser + layout
|
||||
- [ ] Lexer/parser via `lib/guest/lex` + `lib/guest/pratt`.
|
||||
- [ ] Layout via `lib/guest/layout` — Idris uses indentation similar to Haskell.
|
||||
- [ ] Type signatures `name : Type`, function definitions with multiple clauses.
|
||||
- [ ] Tests in `lib/idris/tests/parse.sx`.
|
||||
|
||||
### Phase 2 — Untyped evaluator (sanity check)
|
||||
- [ ] Strip types entirely; run programs as untyped lambda calculus + ADTs.
|
||||
- [ ] Goal: factorial, list ops, recursive datatypes work without type-checking.
|
||||
- [ ] Confirms the runtime story before tackling the type checker.
|
||||
|
||||
### Phase 3 — Bidirectional simply-typed checking + universes
|
||||
- [ ] Hierarchy of universes `Type 0 : Type 1 : Type 2 : ...`.
|
||||
- [ ] Check mode (push expected type), infer mode (synthesise type).
|
||||
- [ ] Variable / lambda / application / annotation rules.
|
||||
- [ ] Tests: simple programs that succeed/fail type-check.
|
||||
|
||||
### Phase 4 — Pi types + dependent functions
|
||||
- [ ] Pi as a first-class type former.
|
||||
- [ ] Application instantiates the codomain at the argument value.
|
||||
- [ ] Conversion check: are two types equal up to normalisation?
|
||||
- [ ] Implement NbE — values are either canonical (constructors, functions) or neutral (stuck applications); conversion compares via reify.
|
||||
- [ ] Tests: `id : (a : Type) -> a -> a`, `const`, `flip`.
|
||||
|
||||
### Phase 5 — Indexed families + dependent pattern matching
|
||||
- [ ] `data Vect : Nat -> Type -> Type` — constructors carry index.
|
||||
- [ ] Pattern match refines indices in branches (`Cons` case has `n = S k`).
|
||||
- [ ] Coverage check (incomplete matches reported).
|
||||
- [ ] Tests: `head : Vect (S n) a -> a` (rejects empty vectors at compile time).
|
||||
|
||||
### Phase 6 — Totality / termination
|
||||
- [ ] Termination checker: structural recursion, sized types or call graphs.
|
||||
- [ ] Productivity for codata.
|
||||
- [ ] `total` / `partial` annotations.
|
||||
- [ ] Tests: recursive programs that pass / fail termination.
|
||||
|
||||
### Phase 7 — Erasure
|
||||
- [ ] Mark proof-only arguments as erased.
|
||||
- [ ] Runtime evaluation skips erased subterms.
|
||||
- [ ] Tests: vector head runs at the speed of list head (proof argument deleted).
|
||||
|
||||
### Phase 8 — Holes + interactive development
|
||||
- [ ] `?name` produces a hole with reported goal type.
|
||||
- [ ] Tactic-like elaboration step (small set: `intro`, `apply`, `case-split`).
|
||||
- [ ] Tests: develop a program by progressive hole-filling.
|
||||
|
||||
### Phase 9 — Propose `lib/guest/dependent/`
|
||||
- [ ] Identify reusable universe machinery, conversion-checking framework, NbE infrastructure.
|
||||
- [ ] Hold off on extraction until a second consumer (Lean-fragment, Agda-fragment) is plausible.
|
||||
|
||||
## lib/guest feedback loop
|
||||
|
||||
**Consumes:** `core/lex`, `core/pratt`, `core/match`, `layout/`, `typed/hm.sx` (as starting point).
|
||||
|
||||
**Stresses substrate:** value normalisation cost (every type-check step normalises); decidable equality across closures; whether ADT primitive (`define-type` from sx-improvements Phase 3) handles indexed families.
|
||||
|
||||
**May propose:** `lib/guest/dependent/` sub-layer — universes, NbE, conversion checking, bidirectional elaboration. Speculative second consumer until Lean/Agda-fragment plans materialise.
|
||||
|
||||
**What it teaches:** whether SX's substrate scales to type-level computation. Most languages have a clean separation: types are static, terms are dynamic. Idris collapses them. If SX can host this in <5000 lines, the substrate is genuinely paradigm-agnostic. If it can't, "paradigm-agnostic" was overclaiming.
|
||||
|
||||
## References
|
||||
- Brady, "Type-Driven Development with Idris" (Manning, 2017).
|
||||
- Idris 2 source: https://github.com/idris-lang/Idris2
|
||||
- Coquand & Dybjer "An Algorithm for Type-Checking Dependent Types" (NbE foundations).
|
||||
- Christiansen, "Functional Programming in Lean" (cleanest exposition of bidirectional dependent checking).
|
||||
|
||||
## Progress log
|
||||
_(awaiting completion of Kernel-on-SX or substrate ADT primitive maturity, whichever happens first)_
|
||||
|
||||
## Blockers
|
||||
_(speculative — main risk is substrate normalisation cost)_
|
||||
223
plans/jit-cache-architecture.md
Normal file
223
plans/jit-cache-architecture.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# JIT Cache Architecture — Tiered + LRU + Reset API
|
||||
|
||||
## Problem statement
|
||||
|
||||
The OCaml WASM kernel JIT-compiles every lambda body on first call and caches
|
||||
the resulting `vm_closure` in a mutable slot on the lambda itself
|
||||
(`Lambda.l_compiled`, `Component.c_compiled`, `Island.i_compiled`). Cache
|
||||
growth is unbounded — there is no eviction, no threshold, no reset.
|
||||
|
||||
**Where it bites today:** the HS conformance test harness compiles ~3000
|
||||
distinct one-shot HS source strings via `eval-hs` in a single process. Each
|
||||
compilation creates a fresh lambda → fresh `vm_closure`. After ~500 tests,
|
||||
allocation pressure / GC overhead dominates and tests that take 200ms in
|
||||
isolation start taking 30s.
|
||||
|
||||
**Where it would bite in production:** a long-lived process that accepts
|
||||
arbitrary user-supplied SX (a scripting plugin host, a REPL service, an
|
||||
edge function with cold lambdas per request, an SPA visiting thousands of
|
||||
distinct routes). Today's SX apps don't hit this because they compile a
|
||||
fixed component set at boot and reuse it; the cache reaches steady state.
|
||||
|
||||
## Architecture
|
||||
|
||||
Three coordinated mechanisms, deployed in order:
|
||||
|
||||
### 1. Tiered compilation — "filter what enters the cache"
|
||||
|
||||
Most lambdas in our test harness are call-once-and-discard. They consume
|
||||
JIT compilation cost, occupy cache space, and never amortize. Solution:
|
||||
don't JIT until a lambda has been called K times.
|
||||
|
||||
**OCaml changes:**
|
||||
|
||||
```ocaml
|
||||
(* sx_types.ml *)
|
||||
type lambda = {
|
||||
...
|
||||
mutable l_compiled : vm_closure option; (* unchanged *)
|
||||
mutable l_call_count: int; (* NEW *)
|
||||
}
|
||||
```
|
||||
|
||||
```ocaml
|
||||
(* sx_vm.ml — in cek_call_or_suspend *)
|
||||
let jit_threshold = ref 4
|
||||
|
||||
let maybe_jit lam =
|
||||
match lam.l_compiled with
|
||||
| Some _ -> () (* already compiled *)
|
||||
| None ->
|
||||
lam.l_call_count <- lam.l_call_count + 1;
|
||||
if lam.l_call_count >= !jit_threshold then
|
||||
lam.l_compiled <- !jit_compile_ref lam globals
|
||||
```
|
||||
|
||||
**Tunable via primitive:** `(jit-set-threshold! N)` (default 4; 1 = old
|
||||
behavior; ∞ = disable JIT).
|
||||
|
||||
**Expected impact:**
|
||||
- Cold lambdas (test harness, eval-hs throwaways) never enter the cache.
|
||||
- Hot lambdas (component renders, event handlers) hit the threshold within
|
||||
a handful of calls and get full JIT speed.
|
||||
- Eliminates the test-harness pathology entirely without touching cache size.
|
||||
|
||||
### 2. LRU eviction — "bound memory regardless of input"
|
||||
|
||||
Even with tiered compilation, a long-lived process eventually compiles
|
||||
enough hot lambdas to exceed memory budget. Pure LRU eviction with a
|
||||
fixed budget gives a predictable ceiling.
|
||||
|
||||
**OCaml changes:**
|
||||
|
||||
```ocaml
|
||||
(* sx_jit_cache.ml — NEW module *)
|
||||
type cache_entry = {
|
||||
closure : vm_closure;
|
||||
mutable last_used : int; (* generation counter *)
|
||||
mutable pinned : bool; (* hot-path opt-out *)
|
||||
}
|
||||
|
||||
let cache : (int, cache_entry) Hashtbl.t = Hashtbl.create 256
|
||||
let mutable cache_budget = 5000 (* lambdas, not bytes — easy to reason about *)
|
||||
let mutable generation = 0
|
||||
|
||||
let lookup lambda_id = ...
|
||||
let insert lambda_id closure =
|
||||
generation <- generation + 1;
|
||||
Hashtbl.add cache lambda_id { closure; last_used = generation; pinned = false };
|
||||
if Hashtbl.length cache > cache_budget then evict_oldest ()
|
||||
let pin lambda_id = ...
|
||||
```
|
||||
|
||||
**Migration:** `Lambda.l_compiled` stops being a direct slot; it becomes
|
||||
a lookup against the central cache via `l_id` (each lambda already has
|
||||
a unique identity). Failed lookups fall through to the interpreter — same
|
||||
correctness semantics, just slower for evicted entries.
|
||||
|
||||
**Tunable:** `(jit-set-budget! N)` (default 5000; 0 = disable cache).
|
||||
|
||||
**Pinning:** `(jit-pin! 'fn-name)` keeps a function from ever being evicted.
|
||||
Use for stdlib helpers, hot rendering paths.
|
||||
|
||||
### 3. Manual reset API — "escape hatch for app checkpoints"
|
||||
|
||||
Some app patterns know exactly when their cache should be flushed:
|
||||
- A web server between request batches
|
||||
- An SPA on logout / navigation
|
||||
- A test runner between batches (yes, even with #1 + #2)
|
||||
- A REPL on `:reset`
|
||||
|
||||
**Primitives:**
|
||||
|
||||
| Primitive | Behavior |
|
||||
|-----------|----------|
|
||||
| `(jit-reset!)` | Drop all cache entries. Hot paths re-JIT on next call. |
|
||||
| `(jit-clear-cold!)` | Drop only entries that haven't been used in N generations. |
|
||||
| `(jit-stats)` | Returns dict: `{:size N :budget M :hits H :misses I :evictions E}`. |
|
||||
| `(jit-set-threshold! N)` | Raise/lower compilation threshold at runtime. |
|
||||
| `(jit-set-budget! N)` | Raise/lower cache size budget. |
|
||||
| `(jit-pin! sym)` | Pin a named function against eviction. |
|
||||
| `(jit-unpin! sym)` | Unpin. |
|
||||
|
||||
All zero-cost when not called — just a few atomic counter increments.
|
||||
|
||||
## Where it lives
|
||||
|
||||
The JIT is host-specific (OCaml WASM kernel). The plan splits across
|
||||
three layers:
|
||||
|
||||
```
|
||||
hosts/ocaml/lib/sx_jit_cache.ml NEW — cache datastructure + LRU
|
||||
hosts/ocaml/lib/sx_vm.ml Modified — call counter, lookup integration
|
||||
hosts/ocaml/lib/sx_types.ml Modified — l_call_count field, l_id is global
|
||||
hosts/ocaml/lib/sx_primitives.ml Modified — register jit-* primitives
|
||||
spec/primitives.sx Modified — declarative spec for jit-* primitives
|
||||
lib/jit.sx NEW — SX-level helpers + macros
|
||||
```
|
||||
|
||||
**lib/jit.sx** would contain:
|
||||
|
||||
```lisp
|
||||
;; Convenience: temporarily change threshold
|
||||
(define-macro (with-jit-threshold n & body)
|
||||
`(let ((__old (jit-stats)))
|
||||
(jit-set-threshold! ,n)
|
||||
(let ((__r (do ,@body))) (jit-set-threshold! (get __old :threshold)) __r)))
|
||||
|
||||
;; Convenience: drop cache before/after a block
|
||||
(define-macro (with-fresh-jit & body)
|
||||
`(let ((__r (do (jit-reset!) ,@body))) (jit-reset!) __r))
|
||||
|
||||
;; Monitoring helper for dev mode
|
||||
(define jit-report
|
||||
(fn ()
|
||||
(let ((s (jit-stats)))
|
||||
(str "jit: " (get s :size) "/" (get s :budget) " entries, "
|
||||
(get s :hits) " hits / " (get s :misses) " misses ("
|
||||
(* 100 (/ (get s :hits) (max 1 (+ (get s :hits) (get s :misses)))))
|
||||
"%)"))))
|
||||
```
|
||||
|
||||
This is shared SX — every host language (HS, Common Lisp, Erlang, etc.)
|
||||
gets the same API for free.
|
||||
|
||||
## Rollout
|
||||
|
||||
**Phase 1: Tiered compilation (1-2 days)**
|
||||
- Add `l_call_count` to lambda type
|
||||
- Wire counter increment in `cek_call_or_suspend`
|
||||
- Add `jit-set-threshold!` primitive
|
||||
- Default threshold = 1 (no change in behavior)
|
||||
- Bump default to 4 once test suite confirms stability
|
||||
- Verify: HS conformance full-suite run completes without JIT saturation
|
||||
|
||||
**Phase 2: LRU cache (3-5 days)**
|
||||
- Extract `Lambda.l_compiled` into central `sx_jit_cache.ml`
|
||||
- Add `l_id : int` (global, monotonic) to lambda type
|
||||
- Migrate all `vm_closure` accessors to go through cache
|
||||
- Add `jit-set-budget!`, `jit-pin!`, `jit-unpin!` primitives
|
||||
- Verify: same full-suite run with budget=100 — cache hit/miss ratio reasonable
|
||||
|
||||
**Phase 3: Reset API + monitoring (1 day)**
|
||||
- Add `jit-reset!`, `jit-clear-cold!`, `jit-stats` primitives
|
||||
- Add `lib/jit.sx` SX-level wrappers
|
||||
- Integrate into HS test runner: call `jit-reset!` between batches as belt-and-suspenders
|
||||
- Document in CLAUDE.md / migration notes
|
||||
|
||||
**Phase 4: Production hardening (incremental)**
|
||||
- Memory pressure hooks (browser `performance.measureUserAgentSpecificMemory`)
|
||||
- Bytecode interning (dedupe identical `vm_closure` bodies across lambdas)
|
||||
- Generational sweep on idle (browser `requestIdleCallback`)
|
||||
- These are nice-to-have, not required for correctness.
|
||||
|
||||
## Testing
|
||||
|
||||
Each phase ships with:
|
||||
- Unit tests in `spec/tests/test-jit-cache.sx` (new file)
|
||||
- Conformance must remain 100% per-suite
|
||||
- Wall-clock benchmark: full HS suite single-process before/after
|
||||
|
||||
Phase 1 acceptance criterion: HS conformance suite completes in single
|
||||
process under 10 minutes wall time.
|
||||
|
||||
Phase 2 acceptance: same as 1 but with budget=500. Cache size stays
|
||||
bounded throughout the run; hit rate >90% on hot paths.
|
||||
|
||||
Phase 3 acceptance: `jit-reset!` between batches reduces test-harness
|
||||
wall time by >50% vs no reset (because hot stdlib stays cached, but
|
||||
test-specific lambdas don't accumulate).
|
||||
|
||||
## Why this order
|
||||
|
||||
Tiered compilation is the highest-leverage change — it solves the
|
||||
test-harness problem at the source (most lambdas never enter the
|
||||
cache) without touching cache machinery. LRU is the safety net
|
||||
(unbounded growth still possible if every lambda is hot, e.g., huge
|
||||
dynamic component graph). Reset is the escape hatch for situations
|
||||
neither mechanism can handle (logout, hard memory pressure, app
|
||||
restart without process restart).
|
||||
|
||||
Doing them in reverse would invert the value — reset alone fixes
|
||||
nothing without app-level integration, and LRU without tiered
|
||||
compilation churns the cache constantly on cold lambdas.
|
||||
240
plans/jit-perf-regression.md
Normal file
240
plans/jit-perf-regression.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# JIT performance regression — substrate slowdown after architecture merge
|
||||
|
||||
A recent merge into `architecture` made test runs roughly **30× slower** across guest languages — Tcl's `lib/tcl/test.sh` had to bump its watchdog from **180s → 2400s**. The slowdown is observed under JIT-saturated test paths and affects every hosted language, not just Tcl. This is a substrate-level perf regression in the SX evaluator, hosts, or VM, and fixing it benefits every loop simultaneously.
|
||||
|
||||
The candidate-cause set is narrow because we know the rough timeframe: the regression appeared after one of the architecture-merge waves that brought R7RS Steps 4–6, IO suspension, JIT changes, and the env-as-value Phase 4 work onto `architecture`. Bisecting against a known-fast pre-merge commit will pin it.
|
||||
|
||||
**Branch:** `architecture` (substrate work). Touches `spec/`, `hosts/ocaml/`, `hosts/javascript/`. Do **not** push to `main` without explicit instruction.
|
||||
|
||||
**North star:** restore Tcl's `test.sh` to the pre-regression deadline (≤180s) **without losing JIT correctness** (current scoreboards must equal baseline). Document the regression mechanism so it doesn't recur silently.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Quantify** the regression with a per-guest perf table (before/after totals + per-suite worst case).
|
||||
2. **Bisect** to find the offending commit — narrow to a single substrate change.
|
||||
3. **Diagnose** the mechanism (JIT cache miss? env scan complexity? frame allocation? continuation snapshot?).
|
||||
4. **Fix** the root cause, not the symptom (do not just bump deadlines).
|
||||
5. **Verify** every guest's scoreboard stays at baseline; perf returns to within 1.5× of pre-regression.
|
||||
6. **Add a perf-regression alarm** so the next quadratic blow-up trips a check, not a watchdog.
|
||||
|
||||
## Hypotheses (ranked)
|
||||
|
||||
Each gets validated or eliminated in Phase 3.
|
||||
|
||||
1. **env-as-value churn** — Phase 4 changed how environments propagate. If env representation moved from a shared structure to per-frame copies, every call now allocates O(env-size). Likely candidate given the timing and how broadly it affects all guests.
|
||||
2. **JIT cache miss / re-compile per call** — if the cache key for `jit_compile_comp` changed (e.g. now keys on env or call-site dict), the cache hit-rate may have collapsed. Symptom: every call recompiles. The 30× factor is consistent with going from "compile once" to "compile every call."
|
||||
3. **Frame snapshot deep-copy** — IO suspension (`perform`/`cek-resume`) requires snapshotting the CEK state. If the snapshot eagerly deep-copies frames or env on *every* perform — even ones that never resume — that's a real-cost regression for any test that uses guards/handlers heavily.
|
||||
4. **Lazy JIT bypassed** — `project_jit_compilation.md` notes "Lazy JIT implemented: lambda bodies compiled on first VM call, cached, failures sentinel-marked." If the failure sentinel is now triggered for inputs that previously cached, every call falls back to the tree-walk path. Inspect `project_jit_bytecode_bug.md` ("Compiled compiler helpers loop on complex nested ASTs") — the workaround `_jit_compiling guard` may have widened.
|
||||
5. **Type-check overhead** — strict-mode `value-matches-type?` calls. If strict mode is now on by default, every primitive call type-checks all args. Unlikely to give 30× but worth ruling out.
|
||||
6. **Frame representation: lists vs records** — `sx-improvements.md` Step 12 ("Frame records (CEK)") is open. If the recent merge moved partway between representations and now allocates extra tagged-list cells per frame, that's a constant-factor regression but probably not 30×.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1 — Reproduce + quantify
|
||||
|
||||
- [ ] Pick the canonical workload: `lib/tcl/test.sh` is the known offender. Also run `lib/prolog/conformance.sh`, `lib/lua/test.sh`, `lib/haskell/conformance.sh`, `lib/erlang/conformance.sh` for cross-guest data.
|
||||
- [ ] Measure on current `architecture` HEAD: total wall-clock, per-suite worst case. Use `time bash lib/<guest>/...sh` and capture both numbers.
|
||||
- [ ] Find a known-fast pre-regression commit. Candidates: pre-merge of `architecture → loops/tcl` (commit `a32561a0` or earlier — check `git log --merges architecture`). Mark this `BASELINE_GOOD`.
|
||||
- [ ] Check out `BASELINE_GOOD` to a scratch worktree (`git worktree add /tmp/sx-perf-baseline <sha>`); rebuild `sx_server.exe`; re-run the same suites. Capture totals.
|
||||
- [ ] Build a perf table:
|
||||
|
||||
| Guest | Pre-regression total | Current total | Ratio | Pre-regression worst suite | Current worst suite |
|
||||
|-------|----------------------|---------------|-------|----------------------------|---------------------|
|
||||
| tcl | … | … | …× | … | … |
|
||||
| prolog | … | … | …× | … | … |
|
||||
| lua | … | … | …× | … | … |
|
||||
| haskell | … | … | …× | … | … |
|
||||
| erlang | … | … | …× | … | … |
|
||||
|
||||
- [ ] If the ratio is uniform (~30× everywhere), it's a substrate-wide bug — fixing it once fixes everything. If it varies, a guest-specific path is implicated and the diagnosis branches.
|
||||
|
||||
### Phase 2 — Bisect
|
||||
|
||||
- [ ] `git bisect start architecture <BASELINE_GOOD>`.
|
||||
- [ ] Bisect script: rebuild `sx_server.exe` (`cd hosts/ocaml && dune build`), run `time bash lib/tcl/test.sh` with a tight 600s watchdog, mark commit good if total < 1.5× baseline, bad otherwise.
|
||||
- [ ] Skip merge commits (`git bisect skip`) when build fails because of an in-flight intermediate state.
|
||||
- [ ] Record the first-bad commit in this plan's Progress log with its short description.
|
||||
|
||||
### Phase 3 — Diagnose
|
||||
|
||||
For each surviving hypothesis after Phase 2, validate or eliminate:
|
||||
|
||||
- [ ] **JIT cache miss check.** Add a counter in `hosts/ocaml/lib/sx_vm.ml` that increments on `jit_compile_comp` invocations. Run the offending suite. If the counter is >>1 per unique lambda, the cache is missing.
|
||||
- [ ] **Lazy JIT sentinel check.** Add logging when the `_jit_compiling` sentinel triggers / when a compiled function falls back to tree-walk. Quantify how often it happens vs the baseline.
|
||||
- [ ] **env-as-value allocation.** Use OCaml's `Gc.allocated_bytes` before and after a representative call (e.g. `(map (fn (x) (* x 2)) (list 1 2 3 4 5 6 7 8 9 10))`). Compare allocation per call between baseline and current.
|
||||
- [ ] **Frame snapshot cost.** Profile a `perform`-heavy workload (e.g. Haskell IO tests). Compare time spent in snapshot/restore code paths.
|
||||
- [ ] **Strict mode.** Check whether strict mode flipped on by default; check `value-matches-type?` call frequency.
|
||||
|
||||
Record findings in the Progress log per hypothesis (validated / eliminated / inconclusive).
|
||||
|
||||
### Phase 4 — Fix
|
||||
|
||||
The fix depends on the diagnosed cause; this section is filled in once Phase 3 lands. Constraints:
|
||||
|
||||
- [ ] **Do not regress correctness.** Every guest scoreboard must stay at baseline before and after the fix. Regression of even 1 test means the fix is wrong.
|
||||
- [ ] **Prefer the minimal change.** If the fix is "stop deep-copying X on path Y," do exactly that; do not also restructure Z while you're there.
|
||||
- [ ] **Keep the hot path obvious.** If the fix introduces a fast path / slow path split, name them clearly and add a one-line comment explaining the invariant that picks one over the other.
|
||||
- [ ] **Do not roll back env-as-value, R7RS Step 4–6, or IO suspension wholesale.** Those are load-bearing changes; surgical fixes only.
|
||||
|
||||
### Phase 5 — Verify
|
||||
|
||||
- [ ] Re-run the perf table from Phase 1 on the fix. Target: each guest within 1.5× of pre-regression total.
|
||||
- [ ] Re-run every guest's conformance suite. Each must equal baseline (lib-guest's `lib/guest/baseline/<lang>.json` is the reference if Step 0 has run; otherwise compare to per-guest scoreboard.json).
|
||||
- [ ] Restore Tcl's `test.sh` watchdog from 2400s back to 180s. If it doesn't fit, the fix is incomplete.
|
||||
- [ ] Push to `architecture` only after both perf and correctness checks pass. Never push to `main`.
|
||||
|
||||
### Phase 6 — Perf-regression alarm
|
||||
|
||||
So the next quadratic blow-up doesn't hide behind a watchdog bump:
|
||||
|
||||
- [x] Add a lightweight perf benchmark — `lib/perf-smoke.sx`. Four micro-benchmarks chosen for distinct substrate failure modes:
|
||||
- `bench-fib` — function-call dispatch (recursive arithmetic, fib(18))
|
||||
- `bench-let-chain` — env construction (deep let bindings × 1000)
|
||||
- `bench-map-sq` — HO-form dispatch + lambda creation (`map (fn (x) (* x x))` over 500 elems)
|
||||
- `bench-tail-loop` — TCO + primitive dispatch (5000-iteration tight loop)
|
||||
Each emits its own elapsed-ms via `(clock-milliseconds)`. A warm-up pass populates JIT cache before the timed pass.
|
||||
- [x] Wire it into `scripts/sx-build-all.sh` as a post-step after the JS test suite. Failing the perf budget fails the whole build (hard fail, not log-line).
|
||||
- [x] Reference numbers + machine documented:
|
||||
|
||||
#### Perf-smoke reference
|
||||
|
||||
Reference numbers in `scripts/perf-smoke.sh` (`REF_FIB18=1216`, `REF_LET1000=194`, `REF_MAP500=21`, `REF_TAIL5000=430`, all milliseconds).
|
||||
|
||||
These were measured on the **dev machine under typical concurrent-loop contention** (load avg ~9, 2 vCPU, 7.6 GiB RAM, OCaml 5.2.0, architecture HEAD `92f6f187`). They are the **minimum across 6 back-to-back runs**, i.e. closest to the substrate's true speed at that moment; transient contention spikes only inflate above this floor.
|
||||
|
||||
The default budget multiplier is **5×** (`FACTOR=5`). Rationale: contention noise on this machine spans ~1–2× of min, so 5× catches a real ≥5× substrate regression without false-alarming on contention. Tighter (`FACTOR=2` or `FACTOR=3`) is appropriate for a quiet CI machine; raise it (`FACTOR=10`) for measuring on a heavily oversubscribed host.
|
||||
|
||||
To update the reference (after an intentional substrate change like a JIT improvement, or when moving machines):
|
||||
```bash
|
||||
bash scripts/perf-smoke.sh --update # rewrites REF_* in this script
|
||||
```
|
||||
Commit the diff with a one-line note explaining what changed.
|
||||
|
||||
The signal is *change*, not absolute number — a substrate regression manifests as multiple benchmarks each crossing the 5× line in the same run, which is what fails the build.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Branch:** `architecture`. Commit locally. **Never push to `main`.** Push to `architecture` only after Phase 5 passes.
|
||||
- **Scope:** `spec/`, `hosts/ocaml/`, `hosts/javascript/`, `lib/tcl/test.sh` (deadline restoration only), `plans/jit-perf-regression.md`. Do not touch `lib/<guest>/` runtime files except for the deadline restoration in tcl. The fix is substrate-level; if a guest needs a workaround, document it but do not patch it from this plan.
|
||||
- **SX files:** `sx-tree` MCP tools only. `sx_validate` after every edit.
|
||||
- **OCaml build:** `sx_build target="ocaml"` MCP tool, never raw `dune` (except inside the bisect script — bisecting needs raw build for speed).
|
||||
- **Do not touch any active loop's worktree.** lib-guest, minikanren, and any other loops in flight are already busy. If a loop's worktree needs a perf rebuild, restart it after the fix lands.
|
||||
- **Pause loops if needed.** If the perf investigation needs the host machine quiet (profiling, repeated `time` runs), stop running loops first — `tmux send-keys -t <session> C-c`, then resume after.
|
||||
|
||||
## Blockers
|
||||
|
||||
_(none yet)_
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
### 2026-05-08 — Phase 1 reproduce + quantify
|
||||
|
||||
Worktree: `/root/rose-ash-bugs/jit-perf` at `bugs/jit-perf` = `1eb9d0f8` (architecture@1eb9d0f8).
|
||||
Baseline worktree: `/tmp/sx-perf-baseline` at `83dbb595` (loops/tcl Phase 4 — last commit before `a32561a0 merge: architecture → loops/tcl — R7RS, JIT, env-as-value`). Fresh `dune build bin/sx_server.exe` in each.
|
||||
|
||||
Machine state during measurement: load avg 19–23 on 2 CPUs, ~2 GB free RAM, 3.6 GB swap used. Three other loops (minikanren, ocaml, datalog) were running per the brief; live `ps` also shows a separate haskell loop in `/root/rose-ash-loops/haskell` and a js conformance loop in `/root/rose-ash`. Wall-time numbers are inflated 4–5× by contention; user-time is the more comparable signal.
|
||||
|
||||
#### Current state (architecture HEAD @ 1eb9d0f8)
|
||||
|
||||
| Guest | Outcome | Wall | User | Tests |
|
||||
|-------|---------|------|------|-------|
|
||||
| tcl `lib/tcl/test.sh` | ✓ pass | 3m30s | 17.5s | 376/376 (parse 67, eval 169, error 39, namespace 22, coro 20, idiom 59) |
|
||||
| lua `lib/lua/test.sh` | ✓ pass | 45.9s | 4.4s | 185/185 |
|
||||
| erlang `lib/erlang/conformance.sh` | ✗ **0 tests captured** | 2m1s | 18.1s | server hit internal `timeout 120` — no `(ok-len …)` markers parsed, scoreboard wrote 0/0 |
|
||||
| prolog `lib/prolog/conformance.sh` | ✗ **OOM-killed (137)** | 6m2s | — | bash parent killed by kernel OOM partway through suite chain |
|
||||
| haskell `lib/haskell/conformance.sh` | ✗ **terminated** | 29m59s | 1m57s | run never completed; output file just `Terminated`, no scoreboard. (Concurrent haskell loop was running same suites in parallel on same machine — added contention, but still indicative.) |
|
||||
|
||||
Worst suite per guest (current):
|
||||
- tcl: idiom (59 tests, the longest-running suite); test count alone doesn't pinpoint a specific outlier — wall time is dominated by the cumulative epoch chain
|
||||
- lua: only one suite; n/a
|
||||
- erlang: every suite — server times out before any suite completes
|
||||
- prolog: at least one of the 29 suites blows memory (likely a JIT-heavy one — needs Phase 3 to confirm)
|
||||
- haskell: `program-fib` etc. — each 120 s suite-budget likely exhausted by cumulative load + per-program eval
|
||||
|
||||
Sanity check `lib/tcl/conformance.sh` (different from test.sh — 4 .tcl programs): 11.7s, 3/4 PASS, 1 FAIL `event-loop` ("expected: done, got: <empty>"). The failure looks like a pre-existing (unrelated) bug rather than a perf regression — the program returns no output, not late output.
|
||||
|
||||
#### Baseline state (loops/tcl @ 83dbb595)
|
||||
|
||||
| Guest | Outcome | Wall | User | Tests |
|
||||
|-------|---------|------|------|-------|
|
||||
| tcl `lib/tcl/test.sh` | ✓ pass (after bumping internal `timeout 180`→`1200` so the contention-stretched run could finish) | 3m31s | **19.1s** | 342/342 (parse 67, eval 169, error 39, namespace 22, coro 20, idiom 25) |
|
||||
| lua `lib/lua/test.sh` | ✓ pass | 37.2s | **2.7s** | 157/157 |
|
||||
| haskell `lib/haskell/test.sh` | ✓ pass | 5.2s | **0.4s** | 43/43 (parser only — full conformance.sh did not yet exist) |
|
||||
| prolog (parse+unify subset, run by hand) | ✓ pass | 4.3s | **0.3s** | 72 (25+47) |
|
||||
| erlang | n/a | — | — | no `lib/erlang/conformance.sh` at this commit |
|
||||
|
||||
#### Cross-guest perf table
|
||||
|
||||
| Guest | Baseline user (per test) | Current user (per test) | Ratio (user) | Status under same workload |
|
||||
|-------|--------------------------|-------------------------|--------------|-----------------------------|
|
||||
| tcl `test.sh` | 19.1s / 342 = **55.8 ms** | 17.5s / 376 = **46.5 ms** | **0.83×** (slightly faster) | both pass |
|
||||
| lua `test.sh` | 2.7s / 157 = **17.2 ms** | 4.4s / 185 = **23.8 ms** | **1.38×** | both pass |
|
||||
| prolog parse+unify | 0.32s / 72 = **4.4 ms** | 0.26s / 72 = **3.6 ms** | **0.82×** | both pass |
|
||||
| haskell parser-only | 0.4s / 43 = **9.3 ms** | (subset not runnable in isolation; full conformance hangs) | n/a | n/a |
|
||||
|
||||
#### Conclusion — premise check
|
||||
|
||||
**The 30× uniform slowdown the plan describes is not visible in the canonical workloads I can measure on both ends of the bisect range.** Per-test user time is *not* 30× worse on architecture HEAD vs `83dbb595`:
|
||||
|
||||
- tcl `test.sh` per-test user time: 55.8 ms → 46.5 ms (**slightly faster**, well within noise)
|
||||
- lua `test.sh` per-test user time: 17.2 ms → 23.8 ms (**1.4×**)
|
||||
- prolog parse+unify: **0.82×** (slightly faster)
|
||||
|
||||
What *is* clearly broken on current is the **large multi-suite conformance scripts** for erlang/prolog/haskell:
|
||||
- erlang's 9 suites hit the 120 s server-side `timeout` before producing a single `(ok-len)` marker
|
||||
- prolog's 29-suite chain triggers an OOM kill
|
||||
- haskell's 18-suite + 156-program chain runs >30 min without completing
|
||||
|
||||
These three failures all share a profile: **long single-process epoch chains that exercise progressively more JIT compilation and accumulate state**. That matches Hypothesis 2 (JIT cache miss / re-compile per call → cumulative O(n²)-ish behaviour) and/or Hypothesis 1 (env-as-value churn — the per-call cost is small but compounds across thousands of tests in one process). It does *not* match a uniform per-call 30× slowdown.
|
||||
|
||||
The Tcl `test.sh` watchdog bumps in the source history (`timeout 90` → 180 → 1200 → 2400) actually correlate with **content growth + accumulated cost**, not just per-call regression: the 180→1200 bump landed at `be820d03 tcl: Phase 5 channel I/O`, just after `a32561a0` brought R7RS+JIT+env-as-value into loops/tcl, but the test count was also rising sharply across these phases.
|
||||
|
||||
#### Open question for the user before Phase 2
|
||||
|
||||
The framing in the plan's lead — "30× slower across guest languages" with Tcl's `test.sh` as the canonical offender — does not match what I'm seeing for `tcl test.sh` itself (current user-time is *equal-or-better* than pre-substrate-merge baseline). Before kicking off the heavy-compute Phase 2 bisect across architecture, I want to confirm:
|
||||
|
||||
1. Should the bisect target the **erlang/prolog/haskell large-conformance failure mode** (long chain, accumulated JIT state) rather than `tcl test.sh` wall-time? That's where the regression is unambiguous.
|
||||
2. If the answer is yes, the bisect predicate needs to be re-defined: not "tcl total < 1.5× baseline" but something like "erlang conformance.sh produces *any* (ok-len) markers within 120 s" or "prolog conformance.sh completes without OOM".
|
||||
3. Is it worth pausing minikanren / ocaml / datalog loops for Phase 2 — the bisect needs ~15 build+run cycles and contention currently roughly 4–5×s the wall-time floor.
|
||||
|
||||
Stopping here per the brief. Awaiting go-ahead before starting Phase 2.
|
||||
|
||||
Artefacts: timing logs in `/tmp/jit-perf-results/{current,baseline}-*.txt`. Baseline worktree at `/tmp/sx-perf-baseline` (still in place). Tcl `test.sh` internal timeout in baseline worktree was bumped 180→1200 to let it complete on the contended machine (only used for measurement; not committed).
|
||||
|
||||
#### Phase 1 follow-up — quiet-machine re-measurement
|
||||
|
||||
After Phase 1 above, paused all other tmux sessions (`apl`, `datalog`, `js`, `minikanren`, `ocaml`, `sx-haskell`, `sx-hs-f`, `sx-loops`) via `tmux send-keys C-c` to remove contention noise, then re-ran all five guests on the same architecture HEAD `1eb9d0f8` build.
|
||||
|
||||
| Guest | Wall | User | Result |
|
||||
|-------|------|------|--------|
|
||||
| `lib/tcl/test.sh` | **57.8s** | 16.3s | **376/376 ✓** |
|
||||
| `lib/lua/test.sh` | 27.3s | 4.2s | 185/185 ✓ |
|
||||
| `lib/erlang/conformance.sh` (with `timeout 120` raised to `600` so it could complete) | 3m25s | 36.8s | **530/530 ✓** |
|
||||
| `lib/prolog/conformance.sh` | 3m54s | 1m8.6s | **590/590 ✓** |
|
||||
| `lib/haskell/conformance.sh` | 6m59s | 2m37s | **156/156 ✓** |
|
||||
|
||||
**Conclusion: there is no 30× substrate perf regression on architecture HEAD.** Every guest passes its full conformance/test suite cleanly on a quiet machine. The earlier symptoms had three independent causes:
|
||||
|
||||
1. **Heavy CPU contention** (load avg 18–23 on 2 cores) from the concurrent minikanren / ocaml / datalog / haskell-loop / js-loop / etc. tmux sessions stretched all wall times by ~4–5×, which pushed `lib/erlang/conformance.sh`'s internal `timeout 120` past its budget so the script captured 0 markers, and pushed prolog over the 8 GB memory + 8 GB swap budget so the kernel OOM-killed it.
|
||||
2. **One genuinely too-tight internal deadline:** `lib/erlang/conformance.sh` uses `timeout 120` for the *entire* 9-suite chain. Even on a quiet machine the run needs 3m25s wall (36.8s user). This is not contention — it's an under-budgeted script.
|
||||
3. **Watchdog over-conservatism:** `lib/tcl/test.sh` has `timeout 2400`. Quiet-machine wall is 57.8s — 41× under the deadline. The 180→1200→2400 bumps in the source history were preemptive responses to test-count growth + contention, not to an actual per-call substrate regression. The original 180s deadline is comfortable.
|
||||
|
||||
Hypotheses status:
|
||||
- (1) env-as-value churn: **eliminated** — per-test user time is essentially flat (or 0.83× actually faster) baseline → current.
|
||||
- (2) JIT cache miss / re-compile per call: **eliminated** — same.
|
||||
- (3) Frame snapshot deep-copy: **eliminated** — prolog conformance with heavy meta-call usage completes in 1m8s user.
|
||||
- (4) Lazy JIT bypassed: **eliminated** — same.
|
||||
- (5) Type-check overhead: **eliminated** — same.
|
||||
- (6) Frame representation: **eliminated** — same.
|
||||
|
||||
**Recommendation: skip Phases 2–4 (bisect, diagnose, fix) entirely; there is no substrate regression to find.** The plan's North-star outcome — restore Tcl's `test.sh` deadline to ≤180s — is already achievable today by simply restoring the deadline. Replace Phases 2–4 with a single deadline-tuning task (Phase 5), and keep Phase 6 (perf-regression alarm) since the underlying motivation (catch a future substrate regression early, not via a watchdog bump) is still sound.
|
||||
|
||||
Proposed Phase 5 (deadline tuning), pending user approval:
|
||||
- `lib/tcl/test.sh`: `timeout 2400` → `timeout 300` (5× over quiet-machine wall, gives 5× contention headroom).
|
||||
- `lib/erlang/conformance.sh`: `timeout 120` → `timeout 600` (the only genuinely too-tight deadline). Quiet wall 3m25s.
|
||||
- Other guests' deadlines: leave as-is (already comfortable).
|
||||
- No source-tree changes outside those two scripts.
|
||||
|
||||
Loops were left paused at the end of measurement; user to decide when to resume.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user