Compare commits
95 Commits
loops/prol
...
90bc1208d9
| Author | SHA1 | Date | |
|---|---|---|---|
| 90bc1208d9 | |||
| 8019e5725b | |||
| 2edd426748 | |||
| 3e07727d6b | |||
| bcde5e126a | |||
| 182e6f63ef | |||
| 912de5a274 | |||
| 077f4a5d38 | |||
| 36e6762539 | |||
| 4022b60901 | |||
| c02ffcf316 | |||
| a7790418f8 | |||
| 3c0a963229 | |||
| a8613656e9 | |||
| ec3512d63b | |||
| 10c278d073 | |||
| 1ad8e74aa6 | |||
| f4193a2e8e | |||
| f43659ce51 | |||
| d4964c166c | |||
| 4d7b3e299c | |||
| 6a34ae3ae1 | |||
| a381154507 | |||
| 24e1a862fb | |||
| d8d5588e42 | |||
| a40a970080 | |||
| 3b0ac67a10 | |||
| 24d78464d8 | |||
| 7d329f024d | |||
| c8582c4d49 | |||
| 036022cc17 | |||
| e9d2003d6a | |||
| be2b11acc2 | |||
| ab3c3693c0 | |||
| 8ba0a33f6e | |||
| e9abc2cf61 | |||
| 3d8937d759 | |||
| dfbcece644 | |||
| b939becd86 | |||
| 60f88ab4fe | |||
| 4b600f17e8 | |||
| 46da676c29 | |||
| 0862a6140b | |||
| edf4e525f8 | |||
| 130d4d7c18 | |||
| ac79328418 | |||
| 0fe00bf7ac | |||
| 06a3eee114 | |||
| c3d2b9d87d | |||
| 7286629cf7 | |||
| da4b526abb | |||
| 59a835efc3 | |||
| 133bdf5295 | |||
| 2e4502878f | |||
| e44cb89ab4 | |||
| 835b5314ce | |||
| 43cc1d9003 | |||
| 8328e96ff6 | |||
| 24522902cc | |||
| a8a79dc902 | |||
| 1ad9d63f1b | |||
| f63b214726 | |||
| 5d1913e730 | |||
| 0dc7e1599c | |||
| 6c87210728 | |||
| 3fb0212414 | |||
| 518ad37def | |||
| d98b5fa223 | |||
| cc0af51921 | |||
| 0ffe208e31 | |||
| b78e06a772 | |||
| 9eb12c66fd | |||
| 21cb9cf51a | |||
| d84cf1882a | |||
| 6602ec8cc9 | |||
| b126d4da76 | |||
| a9d5a1082f | |||
| 0577f245e2 | |||
| f5acb31c94 | |||
| b12a22e68a | |||
| 7888fbfd81 | |||
| 45ec553519 | |||
| e3e767e434 | |||
| c70bbdeb36 | |||
| 8f0fc4ce52 | |||
| 1d85e3a79c | |||
| 5a332fa430 | |||
| d1a00562a4 | |||
| 3759575b29 | |||
| f247cb2898 | |||
| f8023cf74e | |||
| 3316d402fd | |||
| fb72c4ab9c | |||
| e52c209c3d | |||
| 6a00df2609 |
File diff suppressed because it is too large
Load Diff
@@ -293,6 +293,8 @@ env["pop-suite"] = function() {
|
||||
return null;
|
||||
};
|
||||
|
||||
env["test-allowed?"] = function(name) { return true; };
|
||||
|
||||
// Load test framework
|
||||
const projectDir = path.join(__dirname, "..", "..");
|
||||
const specTests = path.join(projectDir, "spec", "tests");
|
||||
@@ -341,6 +343,20 @@ if (fs.existsSync(swapPath)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load spec library files (define-library modules imported by tests)
|
||||
for (const libFile of ["stdlib.sx", "signals.sx", "coroutines.sx"]) {
|
||||
const libPath = path.join(projectDir, "spec", libFile);
|
||||
if (fs.existsSync(libPath)) {
|
||||
const libSrc = fs.readFileSync(libPath, "utf8");
|
||||
const libExprs = Sx.parse(libSrc);
|
||||
for (const expr of libExprs) {
|
||||
try { Sx.eval(expr, env); } catch (e) {
|
||||
console.error(`Error loading spec/${libFile}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load tw system (needed by spec/tests/test-tw.sx)
|
||||
const twDir = path.join(projectDir, "shared", "sx", "templates");
|
||||
for (const twFile of ["tw-type.sx", "tw-layout.sx", "tw.sx"]) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -37,7 +37,10 @@ let rec deep_equal a b =
|
||||
match a, b with
|
||||
| Nil, Nil -> true
|
||||
| Bool a, Bool b -> a = b
|
||||
| Integer a, Integer b -> a = b
|
||||
| Number a, Number b -> a = b
|
||||
| Integer a, Number b -> float_of_int a = b
|
||||
| Number a, Integer b -> a = float_of_int b
|
||||
| String a, String b -> a = b
|
||||
| Symbol a, Symbol b -> a = b
|
||||
| Keyword a, Keyword b -> a = b
|
||||
@@ -226,7 +229,7 @@ let make_test_env () =
|
||||
| [String s] ->
|
||||
let parsed = Sx_parser.parse_all s in
|
||||
(match parsed with
|
||||
| [List (Symbol "sxbc" :: Number _ :: payload :: _)] -> payload
|
||||
| [List (Symbol "sxbc" :: (Number _ | Integer _) :: payload :: _)] -> payload
|
||||
| _ -> raise (Eval_error "bytecode-deserialize: invalid sxbc format"))
|
||||
| _ -> raise (Eval_error "bytecode-deserialize: expected string"));
|
||||
|
||||
@@ -240,7 +243,7 @@ let make_test_env () =
|
||||
| [String s] ->
|
||||
let parsed = Sx_parser.parse_all s in
|
||||
(match parsed with
|
||||
| [List (Symbol "cek-state" :: Number _ :: payload :: _)] -> payload
|
||||
| [List (Symbol "cek-state" :: (Number _ | Integer _) :: payload :: _)] -> payload
|
||||
| _ -> raise (Eval_error "cek-deserialize: invalid cek-state format"))
|
||||
| _ -> raise (Eval_error "cek-deserialize: expected string"));
|
||||
|
||||
@@ -320,7 +323,10 @@ let make_test_env () =
|
||||
bind "identical?" (fun args ->
|
||||
match args with
|
||||
| [a; b] -> Bool (match a, b with
|
||||
| Integer x, Integer y -> x = y
|
||||
| Number x, Number y -> x = y
|
||||
| Integer x, Number y -> float_of_int x = y
|
||||
| Number x, Integer y -> x = float_of_int y
|
||||
| String x, String y -> x = y
|
||||
| Bool x, Bool y -> x = y
|
||||
| Nil, Nil -> true
|
||||
@@ -366,11 +372,15 @@ let make_test_env () =
|
||||
|
||||
bind "append!" (fun args ->
|
||||
match args with
|
||||
| [ListRef r; v; Number n] when int_of_float n = 0 ->
|
||||
| [ListRef r; v; (Number n)] when int_of_float n = 0 ->
|
||||
r := v :: !r; ListRef r (* prepend *)
|
||||
| [ListRef r; v; (Integer 0)] ->
|
||||
r := v :: !r; ListRef r (* prepend Integer index *)
|
||||
| [ListRef r; v] -> r := !r @ [v]; ListRef r (* append in place *)
|
||||
| [List items; v; Number n] when int_of_float n = 0 ->
|
||||
| [List items; v; (Number n)] when int_of_float n = 0 ->
|
||||
List (v :: items) (* immutable prepend *)
|
||||
| [List items; v; (Integer 0)] ->
|
||||
List (v :: items) (* immutable prepend Integer index *)
|
||||
| [List items; v] -> List (items @ [v]) (* immutable fallback *)
|
||||
| _ -> raise (Eval_error "append!: expected list and value"));
|
||||
|
||||
@@ -546,7 +556,10 @@ let make_test_env () =
|
||||
bind "batch-begin!" (fun _args -> Sx_ref.batch_begin_b ());
|
||||
bind "batch-end!" (fun _args -> Sx_ref.batch_end_b ());
|
||||
bind "now-ms" (fun _args -> Number 1000.0);
|
||||
bind "random-int" (fun args -> match args with [Number lo; _] -> Number lo | _ -> Number 0.0);
|
||||
bind "random-int" (fun args -> match args with
|
||||
| [Number lo; _] -> Number lo
|
||||
| [Integer lo; _] -> Integer lo
|
||||
| _ -> Integer 0);
|
||||
bind "try-rerender-page" (fun _args -> Nil);
|
||||
bind "collect!" (fun args ->
|
||||
match args with
|
||||
@@ -1107,6 +1120,47 @@ let make_test_env () =
|
||||
| _ :: _ -> String "confirmed"
|
||||
| _ -> Nil);
|
||||
|
||||
bind "values" (fun args ->
|
||||
match args with
|
||||
| [v] -> v
|
||||
| vs ->
|
||||
let d = Hashtbl.create 2 in
|
||||
Hashtbl.replace d "_values" (Bool true);
|
||||
Hashtbl.replace d "_list" (List vs);
|
||||
Dict d);
|
||||
|
||||
bind "call-with-values" (fun args ->
|
||||
match args with
|
||||
| [producer; consumer] ->
|
||||
let result = Sx_ref.cek_call producer (List []) in
|
||||
let spread = (match result with
|
||||
| Dict d when (match Hashtbl.find_opt d "_values" with Some (Bool true) -> true | _ -> false) ->
|
||||
(match Hashtbl.find_opt d "_list" with Some (List l) -> l | _ -> [result])
|
||||
| _ -> [result])
|
||||
in
|
||||
Sx_ref.cek_call consumer (List spread)
|
||||
| _ -> raise (Eval_error "call-with-values: expected 2 args"));
|
||||
|
||||
bind "promise?" (fun args ->
|
||||
match args with
|
||||
| [v] -> Bool (Sx_ref.is_promise v)
|
||||
| _ -> Bool false);
|
||||
|
||||
bind "make-promise" (fun args ->
|
||||
match args with
|
||||
| [v] ->
|
||||
let d = Hashtbl.create 4 in
|
||||
Hashtbl.replace d "_promise" (Bool true);
|
||||
Hashtbl.replace d "forced" (Bool true);
|
||||
Hashtbl.replace d "value" v;
|
||||
Dict d
|
||||
| _ -> Nil);
|
||||
|
||||
bind "force" (fun args ->
|
||||
match args with
|
||||
| [p] -> Sx_ref.force_promise p
|
||||
| _ -> Nil);
|
||||
|
||||
env
|
||||
|
||||
(* ====================================================================== *)
|
||||
@@ -1142,18 +1196,20 @@ let run_foundation_tests () =
|
||||
in
|
||||
|
||||
Printf.printf "Suite: parser\n";
|
||||
assert_eq "number" (Number 42.0) (List.hd (parse_all "42"));
|
||||
assert_eq "number" (Integer 42) (List.hd (parse_all "42"));
|
||||
assert_eq "string" (String "hello") (List.hd (parse_all "\"hello\""));
|
||||
assert_eq "bool true" (Bool true) (List.hd (parse_all "true"));
|
||||
assert_eq "nil" Nil (List.hd (parse_all "nil"));
|
||||
assert_eq "keyword" (Keyword "class") (List.hd (parse_all ":class"));
|
||||
assert_eq "symbol" (Symbol "foo") (List.hd (parse_all "foo"));
|
||||
assert_eq "list" (List [Symbol "+"; Number 1.0; Number 2.0]) (List.hd (parse_all "(+ 1 2)"));
|
||||
assert_eq "list" (List [Symbol "+"; Integer 1; Integer 2]) (List.hd (parse_all "(+ 1 2)"));
|
||||
(match List.hd (parse_all "(div :class \"card\" (p \"hi\"))") with
|
||||
| List [Symbol "div"; Keyword "class"; String "card"; List [Symbol "p"; String "hi"]] ->
|
||||
incr pass_count; Printf.printf " PASS: nested list\n"
|
||||
| v -> incr fail_count; Printf.printf " FAIL: nested list — got %s\n" (Sx_types.inspect v));
|
||||
(match List.hd (parse_all "'(1 2 3)") with
|
||||
| List [Symbol "quote"; List [Integer 1; Integer 2; Integer 3]] ->
|
||||
incr pass_count; Printf.printf " PASS: quote sugar\n"
|
||||
| List [Symbol "quote"; List [Number 1.0; Number 2.0; Number 3.0]] ->
|
||||
incr pass_count; Printf.printf " PASS: quote sugar\n"
|
||||
| v -> incr fail_count; Printf.printf " FAIL: quote sugar — got %s\n" (Sx_types.inspect v));
|
||||
@@ -1161,7 +1217,7 @@ let run_foundation_tests () =
|
||||
| Dict d when dict_has d "a" && dict_has d "b" ->
|
||||
incr pass_count; Printf.printf " PASS: dict literal\n"
|
||||
| v -> incr fail_count; Printf.printf " FAIL: dict literal — got %s\n" (Sx_types.inspect v));
|
||||
assert_eq "comment" (Number 42.0) (List.hd (parse_all ";; comment\n42"));
|
||||
assert_eq "comment" (Integer 42) (List.hd (parse_all ";; comment\n42"));
|
||||
assert_eq "string escape" (String "hello\nworld") (List.hd (parse_all "\"hello\\nworld\""));
|
||||
assert_eq "multiple exprs" (Number 2.0) (Number (float_of_int (List.length (parse_all "(1 2 3) (4 5)"))));
|
||||
|
||||
@@ -1978,6 +2034,10 @@ let run_spec_tests env test_files =
|
||||
(match Hashtbl.find_opt d "children" with
|
||||
| Some (List l) when i >= 0 && i < List.length l -> List.nth l i
|
||||
| _ -> (match Hashtbl.find_opt d (string_of_int i) with Some v -> v | None -> Nil))
|
||||
| [Dict d; Integer n] ->
|
||||
(match Hashtbl.find_opt d "children" with
|
||||
| Some (List l) when n >= 0 && n < List.length l -> List.nth l n
|
||||
| _ -> (match Hashtbl.find_opt d (string_of_int n) with Some v -> v | None -> Nil))
|
||||
| _ -> Nil);
|
||||
|
||||
(* Stringify a value for DOM string properties *)
|
||||
@@ -2052,8 +2112,8 @@ let run_spec_tests env test_files =
|
||||
Hashtbl.replace d "childNodes" (List [])
|
||||
| _ -> ());
|
||||
stored
|
||||
| [ListRef r; Number n; value] ->
|
||||
let idx = int_of_float n in
|
||||
| [ListRef r; idx_v; value] when (match idx_v with Number _ | Integer _ -> true | _ -> false) ->
|
||||
let idx = match idx_v with Number n -> int_of_float n | Integer n -> n | _ -> 0 in
|
||||
let lst = !r in
|
||||
if idx >= 0 && idx < List.length lst then
|
||||
r := List.mapi (fun i v -> if i = idx then value else v) lst
|
||||
@@ -2190,7 +2250,7 @@ let run_spec_tests env test_files =
|
||||
| [String name; value] ->
|
||||
let attrs = match Hashtbl.find_opt d "attributes" with Some (Dict a) -> a | _ ->
|
||||
let a = Hashtbl.create 4 in Hashtbl.replace d "attributes" (Dict a); a in
|
||||
let sv = match value with String s -> s | Number n ->
|
||||
let sv = match value with String s -> s | Integer n -> string_of_int n | Number n ->
|
||||
let i = int_of_float n in if float_of_int i = n then string_of_int i
|
||||
else string_of_float n | _ -> Sx_types.inspect value in
|
||||
Hashtbl.replace attrs name (String sv);
|
||||
@@ -2632,6 +2692,7 @@ let run_spec_tests env test_files =
|
||||
let rec json_of_value = function
|
||||
| Nil -> `Null
|
||||
| Bool b -> `Bool b
|
||||
| Integer n -> `Int n
|
||||
| Number n ->
|
||||
if Float.is_integer n && Float.abs n < 1e16
|
||||
then `Int (int_of_float n) else `Float n
|
||||
@@ -2647,8 +2708,8 @@ let run_spec_tests env test_files =
|
||||
let rec value_of_json = function
|
||||
| `Null -> Nil
|
||||
| `Bool b -> Bool b
|
||||
| `Int i -> Number (float_of_int i)
|
||||
| `Intlit s -> (try Number (float_of_string s) with _ -> String s)
|
||||
| `Int i -> Integer i
|
||||
| `Intlit s -> (try Integer (int_of_string s) with _ -> try Number (float_of_string s) with _ -> String s)
|
||||
| `Float f -> Number f
|
||||
| `String s -> String s
|
||||
| `List xs -> List (List.map value_of_json xs)
|
||||
@@ -2811,6 +2872,7 @@ let run_spec_tests env test_files =
|
||||
match sx_vm_execute with
|
||||
| Some fn -> Sx_ref.cek_call fn (List args)
|
||||
| None -> Nil)));
|
||||
load_module "stdlib.sx" spec_dir; (* pure SX stdlib: format etc. *)
|
||||
load_module "signals.sx" spec_dir; (* core reactive primitives *)
|
||||
load_module "signals.sx" web_dir; (* web extensions *)
|
||||
load_module "freeze.sx" lib_dir;
|
||||
|
||||
@@ -296,6 +296,10 @@ let read_blob () =
|
||||
(* consume trailing newline *)
|
||||
(try ignore (input_line stdin) with End_of_file -> ());
|
||||
data
|
||||
| [List [Symbol "blob"; Integer n]] ->
|
||||
let data = read_exact_bytes n in
|
||||
(try ignore (input_line stdin) with End_of_file -> ());
|
||||
data
|
||||
| _ -> raise (Eval_error ("read_blob: expected (blob N), got: " ^ line))
|
||||
|
||||
(** Batch IO mode — collect requests during aser-slot, resolve after. *)
|
||||
@@ -357,6 +361,11 @@ let rec read_io_response () =
|
||||
| [List (Symbol "io-response" :: Number n :: values)]
|
||||
when int_of_float n = !current_epoch ->
|
||||
(match values with [v] -> v | _ -> List values)
|
||||
| [List [Symbol "io-response"; Integer n; value]]
|
||||
when n = !current_epoch -> value
|
||||
| [List (Symbol "io-response" :: Integer n :: values)]
|
||||
when n = !current_epoch ->
|
||||
(match values with [v] -> v | _ -> List values)
|
||||
(* Legacy untagged: (io-response value) — accept for backwards compat *)
|
||||
| [List [Symbol "io-response"; value]] -> value
|
||||
| [List (Symbol "io-response" :: values)] ->
|
||||
@@ -396,6 +405,12 @@ let read_batched_io_response () =
|
||||
when int_of_float n = !current_epoch -> s
|
||||
| [List [Symbol "io-response"; Number n; v]]
|
||||
when int_of_float n = !current_epoch -> serialize_value v
|
||||
| [List [Symbol "io-response"; Integer n; String s]]
|
||||
when n = !current_epoch -> s
|
||||
| [List [Symbol "io-response"; Integer n; SxExpr s]]
|
||||
when n = !current_epoch -> s
|
||||
| [List [Symbol "io-response"; Integer n; v]]
|
||||
when n = !current_epoch -> serialize_value v
|
||||
(* Legacy untagged *)
|
||||
| [List [Symbol "io-response"; String s]]
|
||||
| [List [Symbol "io-response"; SxExpr s]] -> s
|
||||
@@ -959,6 +974,7 @@ let setup_io_bridges env =
|
||||
bind "sleep" (fun args -> io_request "sleep" args);
|
||||
bind "set-response-status" (fun args -> match args with
|
||||
| [Number n] -> _pending_response_status := int_of_float n; Nil
|
||||
| [Integer n] -> _pending_response_status := n; Nil
|
||||
| _ -> Nil);
|
||||
bind "set-response-header" (fun args -> io_request "set-response-header" args)
|
||||
|
||||
@@ -1361,6 +1377,7 @@ let rec dispatch env cmd =
|
||||
| Bool true -> "true"
|
||||
| Bool false -> "false"
|
||||
| Number n -> Sx_types.format_number n
|
||||
| Integer n -> string_of_int n
|
||||
| String s -> "\"" ^ escape_sx_string s ^ "\""
|
||||
| Symbol s -> s
|
||||
| Keyword k -> ":" ^ k
|
||||
@@ -1374,6 +1391,10 @@ let rec dispatch env cmd =
|
||||
| Island i -> "~" ^ i.i_name
|
||||
| SxExpr s -> s
|
||||
| RawHTML s -> "\"" ^ escape_sx_string s ^ "\""
|
||||
| Char n -> Sx_types.inspect (Char n)
|
||||
| Eof -> Sx_types.inspect Eof
|
||||
| Port _ -> Sx_types.inspect result
|
||||
| Rational (n, d) -> Printf.sprintf "%d/%d" n d
|
||||
| _ -> "nil"
|
||||
in
|
||||
send_ok_raw (raw_serialize result)
|
||||
@@ -4450,6 +4471,8 @@ let site_mode () =
|
||||
match exprs with
|
||||
| [List [Symbol "epoch"; Number n]] ->
|
||||
current_epoch := int_of_float n
|
||||
| [List [Symbol "epoch"; Integer n]] ->
|
||||
current_epoch := n
|
||||
(* render-page: full SSR pipeline — URL → complete HTML *)
|
||||
| [List [Symbol "render-page"; String path]] ->
|
||||
(try match http_render_page env path [] with
|
||||
@@ -4507,6 +4530,8 @@ let () =
|
||||
(* Epoch marker: (epoch N) — set current epoch, read next command *)
|
||||
| [List [Symbol "epoch"; Number n]] ->
|
||||
current_epoch := int_of_float n
|
||||
| [List [Symbol "epoch"; Integer n]] ->
|
||||
current_epoch := n
|
||||
| [cmd] -> dispatch env cmd
|
||||
| _ -> send_error ("Expected single command, got " ^ string_of_int (List.length exprs))
|
||||
end
|
||||
|
||||
@@ -47,7 +47,9 @@ open Sx_runtime
|
||||
let trampoline_fn : (value -> value) ref = ref (fun v -> v)
|
||||
let trampoline v = !trampoline_fn v
|
||||
|
||||
|
||||
(* Step limit for timeout detection — set to 0 to disable *)
|
||||
let step_limit : int ref = ref 0
|
||||
let step_count : int ref = ref 0
|
||||
|
||||
(* === Mutable globals — backing refs for transpiler's !_ref / _ref := === *)
|
||||
let _strict_ref = ref (Bool false)
|
||||
@@ -126,6 +128,90 @@ let enhance_error_with_trace msg =
|
||||
_last_error_kont_ref := Nil;
|
||||
msg ^ (format_comp_trace trace)
|
||||
|
||||
(* Hand-written sf_define_type — skipped from transpile because the spec uses
|
||||
&rest params and empty-dict literals that the transpiler can't emit cleanly.
|
||||
Implements: (define-type Name (Ctor1 f1 f2) (Ctor2 f3) ...)
|
||||
Creates constructor fns, Name?/Ctor? predicates, Ctor-field accessors,
|
||||
and records ctors in *adt-registry*. *)
|
||||
let sf_define_type args env_val =
|
||||
let items = (match args with List l -> l | _ -> []) in
|
||||
let type_sym = List.nth items 0 in
|
||||
let type_name = value_to_string type_sym in
|
||||
let ctor_specs = List.tl items in
|
||||
let env_has_v k = sx_truthy (env_has env_val (String k)) in
|
||||
let env_bind_v k v = ignore (env_bind env_val (String k) v) in
|
||||
let env_get_v k = env_get env_val (String k) in
|
||||
if not (env_has_v "*adt-registry*") then
|
||||
env_bind_v "*adt-registry*" (Dict (Hashtbl.create 8));
|
||||
let registry = env_get_v "*adt-registry*" in
|
||||
let ctor_names = List.map (fun spec ->
|
||||
(match spec with List (sym :: _) -> String (value_to_string sym) | _ -> Nil)
|
||||
) ctor_specs in
|
||||
(match registry with Dict d -> Hashtbl.replace d type_name (List ctor_names) | _ -> ());
|
||||
env_bind_v (type_name ^ "?")
|
||||
(NativeFn (type_name ^ "?", fun pargs ->
|
||||
(match pargs with
|
||||
| [v] ->
|
||||
(match v with
|
||||
| Dict d -> Bool (Hashtbl.mem d "_adt" &&
|
||||
(match Hashtbl.find_opt d "_type" with Some (String t) -> t = type_name | _ -> false))
|
||||
| _ -> Bool false)
|
||||
| _ -> Bool false)));
|
||||
List.iter (fun spec ->
|
||||
(match spec with
|
||||
| List (sym :: fields) ->
|
||||
let cn = value_to_string sym in
|
||||
let field_names = List.map value_to_string fields in
|
||||
let arity = List.length fields in
|
||||
env_bind_v cn
|
||||
(NativeFn (cn, fun ctor_args ->
|
||||
if List.length ctor_args <> arity then
|
||||
raise (Eval_error (Printf.sprintf "%s: expected %d args, got %d"
|
||||
cn arity (List.length ctor_args)))
|
||||
else begin
|
||||
let d = Hashtbl.create 4 in
|
||||
Hashtbl.replace d "_adt" (Bool true);
|
||||
Hashtbl.replace d "_type" (String type_name);
|
||||
Hashtbl.replace d "_ctor" (String cn);
|
||||
Hashtbl.replace d "_fields" (List ctor_args);
|
||||
Dict d
|
||||
end));
|
||||
env_bind_v (cn ^ "?")
|
||||
(NativeFn (cn ^ "?", fun pargs ->
|
||||
(match pargs with
|
||||
| [v] ->
|
||||
(match v with
|
||||
| Dict d -> Bool (Hashtbl.mem d "_adt" &&
|
||||
(match Hashtbl.find_opt d "_ctor" with Some (String c) -> c = cn | _ -> false))
|
||||
| _ -> Bool false)
|
||||
| _ -> Bool false)));
|
||||
List.iteri (fun idx fname ->
|
||||
env_bind_v (cn ^ "-" ^ fname)
|
||||
(NativeFn (cn ^ "-" ^ fname, fun pargs ->
|
||||
(match pargs with
|
||||
| [v] ->
|
||||
(match v with
|
||||
| Dict d ->
|
||||
(match Hashtbl.find_opt d "_fields" with
|
||||
| Some (List fs) ->
|
||||
if idx < List.length fs then List.nth fs idx
|
||||
else raise (Eval_error (cn ^ "-" ^ fname ^ ": index out of bounds"))
|
||||
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not an ADT")))
|
||||
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not a dict")))
|
||||
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": expected 1 arg")))))
|
||||
) field_names
|
||||
| _ -> ())
|
||||
) ctor_specs;
|
||||
Nil
|
||||
|
||||
(* Register define-type via custom_special_forms so the CEK dispatch finds it.
|
||||
The top-level (register-special-form! ...) in spec/evaluator.sx is not a
|
||||
define and therefore is not transpiled; we wire it up here instead. *)
|
||||
let () = ignore (register_special_form (String "define-type")
|
||||
(NativeFn ("define-type", fun call_args ->
|
||||
match call_args with
|
||||
| [args; env] -> sf_define_type args env
|
||||
| _ -> Nil)))
|
||||
|
||||
|
||||
"""
|
||||
@@ -171,7 +257,10 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str:
|
||||
"debug-log", "debug_log", "range", "chunk-every", "zip-pairs",
|
||||
"string-contains?", "starts-with?", "ends-with?",
|
||||
"string-replace", "trim", "split", "index-of",
|
||||
"pad-left", "pad-right", "char-at", "substring"}
|
||||
"pad-left", "pad-right", "char-at", "substring",
|
||||
# sf-define-type uses &rest + empty-dict literals that the transpiler
|
||||
# can't emit as valid OCaml; hand-written implementation in FIXUPS.
|
||||
"sf-define-type"}
|
||||
defines = [(n, e) for n, e in defines if n not in skip]
|
||||
|
||||
# Deduplicate — keep last definition for each name (CEK overrides tree-walk)
|
||||
|
||||
@@ -89,10 +89,38 @@ let read_symbol s =
|
||||
while s.pos < s.len && is_symbol_char s.src.[s.pos] do advance s done;
|
||||
String.sub s.src start (s.pos - start)
|
||||
|
||||
let gcd a b =
|
||||
let rec g a b = if b = 0 then a else g b (a mod b) in g (abs a) (abs b)
|
||||
|
||||
let make_rat n d =
|
||||
if d = 0 then raise (Parse_error "rational: division by zero");
|
||||
let sign = if d < 0 then -1 else 1 in
|
||||
let g = gcd (abs n) (abs d) in
|
||||
let rn = sign * n / g and rd = sign * d / g in
|
||||
if rd = 1 then Integer rn else Rational (rn, rd)
|
||||
|
||||
let try_number str =
|
||||
match float_of_string_opt str with
|
||||
| Some n -> Some (Number n)
|
||||
| None -> None
|
||||
(* Integers (no '.' or 'e'/'E') → exact Integer; rationals N/D; floats → inexact Number *)
|
||||
let has_dec = String.contains str '.' in
|
||||
let has_exp = String.contains str 'e' || String.contains str 'E' in
|
||||
if has_dec || has_exp then
|
||||
match float_of_string_opt str with
|
||||
| Some n -> Some (Number n)
|
||||
| None -> None
|
||||
else
|
||||
match String.split_on_char '/' str with
|
||||
| [num_s; den_s] when num_s <> "" && den_s <> "" ->
|
||||
(match int_of_string_opt num_s, int_of_string_opt den_s with
|
||||
| Some n, Some d -> (try Some (make_rat n d) with _ -> None)
|
||||
| _ -> None)
|
||||
| _ ->
|
||||
match int_of_string_opt str with
|
||||
| Some n -> Some (Integer n)
|
||||
| None ->
|
||||
(* handles "nan", "inf", "-inf" *)
|
||||
match float_of_string_opt str with
|
||||
| Some n -> Some (Number n)
|
||||
| None -> None
|
||||
|
||||
let rec read_value s : value =
|
||||
skip_whitespace_and_comments s;
|
||||
@@ -108,6 +136,34 @@ let rec read_value s : value =
|
||||
| '"' -> String (read_string s)
|
||||
| '\'' -> advance s; List [Symbol "quote"; read_value s]
|
||||
| '`' -> advance s; List [Symbol "quasiquote"; read_value s]
|
||||
| '#' when s.pos + 1 < s.len && s.src.[s.pos + 1] = '\\' ->
|
||||
(* Character literal: #\a, #\space, #\newline, etc. *)
|
||||
advance s; advance s;
|
||||
if at_end s then raise (Parse_error "Unexpected end of input after #\\");
|
||||
let char_start = s.pos in
|
||||
(* Read a name if starts with ident char, else single char *)
|
||||
if is_ident_start s.src.[s.pos] then begin
|
||||
while s.pos < s.len && is_ident_char s.src.[s.pos] do advance s done;
|
||||
let name = String.sub s.src char_start (s.pos - char_start) in
|
||||
let cp = match name with
|
||||
| "space" -> 32 | "newline" -> 10 | "tab" -> 9
|
||||
| "return" -> 13 | "nul" -> 0 | "null" -> 0
|
||||
| "escape" -> 27 | "delete" -> 127 | "backspace" -> 8
|
||||
| "altmode" -> 27 | "rubout" -> 127
|
||||
| _ -> Char.code name.[0] (* single letter like #\a *)
|
||||
in Char cp
|
||||
end else begin
|
||||
let c = s.src.[s.pos] in
|
||||
advance s;
|
||||
Char (Char.code c)
|
||||
end
|
||||
| '#' when s.pos + 1 < s.len &&
|
||||
(s.src.[s.pos + 1] = 't' || s.src.[s.pos + 1] = 'f') &&
|
||||
(s.pos + 2 >= s.len || not (is_ident_char s.src.[s.pos + 2])) ->
|
||||
(* #t / #f — boolean literals (R7RS shorthand) *)
|
||||
let b = s.src.[s.pos + 1] = 't' in
|
||||
advance s; advance s;
|
||||
Bool b
|
||||
| '#' when s.pos + 1 < s.len && s.src.[s.pos + 1] = ';' ->
|
||||
(* Datum comment: #; discards next expression *)
|
||||
advance s; advance s;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@ let sx_call f args =
|
||||
!Sx_types._cek_eval_lambda_ref f args
|
||||
| Continuation (k, _) ->
|
||||
k (match args with x :: _ -> x | [] -> Nil)
|
||||
| CallccContinuation _ ->
|
||||
| CallccContinuation (_, _) ->
|
||||
raise (Eval_error "callcc continuations must be invoked through the CEK machine")
|
||||
| _ ->
|
||||
let nargs = List.length args in
|
||||
@@ -156,6 +156,9 @@ let get_val container key =
|
||||
| "extra" -> f.cf_extra | "extra2" -> f.cf_extra2
|
||||
| "subscribers" -> f.cf_results
|
||||
| "prev-tracking" -> f.cf_extra
|
||||
| "after-thunk" -> f.cf_f (* wind-after frame *)
|
||||
| "winders-len" -> f.cf_extra (* wind-after frame *)
|
||||
| "body-result" -> f.cf_name (* wind-return frame *)
|
||||
| _ -> Nil)
|
||||
| VmFrame f, String k ->
|
||||
(match k with
|
||||
@@ -208,6 +211,8 @@ let get_val container key =
|
||||
| Dict d, Keyword k -> dict_get d k
|
||||
| (List l | ListRef { contents = l }), Number n ->
|
||||
(try List.nth l (int_of_float n) with _ -> Nil)
|
||||
| (List l | ListRef { contents = l }), Integer n ->
|
||||
(try List.nth l n with _ -> Nil)
|
||||
| Nil, _ -> Nil (* nil.anything → nil *)
|
||||
| _, _ -> Nil (* type mismatch → nil (matches JS/Python behavior) *)
|
||||
|
||||
@@ -381,15 +386,20 @@ let continuation_data v = match v with
|
||||
| _ -> raise (Eval_error "not a continuation")
|
||||
|
||||
(* Callcc (undelimited) continuation support *)
|
||||
let callcc_continuation_p v = match v with CallccContinuation _ -> Bool true | _ -> Bool false
|
||||
let callcc_continuation_p v = match v with CallccContinuation (_, _) -> Bool true | _ -> Bool false
|
||||
|
||||
let make_callcc_continuation captured =
|
||||
CallccContinuation (sx_to_list captured)
|
||||
let make_callcc_continuation captured winders_len =
|
||||
let n = match winders_len with Number f -> int_of_float f | Integer n -> n | _ -> 0 in
|
||||
CallccContinuation (sx_to_list captured, n)
|
||||
|
||||
let callcc_continuation_data v = match v with
|
||||
| CallccContinuation frames -> List frames
|
||||
| CallccContinuation (frames, _) -> List frames
|
||||
| _ -> raise (Eval_error "not a callcc continuation")
|
||||
|
||||
let callcc_continuation_winders_len v = match v with
|
||||
| CallccContinuation (_, n) -> Number (float_of_int n)
|
||||
| _ -> Number 0.0
|
||||
|
||||
(* Dynamic wind — simplified for OCaml (no async) *)
|
||||
let host_error msg =
|
||||
raise (Eval_error (value_to_str msg))
|
||||
|
||||
@@ -43,9 +43,10 @@ type env = {
|
||||
|
||||
and value =
|
||||
| Nil
|
||||
| Bool of bool
|
||||
| Number of float
|
||||
| String of string
|
||||
| Bool of bool
|
||||
| Integer of int (** Exact integer — distinct from inexact float. *)
|
||||
| Number of float (** Inexact float. *)
|
||||
| String of string
|
||||
| Symbol of string
|
||||
| Keyword of string
|
||||
| List of value list
|
||||
@@ -56,7 +57,7 @@ and value =
|
||||
| Macro of macro
|
||||
| Thunk of value * env
|
||||
| Continuation of (value -> value) * dict option
|
||||
| CallccContinuation of value list (** Undelimited continuation — captured kont frames *)
|
||||
| CallccContinuation of value list * int (** Undelimited continuation — captured kont frames + winders depth at capture *)
|
||||
| NativeFn of string * (value list -> value)
|
||||
| Signal of signal
|
||||
| RawHTML of string
|
||||
@@ -72,6 +73,25 @@ and value =
|
||||
| Record of record (** R7RS record — opaque, generative, field-indexed. *)
|
||||
| Parameter of parameter (** R7RS parameter — dynamic binding via kont-stack provide frames. *)
|
||||
| Vector of value array (** R7RS vector — mutable fixed-size array. *)
|
||||
| StringBuffer of Buffer.t (** Mutable string buffer — O(1) amortized append. *)
|
||||
| HashTable of (value, value) Hashtbl.t (** Mutable hash table with arbitrary keys. *)
|
||||
| Char of int (** Unicode codepoint — R7RS char type. *)
|
||||
| Eof (** EOF sentinel — returned by read-char etc. at end of input. *)
|
||||
| Port of sx_port (** String port — input (string cursor) or output (buffer). *)
|
||||
| Rational of int * int (** Exact rational: numerator, denominator (reduced, denom>0). *)
|
||||
| SxSet of (string, value) Hashtbl.t (** Mutable set keyed by inspect(value). *)
|
||||
| SxRegexp of string * string * Re.re (** Regexp: source, flags, compiled. *)
|
||||
| SxBytevector of bytes (** Mutable bytevector — R7RS bytevector type. *)
|
||||
|
||||
(** String input port: source string + mutable cursor position. *)
|
||||
and sx_port_kind =
|
||||
| PortInput of string * int ref
|
||||
| PortOutput of Buffer.t
|
||||
|
||||
and sx_port = {
|
||||
mutable sp_closed : bool;
|
||||
sp_kind : sx_port_kind;
|
||||
}
|
||||
|
||||
(** CEK machine state — record instead of Dict for performance.
|
||||
5 fields × 55K steps/sec = 275K Hashtbl allocations/sec eliminated. *)
|
||||
@@ -392,6 +412,7 @@ let format_number n =
|
||||
|
||||
let value_to_string = function
|
||||
| String s -> s | Symbol s -> s | Keyword k -> k
|
||||
| Integer n -> string_of_int n
|
||||
| Number n -> format_number n
|
||||
| Bool true -> "true" | Bool false -> "false"
|
||||
| Nil -> "" | _ -> "<value>"
|
||||
@@ -461,6 +482,7 @@ let make_keyword name = Keyword (value_to_string name)
|
||||
let type_of = function
|
||||
| Nil -> "nil"
|
||||
| Bool _ -> "boolean"
|
||||
| Integer _ -> "number"
|
||||
| Number _ -> "number"
|
||||
| String _ -> "string"
|
||||
| Symbol _ -> "symbol"
|
||||
@@ -473,7 +495,7 @@ let type_of = function
|
||||
| Macro _ -> "macro"
|
||||
| Thunk _ -> "thunk"
|
||||
| Continuation (_, _) -> "continuation"
|
||||
| CallccContinuation _ -> "continuation"
|
||||
| CallccContinuation (_, _) -> "continuation"
|
||||
| NativeFn _ -> "function"
|
||||
| Signal _ -> "signal"
|
||||
| RawHTML _ -> "raw-html"
|
||||
@@ -488,6 +510,16 @@ let type_of = function
|
||||
| Record r -> r.r_type.rt_name
|
||||
| Parameter _ -> "parameter"
|
||||
| Vector _ -> "vector"
|
||||
| StringBuffer _ -> "string-buffer"
|
||||
| HashTable _ -> "hash-table"
|
||||
| Char _ -> "char"
|
||||
| Eof -> "eof-object"
|
||||
| Port { sp_kind = PortInput _; _ } -> "input-port"
|
||||
| Port { sp_kind = PortOutput _; _ } -> "output-port"
|
||||
| Rational _ -> "rational"
|
||||
| SxSet _ -> "set"
|
||||
| SxRegexp _ -> "regexp"
|
||||
| SxBytevector _ -> "bytevector"
|
||||
|
||||
let is_nil = function Nil -> true | _ -> false
|
||||
let is_lambda = function Lambda _ -> true | _ -> false
|
||||
@@ -503,7 +535,7 @@ let is_signal = function
|
||||
let is_record = function Record _ -> true | _ -> false
|
||||
|
||||
let is_callable = function
|
||||
| Lambda _ | NativeFn _ | Continuation (_, _) | CallccContinuation _ | VmClosure _ -> true
|
||||
| Lambda _ | NativeFn _ | Continuation (_, _) | CallccContinuation (_, _) | VmClosure _ -> true
|
||||
| _ -> false
|
||||
|
||||
|
||||
@@ -616,6 +648,7 @@ let thunk_env = function
|
||||
(** {1 Record operations} *)
|
||||
|
||||
let val_to_int = function
|
||||
| Integer n -> n
|
||||
| Number n -> int_of_float n
|
||||
| v -> raise (Eval_error ("Expected number, got " ^ type_of v))
|
||||
|
||||
@@ -777,6 +810,7 @@ let rec inspect = function
|
||||
| Nil -> "nil"
|
||||
| Bool true -> "true"
|
||||
| Bool false -> "false"
|
||||
| Integer n -> string_of_int n
|
||||
| Number n -> format_number n
|
||||
| String s ->
|
||||
let buf = Buffer.create (String.length s + 2) in
|
||||
@@ -810,7 +844,7 @@ let rec inspect = function
|
||||
Printf.sprintf "<%s(%s)>" tag (String.concat ", " m.m_params)
|
||||
| Thunk _ -> "<thunk>"
|
||||
| Continuation (_, _) -> "<continuation>"
|
||||
| CallccContinuation _ -> "<callcc-continuation>"
|
||||
| CallccContinuation (_, _) -> "<callcc-continuation>"
|
||||
| NativeFn (name, _) -> Printf.sprintf "<native:%s>" name
|
||||
| Signal _ -> "<signal>"
|
||||
| RawHTML s -> Printf.sprintf "\"<raw-html:%d>\"" (String.length s)
|
||||
@@ -831,3 +865,23 @@ let rec inspect = function
|
||||
Printf.sprintf "#(%s)" (String.concat " " elts)
|
||||
| VmFrame f -> Printf.sprintf "<vm-frame:ip=%d base=%d>" f.vf_ip f.vf_base
|
||||
| VmMachine m -> Printf.sprintf "<vm-machine:sp=%d frames=%d>" m.vm_sp (List.length m.vm_frames)
|
||||
| StringBuffer buf -> Printf.sprintf "<string-buffer:%d>" (Buffer.length buf)
|
||||
| HashTable ht -> Printf.sprintf "<hash-table:%d>" (Hashtbl.length ht)
|
||||
| Char n ->
|
||||
let name = match n with
|
||||
| 32 -> "space" | 10 -> "newline" | 9 -> "tab"
|
||||
| 13 -> "return" | 0 -> "nul" | 27 -> "escape"
|
||||
| 127 -> "delete" | 8 -> "backspace"
|
||||
| _ -> let buf = Buffer.create 1 in
|
||||
Buffer.add_utf_8_uchar buf (Uchar.of_int n);
|
||||
Buffer.contents buf
|
||||
in "#\\" ^ name
|
||||
| Eof -> "#!eof"
|
||||
| Port { sp_kind = PortInput (_, pos); sp_closed } ->
|
||||
Printf.sprintf "<input-port:pos=%d%s>" !pos (if sp_closed then ":closed" else "")
|
||||
| Port { sp_kind = PortOutput buf; sp_closed } ->
|
||||
Printf.sprintf "<output-port:len=%d%s>" (Buffer.length buf) (if sp_closed then ":closed" else "")
|
||||
| Rational (n, d) -> Printf.sprintf "%d/%d" n d
|
||||
| SxSet ht -> Printf.sprintf "<set:%d>" (Hashtbl.length ht)
|
||||
| SxRegexp (src, flags, _) -> Printf.sprintf "#/%s/%s" src flags
|
||||
| SxBytevector b -> Printf.sprintf "#u8(%s)" (String.concat " " (List.init (Bytes.length b) (fun i -> string_of_int (Char.code (Bytes.get b i)))))
|
||||
|
||||
@@ -185,7 +185,8 @@ let code_from_value v =
|
||||
| Some _ as r -> r | None -> Hashtbl.find_opt d k2 in
|
||||
let bc_list = match find2 "bytecode" "vc-bytecode" with
|
||||
| Some (List l | ListRef { contents = l }) ->
|
||||
Array.of_list (List.map (fun x -> match x with Number n -> int_of_float n | _ -> 0) l)
|
||||
Array.of_list (List.map (fun x -> match x with
|
||||
| Integer n -> n | Number n -> int_of_float n | _ -> 0) l)
|
||||
| _ -> [||]
|
||||
in
|
||||
let entries = match find2 "constants" "vc-constants" with
|
||||
@@ -198,10 +199,10 @@ let code_from_value v =
|
||||
| _ -> entry
|
||||
) entries in
|
||||
let arity = match find2 "arity" "vc-arity" with
|
||||
| Some (Number n) -> int_of_float n | _ -> 0
|
||||
| Some (Integer n) -> n | Some (Number n) -> int_of_float n | _ -> 0
|
||||
in
|
||||
let rest_arity = match find2 "rest-arity" "vc-rest-arity" with
|
||||
| Some (Number n) -> int_of_float n | _ -> -1
|
||||
| Some (Integer n) -> n | Some (Number n) -> int_of_float n | _ -> -1
|
||||
in
|
||||
(* Compute locals from bytecode: scan for highest LOCAL_GET/LOCAL_SET slot.
|
||||
The compiler's arity may undercount when nested lets add many locals. *)
|
||||
@@ -749,10 +750,7 @@ and run vm =
|
||||
| _ -> (Hashtbl.find Sx_primitives.primitives "/") [a; b])
|
||||
| 164 (* OP_EQ *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
let rec norm = function
|
||||
| ListRef { contents = l } -> List (List.map norm l)
|
||||
| List l -> List (List.map norm l) | v -> v in
|
||||
push vm (Bool (norm a = norm b))
|
||||
push vm ((Hashtbl.find Sx_primitives.primitives "=") [a; b])
|
||||
| 165 (* OP_LT *) ->
|
||||
let b = pop vm and a = pop vm in
|
||||
push vm (match a, b with
|
||||
@@ -771,10 +769,10 @@ and run vm =
|
||||
| 168 (* OP_LEN *) ->
|
||||
let v = pop vm in
|
||||
push vm (match v with
|
||||
| List l | ListRef { contents = l } -> Number (float_of_int (List.length l))
|
||||
| String s -> Number (float_of_int (String.length s))
|
||||
| Dict d -> Number (float_of_int (Hashtbl.length d))
|
||||
| Nil -> Number 0.0
|
||||
| List l | ListRef { contents = l } -> Integer (List.length l)
|
||||
| String s -> Integer (String.length s)
|
||||
| Dict d -> Integer (Hashtbl.length d)
|
||||
| Nil -> Integer 0
|
||||
| _ -> (Hashtbl.find Sx_primitives.primitives "len") [v])
|
||||
| 169 (* OP_FIRST *) ->
|
||||
let v = pop vm in
|
||||
|
||||
@@ -256,6 +256,7 @@
|
||||
"callcc-continuation?"
|
||||
"callcc-continuation-data"
|
||||
"make-callcc-continuation"
|
||||
"callcc-continuation-winders-len"
|
||||
"dynamic-wind-call"
|
||||
"strip-prefix"
|
||||
"component-set-param-types!"
|
||||
@@ -295,7 +296,8 @@
|
||||
"*bind-tracking*"
|
||||
"*provide-batch-depth*"
|
||||
"*provide-batch-queue*"
|
||||
"*provide-subscribers*"))
|
||||
"*provide-subscribers*"
|
||||
"*winders*"))
|
||||
|
||||
(define
|
||||
ml-is-mutable-global?
|
||||
@@ -533,13 +535,13 @@
|
||||
"; cf_env = "
|
||||
(ef "env")
|
||||
"; cf_name = "
|
||||
(if (= frame-type "if") (ef "else") (ef "name"))
|
||||
(if (= frame-type "if") (ef "else") (cond (some (fn (k) (= k "body-result")) items) (ef "body-result") :else (ef "name")))
|
||||
"; cf_body = "
|
||||
(if (= frame-type "if") (ef "then") (ef "body"))
|
||||
"; cf_remaining = "
|
||||
(ef "remaining")
|
||||
"; cf_f = "
|
||||
(ef "f")
|
||||
(cond (some (fn (k) (= k "after-thunk")) items) (ef "after-thunk") (some (fn (k) (= k "f")) items) (ef "f") :else "Nil")
|
||||
"; cf_args = "
|
||||
(cond
|
||||
(some (fn (k) (= k "evaled")) items)
|
||||
@@ -582,6 +584,8 @@
|
||||
(ef "prev-tracking")
|
||||
(some (fn (k) (= k "extra")) items)
|
||||
(ef "extra")
|
||||
(some (fn (k) (= k "winders-len")) items)
|
||||
(ef "winders-len")
|
||||
:else "Nil")
|
||||
"; cf_extra2 = "
|
||||
(cond
|
||||
|
||||
289
lib/apl/runtime.sx
Normal file
289
lib/apl/runtime.sx
Normal file
@@ -0,0 +1,289 @@
|
||||
;; lib/apl/runtime.sx — APL primitives on SX
|
||||
;;
|
||||
;; APL vectors are represented as SX lists (functional, immutable results).
|
||||
;; Operations are rank-polymorphic: scalar/vector arguments both accepted.
|
||||
;; Index origin: 1 (traditional APL).
|
||||
;;
|
||||
;; Primitives used:
|
||||
;; map (multi-arg, Phase 1)
|
||||
;; bitwise-and/or/xor/not/arithmetic-shift (Phase 7)
|
||||
;; make-set/set-member?/set-add!/set->list (Phase 18)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Core vector constructors
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; ⍳N — iota: generate integer vector 1, 2, ..., N
|
||||
(define
|
||||
(apl-iota n)
|
||||
(letrec
|
||||
((go (fn (i acc) (if (< i 1) acc (go (- i 1) (cons i acc))))))
|
||||
(go n (list))))
|
||||
|
||||
;; ⍴A — shape (length of a vector)
|
||||
(define (apl-rho v) (if (list? v) (len v) 1))
|
||||
|
||||
;; A[I] — 1-indexed access
|
||||
(define (apl-at v i) (nth v (- i 1)))
|
||||
|
||||
;; Scalar predicate
|
||||
(define (apl-scalar? v) (not (list? v)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Rank-polymorphic helpers
|
||||
;; dyadic: scalar/vector × scalar/vector → scalar/vector
|
||||
;; monadic: scalar/vector → scalar/vector
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(apl-dyadic op a b)
|
||||
(cond
|
||||
((and (list? a) (list? b)) (map op a b))
|
||||
((list? a) (map (fn (x) (op x b)) a))
|
||||
((list? b) (map (fn (y) (op a y)) b))
|
||||
(else (op a b))))
|
||||
|
||||
(define (apl-monadic op a) (if (list? a) (map op a) (op a)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Arithmetic (element-wise, rank-polymorphic)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (apl-add a b) (apl-dyadic + a b))
|
||||
(define (apl-sub a b) (apl-dyadic - a b))
|
||||
(define (apl-mul a b) (apl-dyadic * a b))
|
||||
(define (apl-div a b) (apl-dyadic / a b))
|
||||
(define (apl-mod a b) (apl-dyadic modulo a b))
|
||||
(define (apl-pow a b) (apl-dyadic pow a b))
|
||||
(define (apl-max a b) (apl-dyadic (fn (x y) (if (> x y) x y)) a b))
|
||||
(define (apl-min a b) (apl-dyadic (fn (x y) (if (< x y) x y)) a b))
|
||||
|
||||
(define (apl-neg a) (apl-monadic (fn (x) (- 0 x)) a))
|
||||
(define (apl-abs a) (apl-monadic abs a))
|
||||
(define (apl-floor a) (apl-monadic floor a))
|
||||
(define (apl-ceil a) (apl-monadic ceil a))
|
||||
(define (apl-sqrt a) (apl-monadic sqrt a))
|
||||
(define (apl-exp a) (apl-monadic exp a))
|
||||
(define (apl-log a) (apl-monadic log a))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Comparison (element-wise, returns 0/1 booleans)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (apl-bool v) (if v 1 0))
|
||||
|
||||
(define (apl-eq a b) (apl-dyadic (fn (x y) (apl-bool (= x y))) a b))
|
||||
(define
|
||||
(apl-neq a b)
|
||||
(apl-dyadic (fn (x y) (apl-bool (not (= x y)))) a b))
|
||||
(define (apl-lt a b) (apl-dyadic (fn (x y) (apl-bool (< x y))) a b))
|
||||
(define (apl-le a b) (apl-dyadic (fn (x y) (apl-bool (<= x y))) a b))
|
||||
(define (apl-gt a b) (apl-dyadic (fn (x y) (apl-bool (> x y))) a b))
|
||||
(define (apl-ge a b) (apl-dyadic (fn (x y) (apl-bool (>= x y))) a b))
|
||||
|
||||
;; Boolean logic (0/1 vectors)
|
||||
(define
|
||||
(apl-and a b)
|
||||
(apl-dyadic
|
||||
(fn
|
||||
(x y)
|
||||
(if
|
||||
(and (not (= x 0)) (not (= y 0)))
|
||||
1
|
||||
0))
|
||||
a
|
||||
b))
|
||||
(define
|
||||
(apl-or a b)
|
||||
(apl-dyadic
|
||||
(fn
|
||||
(x y)
|
||||
(if
|
||||
(or (not (= x 0)) (not (= y 0)))
|
||||
1
|
||||
0))
|
||||
a
|
||||
b))
|
||||
(define
|
||||
(apl-not a)
|
||||
(apl-monadic (fn (x) (if (= x 0) 1 0)) a))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Bitwise operations (element-wise)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (apl-bitand a b) (apl-dyadic bitwise-and a b))
|
||||
(define (apl-bitor a b) (apl-dyadic bitwise-or a b))
|
||||
(define (apl-bitxor a b) (apl-dyadic bitwise-xor a b))
|
||||
(define (apl-bitnot a) (apl-monadic bitwise-not a))
|
||||
(define
|
||||
(apl-lshift a b)
|
||||
(apl-dyadic (fn (x n) (arithmetic-shift x n)) a b))
|
||||
(define
|
||||
(apl-rshift a b)
|
||||
(apl-dyadic (fn (x n) (arithmetic-shift x (- 0 n))) a b))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. Reduction (fold) and scan
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (apl-reduce-add v) (reduce + 0 v))
|
||||
(define (apl-reduce-mul v) (reduce * 1 v))
|
||||
(define
|
||||
(apl-reduce-max v)
|
||||
(reduce (fn (acc x) (if (> acc x) acc x)) (first v) (rest v)))
|
||||
(define
|
||||
(apl-reduce-min v)
|
||||
(reduce (fn (acc x) (if (< acc x) acc x)) (first v) (rest v)))
|
||||
(define
|
||||
(apl-reduce-and v)
|
||||
(reduce
|
||||
(fn
|
||||
(acc x)
|
||||
(if
|
||||
(and (not (= acc 0)) (not (= x 0)))
|
||||
1
|
||||
0))
|
||||
1
|
||||
v))
|
||||
(define
|
||||
(apl-reduce-or v)
|
||||
(reduce
|
||||
(fn
|
||||
(acc x)
|
||||
(if
|
||||
(or (not (= acc 0)) (not (= x 0)))
|
||||
1
|
||||
0))
|
||||
0
|
||||
v))
|
||||
|
||||
;; Scan: prefix reduction (yields a vector of running totals)
|
||||
(define
|
||||
(apl-scan op v)
|
||||
(if
|
||||
(= (len v) 0)
|
||||
(list)
|
||||
(letrec
|
||||
((go (fn (xs acc result) (if (= (len xs) 0) (reverse result) (let ((next (op acc (first xs)))) (go (rest xs) next (cons next result)))))))
|
||||
(go (rest v) (first v) (list (first v))))))
|
||||
|
||||
(define (apl-scan-add v) (apl-scan + v))
|
||||
(define (apl-scan-mul v) (apl-scan * v))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 7. Vector manipulation
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; ⌽A — reverse
|
||||
(define (apl-reverse v) (reverse v))
|
||||
|
||||
;; A,B — catenate
|
||||
(define
|
||||
(apl-cat a b)
|
||||
(cond
|
||||
((and (list? a) (list? b)) (append a b))
|
||||
((list? a) (append a (list b)))
|
||||
((list? b) (cons a b))
|
||||
(else (list a b))))
|
||||
|
||||
;; ↑N A — take first N elements (negative: take last N)
|
||||
(define
|
||||
(apl-take n v)
|
||||
(if
|
||||
(>= n 0)
|
||||
(letrec
|
||||
((go (fn (xs i) (if (or (= i 0) (= (len xs) 0)) (list) (cons (first xs) (go (rest xs) (- i 1)))))))
|
||||
(go v n))
|
||||
(apl-reverse (apl-take (- 0 n) (apl-reverse v)))))
|
||||
|
||||
;; ↓N A — drop first N elements
|
||||
(define
|
||||
(apl-drop n v)
|
||||
(if
|
||||
(>= n 0)
|
||||
(letrec
|
||||
((go (fn (xs i) (if (or (= i 0) (= (len xs) 0)) xs (go (rest xs) (- i 1))))))
|
||||
(go v n))
|
||||
(apl-reverse (apl-drop (- 0 n) (apl-reverse v)))))
|
||||
|
||||
;; Rotate left by n positions
|
||||
(define
|
||||
(apl-rotate n v)
|
||||
(let ((m (modulo n (len v)))) (append (apl-drop m v) (apl-take m v))))
|
||||
|
||||
;; Compression: A/B — select elements of B where A is 1
|
||||
(define
|
||||
(apl-compress mask v)
|
||||
(if
|
||||
(= (len mask) 0)
|
||||
(list)
|
||||
(let
|
||||
((rest-result (apl-compress (rest mask) (rest v))))
|
||||
(if
|
||||
(not (= (first mask) 0))
|
||||
(cons (first v) rest-result)
|
||||
rest-result))))
|
||||
|
||||
;; Indexing: A[B] — select elements at indices B (1-indexed)
|
||||
(define (apl-index v indices) (map (fn (i) (apl-at v i)) indices))
|
||||
|
||||
;; Grade up: indices that would sort the vector ascending
|
||||
(define
|
||||
(apl-grade-up v)
|
||||
(let
|
||||
((indexed (map (fn (x i) (list x i)) v (apl-iota (len v)))))
|
||||
(map (fn (p) (nth p 1)) (sort indexed))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 8. Set operations (∊ ∪ ∩ ~)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Membership ∊: for each element in A, is it in B? → 0/1 vector
|
||||
(define
|
||||
(apl-member a b)
|
||||
(let
|
||||
((bset (let ((s (make-set))) (for-each (fn (x) (set-add! s x)) b) s)))
|
||||
(if
|
||||
(list? a)
|
||||
(map (fn (x) (apl-bool (set-member? bset x))) a)
|
||||
(apl-bool (set-member? bset a)))))
|
||||
|
||||
;; Nub ∪A — unique elements, preserving order
|
||||
(define
|
||||
(apl-nub v)
|
||||
(let
|
||||
((seen (make-set)))
|
||||
(letrec
|
||||
((go (fn (xs acc) (if (= (len xs) 0) (reverse acc) (if (set-member? seen (first xs)) (go (rest xs) acc) (begin (set-add! seen (first xs)) (go (rest xs) (cons (first xs) acc))))))))
|
||||
(go v (list)))))
|
||||
|
||||
;; Union A∪B — nub of concatenation
|
||||
(define (apl-union a b) (apl-nub (apl-cat a b)))
|
||||
|
||||
;; Intersection A∩B
|
||||
(define
|
||||
(apl-intersect a b)
|
||||
(let
|
||||
((bset (let ((s (make-set))) (for-each (fn (x) (set-add! s x)) b) s)))
|
||||
(filter (fn (x) (set-member? bset x)) a)))
|
||||
|
||||
;; Without A~B
|
||||
(define
|
||||
(apl-without a b)
|
||||
(let
|
||||
((bset (let ((s (make-set))) (for-each (fn (x) (set-add! s x)) b) s)))
|
||||
(filter (fn (x) (not (set-member? bset x))) a)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 9. Format (⍕) — APL-style display
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(apl-format v)
|
||||
(if
|
||||
(list? v)
|
||||
(letrec
|
||||
((go (fn (xs acc) (if (= (len xs) 0) acc (go (rest xs) (str acc (if (= acc "") "" " ") (str (first xs))))))))
|
||||
(go v ""))
|
||||
(str v)))
|
||||
51
lib/apl/test.sh
Executable file
51
lib/apl/test.sh
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/apl/test.sh — smoke-test the APL runtime layer.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "spec/stdlib.sx")
|
||||
(load "lib/apl/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/apl/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(list apl-test-pass apl-test-fail)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}')
|
||||
if [ -z "$LINE" ]; then
|
||||
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 3 //; s/\)$//')
|
||||
fi
|
||||
if [ -z "$LINE" ]; then
|
||||
echo "ERROR: could not extract summary"
|
||||
echo "$OUTPUT" | tail -10
|
||||
exit 1
|
||||
fi
|
||||
|
||||
P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/')
|
||||
F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/')
|
||||
TOTAL=$((P + F))
|
||||
|
||||
if [ "$F" -eq 0 ]; then
|
||||
echo "ok $P/$TOTAL lib/apl tests passed"
|
||||
else
|
||||
echo "FAIL $P/$TOTAL passed, $F failed"
|
||||
fi
|
||||
|
||||
[ "$F" -eq 0 ]
|
||||
327
lib/apl/tests/runtime.sx
Normal file
327
lib/apl/tests/runtime.sx
Normal file
@@ -0,0 +1,327 @@
|
||||
;; lib/apl/tests/runtime.sx — Tests for lib/apl/runtime.sx
|
||||
|
||||
;; --- Test framework ---
|
||||
(define apl-test-pass 0)
|
||||
(define apl-test-fail 0)
|
||||
(define apl-test-fails (list))
|
||||
|
||||
(define
|
||||
(apl-test name got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! apl-test-pass (+ apl-test-pass 1))
|
||||
(begin
|
||||
(set! apl-test-fail (+ apl-test-fail 1))
|
||||
(set! apl-test-fails (append apl-test-fails (list {:got got :expected expected :name name}))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Core vector constructors
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"iota 5"
|
||||
(apl-iota 5)
|
||||
(list 1 2 3 4 5))
|
||||
(apl-test "iota 1" (apl-iota 1) (list 1))
|
||||
(apl-test "iota 0" (apl-iota 0) (list))
|
||||
(apl-test
|
||||
"rho list"
|
||||
(apl-rho (list 1 2 3))
|
||||
3)
|
||||
(apl-test "rho scalar" (apl-rho 42) 1)
|
||||
(apl-test
|
||||
"at 1"
|
||||
(apl-at (list 10 20 30) 1)
|
||||
10)
|
||||
(apl-test
|
||||
"at 3"
|
||||
(apl-at (list 10 20 30) 3)
|
||||
30)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Arithmetic — element-wise and rank-polymorphic
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"add v+v"
|
||||
(apl-add
|
||||
(list 1 2 3)
|
||||
(list 10 20 30))
|
||||
(list 11 22 33))
|
||||
(apl-test
|
||||
"add s+v"
|
||||
(apl-add 10 (list 1 2 3))
|
||||
(list 11 12 13))
|
||||
(apl-test
|
||||
"add v+s"
|
||||
(apl-add (list 1 2 3) 100)
|
||||
(list 101 102 103))
|
||||
(apl-test "add s+s" (apl-add 3 4) 7)
|
||||
(apl-test
|
||||
"sub v-v"
|
||||
(apl-sub
|
||||
(list 5 4 3)
|
||||
(list 1 2 3))
|
||||
(list 4 2 0))
|
||||
(apl-test
|
||||
"mul v*s"
|
||||
(apl-mul (list 1 2 3) 3)
|
||||
(list 3 6 9))
|
||||
(apl-test
|
||||
"neg -v"
|
||||
(apl-neg (list 1 -2 3))
|
||||
(list -1 2 -3))
|
||||
(apl-test
|
||||
"abs v"
|
||||
(apl-abs (list -1 2 -3))
|
||||
(list 1 2 3))
|
||||
(apl-test
|
||||
"floor v"
|
||||
(apl-floor (list 1.7 2.2 3.9))
|
||||
(list 1 2 3))
|
||||
(apl-test
|
||||
"ceil v"
|
||||
(apl-ceil (list 1.1 2.5 3))
|
||||
(list 2 3 3))
|
||||
(apl-test
|
||||
"max v v"
|
||||
(apl-max
|
||||
(list 1 5 3)
|
||||
(list 4 2 6))
|
||||
(list 4 5 6))
|
||||
(apl-test
|
||||
"min v v"
|
||||
(apl-min
|
||||
(list 1 5 3)
|
||||
(list 4 2 6))
|
||||
(list 1 2 3))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Comparison (returns 0/1)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test "eq 3 3" (apl-eq 3 3) 1)
|
||||
(apl-test "eq 3 4" (apl-eq 3 4) 0)
|
||||
(apl-test
|
||||
"gt v>s"
|
||||
(apl-gt (list 1 5 3 7) 4)
|
||||
(list 0 1 0 1))
|
||||
(apl-test
|
||||
"lt v<v"
|
||||
(apl-lt
|
||||
(list 1 2 3)
|
||||
(list 3 2 1))
|
||||
(list 1 0 0))
|
||||
(apl-test
|
||||
"le v<=s"
|
||||
(apl-le (list 3 4 5) 4)
|
||||
(list 1 1 0))
|
||||
(apl-test
|
||||
"ge v>=s"
|
||||
(apl-ge (list 3 4 5) 4)
|
||||
(list 0 1 1))
|
||||
(apl-test
|
||||
"neq v!=s"
|
||||
(apl-neq (list 1 2 3) 2)
|
||||
(list 1 0 1))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Boolean logic (0/1 values)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test "and 1 1" (apl-and 1 1) 1)
|
||||
(apl-test "and 1 0" (apl-and 1 0) 0)
|
||||
(apl-test "or 0 1" (apl-or 0 1) 1)
|
||||
(apl-test "or 0 0" (apl-or 0 0) 0)
|
||||
(apl-test "not 0" (apl-not 0) 1)
|
||||
(apl-test "not 1" (apl-not 1) 0)
|
||||
(apl-test
|
||||
"not vec"
|
||||
(apl-not (list 1 0 1 0))
|
||||
(list 0 1 0 1))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Bitwise operations
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test "bitand s" (apl-bitand 5 3) 1)
|
||||
(apl-test "bitor s" (apl-bitor 5 3) 7)
|
||||
(apl-test "bitxor s" (apl-bitxor 5 3) 6)
|
||||
(apl-test "bitnot 0" (apl-bitnot 0) -1)
|
||||
(apl-test "lshift 1 4" (apl-lshift 1 4) 16)
|
||||
(apl-test "rshift 16 2" (apl-rshift 16 2) 4)
|
||||
(apl-test
|
||||
"bitand vec"
|
||||
(apl-bitand (list 5 6) (list 3 7))
|
||||
(list 1 6))
|
||||
(apl-test
|
||||
"bitor vec"
|
||||
(apl-bitor (list 5 6) (list 3 7))
|
||||
(list 7 7))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. Reduction and scan
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"reduce-add"
|
||||
(apl-reduce-add
|
||||
(list 1 2 3 4 5))
|
||||
15)
|
||||
(apl-test
|
||||
"reduce-mul"
|
||||
(apl-reduce-mul (list 1 2 3 4))
|
||||
24)
|
||||
(apl-test
|
||||
"reduce-max"
|
||||
(apl-reduce-max
|
||||
(list 3 1 4 1 5))
|
||||
5)
|
||||
(apl-test
|
||||
"reduce-min"
|
||||
(apl-reduce-min
|
||||
(list 3 1 4 1 5))
|
||||
1)
|
||||
(apl-test
|
||||
"reduce-and"
|
||||
(apl-reduce-and (list 1 1 1))
|
||||
1)
|
||||
(apl-test
|
||||
"reduce-and0"
|
||||
(apl-reduce-and (list 1 0 1))
|
||||
0)
|
||||
(apl-test
|
||||
"reduce-or"
|
||||
(apl-reduce-or (list 0 1 0))
|
||||
1)
|
||||
(apl-test
|
||||
"scan-add"
|
||||
(apl-scan-add (list 1 2 3 4))
|
||||
(list 1 3 6 10))
|
||||
(apl-test
|
||||
"scan-mul"
|
||||
(apl-scan-mul (list 1 2 3 4))
|
||||
(list 1 2 6 24))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 7. Vector manipulation
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"reverse"
|
||||
(apl-reverse (list 1 2 3 4))
|
||||
(list 4 3 2 1))
|
||||
(apl-test
|
||||
"cat v v"
|
||||
(apl-cat (list 1 2) (list 3 4))
|
||||
(list 1 2 3 4))
|
||||
(apl-test
|
||||
"cat v s"
|
||||
(apl-cat (list 1 2) 3)
|
||||
(list 1 2 3))
|
||||
(apl-test
|
||||
"cat s v"
|
||||
(apl-cat 1 (list 2 3))
|
||||
(list 1 2 3))
|
||||
(apl-test
|
||||
"cat s s"
|
||||
(apl-cat 1 2)
|
||||
(list 1 2))
|
||||
(apl-test
|
||||
"take 3"
|
||||
(apl-take
|
||||
3
|
||||
(list 10 20 30 40 50))
|
||||
(list 10 20 30))
|
||||
(apl-test
|
||||
"take 0"
|
||||
(apl-take 0 (list 1 2 3))
|
||||
(list))
|
||||
(apl-test
|
||||
"take neg"
|
||||
(apl-take -2 (list 10 20 30))
|
||||
(list 20 30))
|
||||
(apl-test
|
||||
"drop 2"
|
||||
(apl-drop 2 (list 10 20 30 40))
|
||||
(list 30 40))
|
||||
(apl-test
|
||||
"drop neg"
|
||||
(apl-drop -1 (list 10 20 30))
|
||||
(list 10 20))
|
||||
(apl-test
|
||||
"rotate 2"
|
||||
(apl-rotate
|
||||
2
|
||||
(list 1 2 3 4 5))
|
||||
(list 3 4 5 1 2))
|
||||
(apl-test
|
||||
"compress"
|
||||
(apl-compress
|
||||
(list 1 0 1 0)
|
||||
(list 10 20 30 40))
|
||||
(list 10 30))
|
||||
(apl-test
|
||||
"index"
|
||||
(apl-index
|
||||
(list 10 20 30 40)
|
||||
(list 2 4))
|
||||
(list 20 40))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 8. Set operations
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"member yes"
|
||||
(apl-member
|
||||
(list 1 2 5)
|
||||
(list 2 4 6))
|
||||
(list 0 1 0))
|
||||
(apl-test
|
||||
"member s"
|
||||
(apl-member 2 (list 1 2 3))
|
||||
1)
|
||||
(apl-test
|
||||
"member no"
|
||||
(apl-member 9 (list 1 2 3))
|
||||
0)
|
||||
(apl-test
|
||||
"nub"
|
||||
(apl-nub (list 1 2 1 3 2))
|
||||
(list 1 2 3))
|
||||
(apl-test
|
||||
"union"
|
||||
(apl-union
|
||||
(list 1 2 3)
|
||||
(list 2 3 4))
|
||||
(list 1 2 3 4))
|
||||
(apl-test
|
||||
"intersect"
|
||||
(apl-intersect
|
||||
(list 1 2 3 4)
|
||||
(list 2 4 6))
|
||||
(list 2 4))
|
||||
(apl-test
|
||||
"without"
|
||||
(apl-without
|
||||
(list 1 2 3 4)
|
||||
(list 2 4))
|
||||
(list 1 3))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 9. Format
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(apl-test
|
||||
"format vec"
|
||||
(apl-format (list 1 2 3))
|
||||
"1 2 3")
|
||||
(apl-test "format scalar" (apl-format 42) "42")
|
||||
(apl-test "format empty" (apl-format (list)) "")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Summary
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(list apl-test-pass apl-test-fail)
|
||||
306
lib/common-lisp/runtime.sx
Normal file
306
lib/common-lisp/runtime.sx
Normal file
@@ -0,0 +1,306 @@
|
||||
;; lib/common-lisp/runtime.sx — CL built-ins using SX spec primitives
|
||||
;;
|
||||
;; Provides CL-specific wrappers and helpers. Deliberately thin: wherever
|
||||
;; an SX spec primitive already does the job, we alias it rather than
|
||||
;; reinventing it.
|
||||
;;
|
||||
;; Primitives used from spec:
|
||||
;; char/char->integer/integer->char/char-upcase/char-downcase
|
||||
;; format (Phase 21 — must be loaded before this file)
|
||||
;; gensym (Phase 12)
|
||||
;; rational/rational? (Phase 16)
|
||||
;; make-set/set-member?/set-union/etc (Phase 18)
|
||||
;; open-input-string/read-char/etc (Phase 14)
|
||||
;; modulo/remainder/quotient/gcd/lcm/expt (Phase 2 / Phase 15)
|
||||
;; number->string with radix (Phase 15)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Type predicates
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (cl-null? x) (= x nil))
|
||||
(define (cl-consp? x) (and (list? x) (not (cl-empty? x))))
|
||||
(define (cl-listp? x) (or (cl-empty? x) (list? x)))
|
||||
(define (cl-atom? x) (not (cl-consp? x)))
|
||||
|
||||
(define
|
||||
(cl-numberp? x)
|
||||
(let ((t (type-of x))) (or (= t "number") (= t "rational"))))
|
||||
|
||||
(define cl-integerp? integer?)
|
||||
(define cl-floatp? float?)
|
||||
(define cl-rationalp? rational?)
|
||||
|
||||
(define (cl-realp? x) (or (integer? x) (float? x) (rational? x)))
|
||||
|
||||
(define cl-characterp? char?)
|
||||
(define cl-stringp? (fn (x) (= (type-of x) "string")))
|
||||
(define cl-symbolp? (fn (x) (= (type-of x) "symbol")))
|
||||
(define cl-keywordp? (fn (x) (= (type-of x) "keyword")))
|
||||
|
||||
(define
|
||||
(cl-functionp? x)
|
||||
(let
|
||||
((t (type-of x)))
|
||||
(or
|
||||
(= t "function")
|
||||
(= t "lambda")
|
||||
(= t "native-fn")
|
||||
(= t "component"))))
|
||||
|
||||
(define cl-vectorp? vector?)
|
||||
(define cl-arrayp? vector?)
|
||||
|
||||
;; sx_server: (rest (list x)) returns () not nil — cl-empty? handles both
|
||||
(define
|
||||
(cl-empty? x)
|
||||
(or (nil? x) (and (list? x) (= (len x) 0))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Arithmetic — thin aliases to spec primitives
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define cl-mod modulo)
|
||||
(define cl-rem remainder)
|
||||
(define cl-gcd gcd)
|
||||
(define cl-lcm lcm)
|
||||
(define cl-expt expt)
|
||||
(define cl-floor floor)
|
||||
(define cl-ceiling ceil)
|
||||
(define cl-truncate truncate)
|
||||
(define cl-round round)
|
||||
(define cl-abs (fn (x) (if (< x 0) (- 0 x) x)))
|
||||
(define cl-min (fn (a b) (if (< a b) a b)))
|
||||
(define cl-max (fn (a b) (if (> a b) a b)))
|
||||
(define cl-quotient quotient)
|
||||
|
||||
(define
|
||||
(cl-signum x)
|
||||
(cond
|
||||
((> x 0) 1)
|
||||
((< x 0) -1)
|
||||
(else 0)))
|
||||
|
||||
(define (cl-evenp? n) (= (modulo n 2) 0))
|
||||
(define (cl-oddp? n) (= (modulo n 2) 1))
|
||||
(define (cl-zerop? n) (= n 0))
|
||||
(define (cl-plusp? n) (> n 0))
|
||||
(define (cl-minusp? n) (< n 0))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Character functions — alias spec char primitives + CL name mapping
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define cl-char->integer char->integer)
|
||||
(define cl-integer->char integer->char)
|
||||
(define cl-char-upcase char-upcase)
|
||||
(define cl-char-downcase char-downcase)
|
||||
(define cl-char-code char->integer)
|
||||
(define cl-code-char integer->char)
|
||||
|
||||
(define cl-char=? char=?)
|
||||
(define cl-char<? char<?)
|
||||
(define cl-char>? char>?)
|
||||
(define cl-char<=? char<=?)
|
||||
(define cl-char>=? char>=?)
|
||||
(define cl-char-ci=? char-ci=?)
|
||||
(define cl-char-ci<? char-ci<?)
|
||||
(define cl-char-ci>? char-ci>?)
|
||||
|
||||
;; Inline predicates — char-alphabetic?/char-numeric? unreliable in sx_server
|
||||
(define
|
||||
(cl-alpha-char-p c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(or
|
||||
(and (>= n 65) (<= n 90))
|
||||
(and (>= n 97) (<= n 122)))))
|
||||
|
||||
(define
|
||||
(cl-digit-char-p c)
|
||||
(let ((n (char->integer c))) (and (>= n 48) (<= n 57))))
|
||||
|
||||
(define
|
||||
(cl-alphanumericp c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(or
|
||||
(and (>= n 48) (<= n 57))
|
||||
(and (>= n 65) (<= n 90))
|
||||
(and (>= n 97) (<= n 122)))))
|
||||
|
||||
(define
|
||||
(cl-upper-case-p c)
|
||||
(let ((n (char->integer c))) (and (>= n 65) (<= n 90))))
|
||||
|
||||
(define
|
||||
(cl-lower-case-p c)
|
||||
(let ((n (char->integer c))) (and (>= n 97) (<= n 122))))
|
||||
|
||||
;; Named character constants
|
||||
(define cl-char-space (integer->char 32))
|
||||
(define cl-char-newline (integer->char 10))
|
||||
(define cl-char-tab (integer->char 9))
|
||||
(define cl-char-backspace (integer->char 8))
|
||||
(define cl-char-return (integer->char 13))
|
||||
(define cl-char-null (integer->char 0))
|
||||
(define cl-char-escape (integer->char 27))
|
||||
(define cl-char-delete (integer->char 127))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. String + IO — use spec format and ports
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; CL format: (cl-format nil "~a ~a" x y) — nil destination means return string
|
||||
(define
|
||||
(cl-format dest template &rest args)
|
||||
(let ((s (apply format (cons template args)))) (if (= dest nil) s s)))
|
||||
|
||||
(define cl-write-to-string write-to-string)
|
||||
(define cl-princ-to-string display-to-string)
|
||||
|
||||
;; CL read-from-string: parse value from a string using SX port
|
||||
(define
|
||||
(cl-read-from-string s)
|
||||
(let ((p (open-input-string s))) (read p)))
|
||||
|
||||
;; String stream (output)
|
||||
(define cl-make-string-output-stream open-output-string)
|
||||
(define cl-get-output-stream-string get-output-string)
|
||||
|
||||
;; String stream (input)
|
||||
(define cl-make-string-input-stream open-input-string)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Gensym
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define cl-gensym gensym)
|
||||
(define cl-gentemp gensym)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. Multiple values (CL: values / nth-value)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (cl-values &rest args) {:_values true :_list args})
|
||||
|
||||
(define
|
||||
(cl-call-with-values producer consumer)
|
||||
(let
|
||||
((mv (producer)))
|
||||
(if
|
||||
(and (dict? mv) (get mv :_values))
|
||||
(apply consumer (get mv :_list))
|
||||
(consumer mv))))
|
||||
|
||||
(define
|
||||
(cl-nth-value n mv)
|
||||
(cond
|
||||
((and (dict? mv) (get mv :_values))
|
||||
(let
|
||||
((lst (get mv :_list)))
|
||||
(if (>= n (len lst)) nil (nth lst n))))
|
||||
((= n 0) mv)
|
||||
(else nil)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 7. Sets (CL: adjoin / member / union / intersection / set-difference)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define cl-make-set make-set)
|
||||
(define cl-set? set?)
|
||||
(define cl-set-add set-add!)
|
||||
(define cl-set-memberp set-member?)
|
||||
(define cl-set-remove set-remove!)
|
||||
(define cl-set-union set-union)
|
||||
(define cl-set-intersect set-intersection)
|
||||
(define cl-set-difference set-difference)
|
||||
(define cl-list->set list->set)
|
||||
(define cl-set->list set->list)
|
||||
|
||||
;; CL: (member item list) — returns tail starting at item, or nil
|
||||
(define
|
||||
(cl-member item lst)
|
||||
(cond
|
||||
((cl-empty? lst) nil)
|
||||
((equal? item (first lst)) lst)
|
||||
(else (cl-member item (rest lst)))))
|
||||
|
||||
;; CL: (adjoin item list) — cons only if not already present
|
||||
(define (cl-adjoin item lst) (if (cl-member item lst) lst (cons item lst)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 8. Radix formatting (CL: (write-to-string n :base radix))
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (cl-integer-to-string n radix) (number->string n radix))
|
||||
|
||||
(define (cl-string-to-integer s radix) (string->number s radix))
|
||||
|
||||
;; CL ~R directive helpers
|
||||
(define (cl-format-binary n) (number->string n 2))
|
||||
(define (cl-format-octal n) (number->string n 8))
|
||||
(define (cl-format-hex n) (number->string n 16))
|
||||
(define (cl-format-decimal n) (number->string n 10))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 9. List utilities — cl-empty? guards against () from rest
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(cl-last lst)
|
||||
(cond
|
||||
((cl-empty? lst) nil)
|
||||
((cl-empty? (rest lst)) lst)
|
||||
(else (cl-last (rest lst)))))
|
||||
|
||||
(define
|
||||
(cl-butlast lst)
|
||||
(if
|
||||
(or (cl-empty? lst) (cl-empty? (rest lst)))
|
||||
nil
|
||||
(cons (first lst) (cl-butlast (rest lst)))))
|
||||
|
||||
(define
|
||||
(cl-nthcdr n lst)
|
||||
(if (= n 0) lst (cl-nthcdr (- n 1) (rest lst))))
|
||||
|
||||
(define (cl-nth n lst) (first (cl-nthcdr n lst)))
|
||||
|
||||
(define (cl-list-length lst) (len lst))
|
||||
|
||||
(define
|
||||
(cl-copy-list lst)
|
||||
(if (cl-empty? lst) nil (cons (first lst) (cl-copy-list (rest lst)))))
|
||||
|
||||
(define
|
||||
(cl-flatten lst)
|
||||
(cond
|
||||
((cl-empty? lst) nil)
|
||||
((list? (first lst))
|
||||
(append (cl-flatten (first lst)) (cl-flatten (rest lst))))
|
||||
(else (cons (first lst) (cl-flatten (rest lst))))))
|
||||
|
||||
;; CL: (assoc key alist) — returns matching pair or nil
|
||||
(define
|
||||
(cl-assoc key alist)
|
||||
(cond
|
||||
((cl-empty? alist) nil)
|
||||
((equal? key (first (first alist))) (first alist))
|
||||
(else (cl-assoc key (rest alist)))))
|
||||
|
||||
;; CL: (rassoc val alist) — reverse assoc (match on second element)
|
||||
(define
|
||||
(cl-rassoc val alist)
|
||||
(cond
|
||||
((cl-empty? alist) nil)
|
||||
((equal? val (first (rest (first alist)))) (first alist))
|
||||
(else (cl-rassoc val (rest alist)))))
|
||||
|
||||
;; CL: (getf plist key) — property list lookup
|
||||
(define
|
||||
(cl-getf plist key)
|
||||
(cond
|
||||
((or (cl-empty? plist) (cl-empty? (rest plist))) nil)
|
||||
((equal? (first plist) key) (first (rest plist)))
|
||||
(else (cl-getf (rest (rest plist)) key))))
|
||||
302
lib/common-lisp/test.sh
Executable file
302
lib/common-lisp/test.sh
Executable file
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/common-lisp/test.sh — quick smoke-test the CL runtime layer.
|
||||
# Uses sx_server.exe epoch protocol (same as lib/lua/test.sh).
|
||||
#
|
||||
# Usage:
|
||||
# bash lib/common-lisp/test.sh
|
||||
# bash lib/common-lisp/test.sh -v
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found. Run: cd hosts/ocaml && dune build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERBOSE="${1:-}"
|
||||
PASS=0; FAIL=0; ERRORS=""
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "spec/stdlib.sx")
|
||||
(load "lib/common-lisp/runtime.sx")
|
||||
|
||||
;; --- Type predicates ---
|
||||
(epoch 10)
|
||||
(eval "(cl-null? nil)")
|
||||
(epoch 11)
|
||||
(eval "(cl-null? false)")
|
||||
(epoch 12)
|
||||
(eval "(cl-consp? (list 1 2))")
|
||||
(epoch 13)
|
||||
(eval "(cl-consp? nil)")
|
||||
(epoch 14)
|
||||
(eval "(cl-listp? nil)")
|
||||
(epoch 15)
|
||||
(eval "(cl-listp? (list 1))")
|
||||
(epoch 16)
|
||||
(eval "(cl-atom? nil)")
|
||||
(epoch 17)
|
||||
(eval "(cl-atom? (list 1))")
|
||||
(epoch 18)
|
||||
(eval "(cl-integerp? 42)")
|
||||
(epoch 19)
|
||||
(eval "(cl-floatp? 3.14)")
|
||||
(epoch 20)
|
||||
(eval "(cl-characterp? (integer->char 65))")
|
||||
(epoch 21)
|
||||
(eval "(cl-stringp? \"hello\")")
|
||||
|
||||
;; --- Arithmetic ---
|
||||
(epoch 30)
|
||||
(eval "(cl-mod 10 3)")
|
||||
(epoch 31)
|
||||
(eval "(cl-rem 10 3)")
|
||||
(epoch 32)
|
||||
(eval "(cl-quotient 10 3)")
|
||||
(epoch 33)
|
||||
(eval "(cl-gcd 12 8)")
|
||||
(epoch 34)
|
||||
(eval "(cl-lcm 4 6)")
|
||||
(epoch 35)
|
||||
(eval "(cl-abs -5)")
|
||||
(epoch 36)
|
||||
(eval "(cl-abs 5)")
|
||||
(epoch 37)
|
||||
(eval "(cl-min 2 7)")
|
||||
(epoch 38)
|
||||
(eval "(cl-max 2 7)")
|
||||
(epoch 39)
|
||||
(eval "(cl-evenp? 4)")
|
||||
(epoch 40)
|
||||
(eval "(cl-evenp? 3)")
|
||||
(epoch 41)
|
||||
(eval "(cl-oddp? 7)")
|
||||
(epoch 42)
|
||||
(eval "(cl-zerop? 0)")
|
||||
(epoch 43)
|
||||
(eval "(cl-plusp? 1)")
|
||||
(epoch 44)
|
||||
(eval "(cl-minusp? -1)")
|
||||
(epoch 45)
|
||||
(eval "(cl-signum 42)")
|
||||
(epoch 46)
|
||||
(eval "(cl-signum -7)")
|
||||
(epoch 47)
|
||||
(eval "(cl-signum 0)")
|
||||
|
||||
;; --- Characters ---
|
||||
(epoch 50)
|
||||
(eval "(cl-char-code (integer->char 65))")
|
||||
(epoch 51)
|
||||
(eval "(char? (cl-code-char 65))")
|
||||
(epoch 52)
|
||||
(eval "(cl-char=? (integer->char 65) (integer->char 65))")
|
||||
(epoch 53)
|
||||
(eval "(cl-char<? (integer->char 65) (integer->char 90))")
|
||||
(epoch 54)
|
||||
(eval "(cl-char-code cl-char-space)")
|
||||
(epoch 55)
|
||||
(eval "(cl-char-code cl-char-newline)")
|
||||
(epoch 56)
|
||||
(eval "(cl-alpha-char-p (integer->char 65))")
|
||||
(epoch 57)
|
||||
(eval "(cl-digit-char-p (integer->char 48))")
|
||||
|
||||
;; --- Format ---
|
||||
(epoch 60)
|
||||
(eval "(cl-format nil \"hello\")")
|
||||
(epoch 61)
|
||||
(eval "(cl-format nil \"~a\" \"world\")")
|
||||
(epoch 62)
|
||||
(eval "(cl-format nil \"~d\" 42)")
|
||||
(epoch 63)
|
||||
(eval "(cl-format nil \"~x\" 255)")
|
||||
(epoch 64)
|
||||
(eval "(cl-format nil \"x=~d y=~d\" 3 4)")
|
||||
|
||||
;; --- Gensym ---
|
||||
(epoch 70)
|
||||
(eval "(= (type-of (cl-gensym)) \"symbol\")")
|
||||
(epoch 71)
|
||||
(eval "(not (= (cl-gensym) (cl-gensym)))")
|
||||
|
||||
;; --- Sets ---
|
||||
(epoch 80)
|
||||
(eval "(cl-set? (cl-make-set))")
|
||||
(epoch 81)
|
||||
(eval "(let ((s (cl-make-set))) (do (cl-set-add s 1) (cl-set-memberp s 1)))")
|
||||
(epoch 82)
|
||||
(eval "(cl-set-memberp (cl-make-set) 42)")
|
||||
(epoch 83)
|
||||
(eval "(cl-set-memberp (cl-list->set (list 1 2 3)) 2)")
|
||||
|
||||
;; --- Lists ---
|
||||
(epoch 90)
|
||||
(eval "(cl-nth 0 (list 1 2 3))")
|
||||
(epoch 91)
|
||||
(eval "(cl-nth 2 (list 1 2 3))")
|
||||
(epoch 92)
|
||||
(eval "(cl-last (list 1 2 3))")
|
||||
(epoch 93)
|
||||
(eval "(cl-butlast (list 1 2 3))")
|
||||
(epoch 94)
|
||||
(eval "(cl-nthcdr 1 (list 1 2 3))")
|
||||
(epoch 95)
|
||||
(eval "(cl-assoc \"b\" (list (list \"a\" 1) (list \"b\" 2)))")
|
||||
(epoch 96)
|
||||
(eval "(cl-assoc \"z\" (list (list \"a\" 1)))")
|
||||
(epoch 97)
|
||||
(eval "(cl-getf (list \"x\" 42 \"y\" 99) \"x\")")
|
||||
(epoch 98)
|
||||
(eval "(cl-adjoin 0 (list 1 2))")
|
||||
(epoch 99)
|
||||
(eval "(cl-adjoin 1 (list 1 2))")
|
||||
(epoch 100)
|
||||
(eval "(cl-member 2 (list 1 2 3))")
|
||||
(epoch 101)
|
||||
(eval "(cl-member 9 (list 1 2 3))")
|
||||
(epoch 102)
|
||||
(eval "(cl-flatten (list 1 (list 2 3) 4))")
|
||||
|
||||
;; --- Radix ---
|
||||
(epoch 110)
|
||||
(eval "(cl-format-binary 10)")
|
||||
(epoch 111)
|
||||
(eval "(cl-format-octal 15)")
|
||||
(epoch 112)
|
||||
(eval "(cl-format-hex 255)")
|
||||
(epoch 113)
|
||||
(eval "(cl-format-decimal 42)")
|
||||
(epoch 114)
|
||||
(eval "(cl-integer-to-string 31 16)")
|
||||
(epoch 115)
|
||||
(eval "(cl-string-to-integer \"1f\" 16)")
|
||||
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 30 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
check() {
|
||||
local epoch="$1" desc="$2" expected="$3"
|
||||
local actual
|
||||
# ok-len format: value appears on the line AFTER "(ok-len N length)"
|
||||
actual=$(echo "$OUTPUT" | grep -A1 "^(ok-len $epoch " | tail -1 || true)
|
||||
# strip any leading "(ok-len ...)" if grep -A1 returned it instead
|
||||
if echo "$actual" | grep -q "^(ok-len"; then actual=""; fi
|
||||
if [ -z "$actual" ]; then
|
||||
actual=$(echo "$OUTPUT" | grep "^(ok $epoch " | head -1 || true)
|
||||
fi
|
||||
if [ -z "$actual" ]; then
|
||||
actual=$(echo "$OUTPUT" | grep "^(error $epoch " | head -1 || true)
|
||||
fi
|
||||
[ -z "$actual" ] && actual="<no output for epoch $epoch>"
|
||||
|
||||
if echo "$actual" | grep -qF -- "$expected"; then
|
||||
PASS=$((PASS+1))
|
||||
[ "$VERBOSE" = "-v" ] && echo " ok $desc"
|
||||
else
|
||||
FAIL=$((FAIL+1))
|
||||
ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
# Type predicates
|
||||
check 10 "cl-null? nil" "true"
|
||||
check 11 "cl-null? false" "false"
|
||||
check 12 "cl-consp? pair" "true"
|
||||
check 13 "cl-consp? nil" "false"
|
||||
check 14 "cl-listp? nil" "true"
|
||||
check 15 "cl-listp? list" "true"
|
||||
check 16 "cl-atom? nil" "true"
|
||||
check 17 "cl-atom? pair" "false"
|
||||
check 18 "cl-integerp?" "true"
|
||||
check 19 "cl-floatp?" "true"
|
||||
check 20 "cl-characterp?" "true"
|
||||
check 21 "cl-stringp?" "true"
|
||||
|
||||
# Arithmetic
|
||||
check 30 "cl-mod 10 3" "1"
|
||||
check 31 "cl-rem 10 3" "1"
|
||||
check 32 "cl-quotient 10 3" "3"
|
||||
check 33 "cl-gcd 12 8" "4"
|
||||
check 34 "cl-lcm 4 6" "12"
|
||||
check 35 "cl-abs -5" "5"
|
||||
check 36 "cl-abs 5" "5"
|
||||
check 37 "cl-min 2 7" "2"
|
||||
check 38 "cl-max 2 7" "7"
|
||||
check 39 "cl-evenp? 4" "true"
|
||||
check 40 "cl-evenp? 3" "false"
|
||||
check 41 "cl-oddp? 7" "true"
|
||||
check 42 "cl-zerop? 0" "true"
|
||||
check 43 "cl-plusp? 1" "true"
|
||||
check 44 "cl-minusp? -1" "true"
|
||||
check 45 "cl-signum pos" "1"
|
||||
check 46 "cl-signum neg" "-1"
|
||||
check 47 "cl-signum zero" "0"
|
||||
|
||||
# Characters
|
||||
check 50 "cl-char-code" "65"
|
||||
check 51 "code-char returns char" "true"
|
||||
check 52 "cl-char=?" "true"
|
||||
check 53 "cl-char<?" "true"
|
||||
check 54 "cl-char-space code" "32"
|
||||
check 55 "cl-char-newline code" "10"
|
||||
check 56 "cl-alpha-char-p A" "true"
|
||||
check 57 "cl-digit-char-p 0" "true"
|
||||
|
||||
# Format
|
||||
check 60 "cl-format plain" '"hello"'
|
||||
check 61 "cl-format ~a" '"world"'
|
||||
check 62 "cl-format ~d" '"42"'
|
||||
check 63 "cl-format ~x" '"ff"'
|
||||
check 64 "cl-format multi" '"x=3 y=4"'
|
||||
|
||||
# Gensym
|
||||
check 70 "gensym returns symbol" "true"
|
||||
check 71 "gensyms are unique" "true"
|
||||
|
||||
# Sets
|
||||
check 80 "make-set is set?" "true"
|
||||
check 81 "set-add + member" "true"
|
||||
check 82 "member in empty" "false"
|
||||
check 83 "list->set member" "true"
|
||||
|
||||
# Lists
|
||||
check 90 "cl-nth 0" "1"
|
||||
check 91 "cl-nth 2" "3"
|
||||
check 92 "cl-last" "(3)"
|
||||
check 93 "cl-butlast" "(1 2)"
|
||||
check 94 "cl-nthcdr 1" "(2 3)"
|
||||
check 95 "cl-assoc hit" '("b" 2)'
|
||||
check 96 "cl-assoc miss" "nil"
|
||||
check 97 "cl-getf hit" "42"
|
||||
check 98 "cl-adjoin new" "(0 1 2)"
|
||||
check 99 "cl-adjoin dup" "(1 2)"
|
||||
check 100 "cl-member hit" "(2 3)"
|
||||
check 101 "cl-member miss" "nil"
|
||||
check 102 "cl-flatten" "(1 2 3 4)"
|
||||
|
||||
# Radix
|
||||
check 110 "cl-format-binary 10" '"1010"'
|
||||
check 111 "cl-format-octal 15" '"17"'
|
||||
check 112 "cl-format-hex 255" '"ff"'
|
||||
check 113 "cl-format-decimal 42" '"42"'
|
||||
check 114 "n->s base 16" '"1f"'
|
||||
check 115 "s->n base 16" "31"
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "ok $PASS/$TOTAL lib/common-lisp tests passed"
|
||||
else
|
||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
||||
echo "$ERRORS"
|
||||
fi
|
||||
[ $FAIL -eq 0 ]
|
||||
230
lib/erlang/runtime.sx
Normal file
230
lib/erlang/runtime.sx
Normal file
@@ -0,0 +1,230 @@
|
||||
;; lib/erlang/runtime.sx — Erlang BIFs and stdlib wrappers on SX primitives
|
||||
;;
|
||||
;; Provides Erlang-idiomatic wrappers. Thin where spec primitives match;
|
||||
;; inline where Erlang semantics differ (e.g. rem sign, integer division).
|
||||
;;
|
||||
;; Primitives used from spec:
|
||||
;; integer?/float? (Phase 2)
|
||||
;; remainder/quotient (Phase 2 / Phase 15)
|
||||
;; bitwise-and/or/xor/not (Phase 7)
|
||||
;; arithmetic-shift (Phase 7)
|
||||
;; make-set/set-add!/etc (Phase 18)
|
||||
;; make-regexp/regexp-match/etc (Phase 20)
|
||||
;; gcd (Phase 15)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Numeric tower — type predicates + conversions
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define er-is-integer? integer?)
|
||||
(define er-is-float? float?)
|
||||
(define (er-is-number? x) (or (integer? x) (float? x)))
|
||||
(define (er-is-atom? x) (= (type-of x) "symbol"))
|
||||
(define er-is-list? list?)
|
||||
(define er-is-binary? bytevector?)
|
||||
|
||||
;; Erlang float/1 coerces an integer to float
|
||||
(define (er-float x) (* 1 x))
|
||||
|
||||
;; Erlang trunc/1 — truncate toward zero
|
||||
(define er-trunc truncate)
|
||||
|
||||
;; Erlang round/1 — round to nearest integer
|
||||
(define er-round round)
|
||||
|
||||
;; Erlang abs/1
|
||||
(define er-abs abs)
|
||||
|
||||
;; Erlang max/min (BIFs in OTP 26)
|
||||
(define (er-max a b) (if (>= a b) a b))
|
||||
(define (er-min a b) (if (<= a b) a b))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Integer arithmetic — div + rem (Erlang semantics)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Erlang div: integer division truncating toward zero
|
||||
(define er-div quotient)
|
||||
|
||||
;; Erlang rem: remainder with sign of dividend (matches remainder primitive)
|
||||
(define er-rem remainder)
|
||||
|
||||
;; Erlang gcd (non-standard BIF but useful)
|
||||
(define er-gcd gcd)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Bitwise ops — band / bor / bxor / bnot / bsl / bsr
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define er-band bitwise-and)
|
||||
(define er-bor bitwise-or)
|
||||
(define er-bxor bitwise-xor)
|
||||
(define er-bnot bitwise-not)
|
||||
|
||||
;; bsl: bit shift left by N positions
|
||||
(define (er-bsl x n) (arithmetic-shift x n))
|
||||
|
||||
;; bsr: bit shift right by N positions
|
||||
(define (er-bsr x n) (arithmetic-shift x (- 0 n)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Sets module — thin wrappers matching Erlang sets API
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define er-sets-new make-set)
|
||||
(define er-sets-add-element set-add!)
|
||||
(define er-sets-is-element set-member?)
|
||||
(define er-sets-del-element set-remove!)
|
||||
(define er-sets-union set-union)
|
||||
(define er-sets-intersection set-intersection)
|
||||
(define er-sets-subtract set-difference)
|
||||
(define er-sets-to-list set->list)
|
||||
(define er-sets-from-list list->set)
|
||||
(define (er-sets-size s) (len (set->list s)))
|
||||
(define (er-sets-is-set? x) (set? x))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Regexp — re module wrappers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; er-re-run: returns match dict or nil (no match)
|
||||
(define
|
||||
(er-re-run subject pattern)
|
||||
(regexp-match (make-regexp pattern) subject))
|
||||
|
||||
;; er-re-replace: replace first match
|
||||
(define
|
||||
(er-re-replace subject pattern replacement)
|
||||
(regexp-replace (make-regexp pattern) subject replacement))
|
||||
|
||||
;; er-re-replace-all: global replace
|
||||
(define
|
||||
(er-re-replace-all subject pattern replacement)
|
||||
(regexp-replace-all (make-regexp pattern) subject replacement))
|
||||
|
||||
;; er-re-match-groups: extract capture groups from a match result
|
||||
(define (er-re-match-groups m) (if (= m nil) nil (get m :groups)))
|
||||
|
||||
;; er-re-split: split string on regexp delimiter
|
||||
(define
|
||||
(er-re-split subject pattern)
|
||||
(let
|
||||
((re (make-regexp pattern))
|
||||
(ms (regexp-match-all (make-regexp pattern) subject)))
|
||||
(if
|
||||
(= (len ms) 0)
|
||||
(list subject)
|
||||
(letrec
|
||||
((go (fn (matches pos acc) (if (= (len matches) 0) (append acc (list (substring subject pos (len subject)))) (let ((m (first matches)) (start (get (first matches) :start)) (end (get (first matches) :end))) (go (rest matches) end (append acc (list (substring subject pos start)))))))))
|
||||
(go ms 0 (list))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. List BIFs — hd/tl/length + lists module
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (er-hd lst) (first lst))
|
||||
(define (er-tl lst) (rest lst))
|
||||
(define (er-length lst) (len lst))
|
||||
|
||||
;; lists:member/2
|
||||
(define
|
||||
(er-lists-member elem lst)
|
||||
(cond
|
||||
((= (len lst) 0) false)
|
||||
((= elem (first lst)) true)
|
||||
(else (er-lists-member elem (rest lst)))))
|
||||
|
||||
;; lists:reverse/1
|
||||
(define er-lists-reverse reverse)
|
||||
|
||||
;; lists:append/2
|
||||
(define er-lists-append append)
|
||||
|
||||
;; lists:flatten/1
|
||||
(define
|
||||
(er-lists-flatten lst)
|
||||
(cond
|
||||
((= (len lst) 0) (list))
|
||||
((list? (first lst))
|
||||
(append (er-lists-flatten (first lst)) (er-lists-flatten (rest lst))))
|
||||
(else (cons (first lst) (er-lists-flatten (rest lst))))))
|
||||
|
||||
;; lists:nth/2 — 1-indexed
|
||||
(define (er-lists-nth n lst) (nth lst (- n 1)))
|
||||
|
||||
;; lists:map/2
|
||||
(define er-lists-map map)
|
||||
|
||||
;; lists:filter/2
|
||||
(define er-lists-filter filter)
|
||||
|
||||
;; lists:foldl/3 — (Fun, Acc0, List)
|
||||
(define
|
||||
(er-lists-foldl f acc lst)
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
acc
|
||||
(er-lists-foldl f (f (first lst) acc) (rest lst))))
|
||||
|
||||
;; lists:foldr/3
|
||||
(define
|
||||
(er-lists-foldr f acc lst)
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
acc
|
||||
(f (first lst) (er-lists-foldr f acc (rest lst)))))
|
||||
|
||||
;; lists:zip/2
|
||||
(define
|
||||
(er-lists-zip a b)
|
||||
(if
|
||||
(or (= (len a) 0) (= (len b) 0))
|
||||
(list)
|
||||
(cons (list (first a) (first b)) (er-lists-zip (rest a) (rest b)))))
|
||||
|
||||
;; lists:seq/2 — generate integer range (1-indexed like Erlang)
|
||||
(define
|
||||
(er-lists-seq from to)
|
||||
(if
|
||||
(> from to)
|
||||
(list)
|
||||
(cons from (er-lists-seq (+ from 1) to))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 7. Type conversion BIFs
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; atom_to_list/1 — convert atom (symbol) to its name string
|
||||
(define (er-atom-to-list a) (symbol->string a))
|
||||
|
||||
;; list_to_atom/1 — convert string to atom (symbol)
|
||||
(define (er-list-to-atom s) (make-symbol s))
|
||||
|
||||
;; integer_to_list/1
|
||||
(define (er-integer-to-list n) (str n))
|
||||
|
||||
;; list_to_integer/1
|
||||
(define (er-list-to-integer s) (truncate (parse-number s)))
|
||||
|
||||
;; float_to_list/1
|
||||
(define (er-float-to-list f) (str f))
|
||||
|
||||
;; list_to_float/1
|
||||
(define (er-list-to-float s) (* 1 (parse-number s)))
|
||||
|
||||
;; integer_to_list/2 — with radix (e.g. 16 for hex)
|
||||
(define (er-integer-to-list-radix n radix) (number->string n radix))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 8. ok/error tuple helpers — Erlang idiom {ok, Val} / {error, Reason}
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (er-ok val) (list "ok" val))
|
||||
(define (er-error reason) (list "error" reason))
|
||||
(define
|
||||
(er-is-ok? t)
|
||||
(and (list? t) (= (len t) 2) (= (first t) "ok")))
|
||||
(define
|
||||
(er-is-error? t)
|
||||
(and (list? t) (= (len t) 2) (= (first t) "error")))
|
||||
(define (er-unwrap t) (nth t 1))
|
||||
260
lib/erlang/test.sh
Executable file
260
lib/erlang/test.sh
Executable file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/erlang/test.sh — smoke-test the Erlang runtime layer.
|
||||
# Uses sx_server.exe epoch protocol.
|
||||
#
|
||||
# Usage:
|
||||
# bash lib/erlang/test.sh
|
||||
# bash lib/erlang/test.sh -v
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found. Run: cd hosts/ocaml && dune build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERBOSE="${1:-}"
|
||||
PASS=0; FAIL=0; ERRORS=""
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "lib/erlang/runtime.sx")
|
||||
|
||||
;; --- Numeric tower ---
|
||||
(epoch 10)
|
||||
(eval "(er-is-integer? 42)")
|
||||
(epoch 11)
|
||||
(eval "(er-is-integer? 3.14)")
|
||||
(epoch 12)
|
||||
(eval "(er-is-float? 3.14)")
|
||||
(epoch 13)
|
||||
(eval "(er-is-float? 42)")
|
||||
(epoch 14)
|
||||
(eval "(er-is-number? 42)")
|
||||
(epoch 15)
|
||||
(eval "(er-is-number? 3.14)")
|
||||
(epoch 16)
|
||||
(eval "(er-float 5)")
|
||||
(epoch 17)
|
||||
(eval "(er-trunc 3.9)")
|
||||
(epoch 18)
|
||||
(eval "(er-round 3.5)")
|
||||
(epoch 19)
|
||||
(eval "(er-abs -7)")
|
||||
(epoch 20)
|
||||
(eval "(er-max 3 7)")
|
||||
(epoch 21)
|
||||
(eval "(er-min 3 7)")
|
||||
|
||||
;; --- div + rem ---
|
||||
(epoch 30)
|
||||
(eval "(er-div 10 3)")
|
||||
(epoch 31)
|
||||
(eval "(er-div -10 3)")
|
||||
(epoch 32)
|
||||
(eval "(er-rem 10 3)")
|
||||
(epoch 33)
|
||||
(eval "(er-rem -10 3)")
|
||||
(epoch 34)
|
||||
(eval "(er-gcd 12 8)")
|
||||
|
||||
;; --- Bitwise ---
|
||||
(epoch 40)
|
||||
(eval "(er-band 12 10)")
|
||||
(epoch 41)
|
||||
(eval "(er-bor 12 10)")
|
||||
(epoch 42)
|
||||
(eval "(er-bxor 12 10)")
|
||||
(epoch 43)
|
||||
(eval "(er-bnot 0)")
|
||||
(epoch 44)
|
||||
(eval "(er-bsl 1 4)")
|
||||
(epoch 45)
|
||||
(eval "(er-bsr 16 2)")
|
||||
|
||||
;; --- Sets ---
|
||||
(epoch 50)
|
||||
(eval "(er-sets-is-set? (er-sets-new))")
|
||||
(epoch 51)
|
||||
(eval "(let ((s (er-sets-new))) (do (er-sets-add-element s 1) (er-sets-is-element s 1)))")
|
||||
(epoch 52)
|
||||
(eval "(er-sets-is-element (er-sets-new) 42)")
|
||||
(epoch 53)
|
||||
(eval "(er-sets-is-element (er-sets-from-list (list 1 2 3)) 2)")
|
||||
(epoch 54)
|
||||
(eval "(er-sets-size (er-sets-from-list (list 1 2 3)))")
|
||||
(epoch 55)
|
||||
(eval "(len (er-sets-to-list (er-sets-from-list (list 1 2 3))))")
|
||||
|
||||
;; --- Regexp ---
|
||||
(epoch 60)
|
||||
(eval "(not (= (er-re-run \"hello\" \"ll\") nil))")
|
||||
(epoch 61)
|
||||
(eval "(= (er-re-run \"hello\" \"xyz\") nil)")
|
||||
(epoch 62)
|
||||
(eval "(get (er-re-run \"hello\" \"ll\") :match)")
|
||||
(epoch 63)
|
||||
(eval "(er-re-replace \"hello\" \"l\" \"r\")")
|
||||
(epoch 64)
|
||||
(eval "(er-re-replace-all \"hello\" \"l\" \"r\")")
|
||||
(epoch 65)
|
||||
(eval "(er-re-match-groups (er-re-run \"hello world\" \"(\\w+)\\s+(\\w+)\"))")
|
||||
(epoch 66)
|
||||
(eval "(len (er-re-split \"a,b,c\" \",\"))")
|
||||
|
||||
;; --- List BIFs ---
|
||||
(epoch 70)
|
||||
(eval "(er-hd (list 1 2 3))")
|
||||
(epoch 71)
|
||||
(eval "(er-tl (list 1 2 3))")
|
||||
(epoch 72)
|
||||
(eval "(er-length (list 1 2 3))")
|
||||
(epoch 73)
|
||||
(eval "(er-lists-member 2 (list 1 2 3))")
|
||||
(epoch 74)
|
||||
(eval "(er-lists-member 9 (list 1 2 3))")
|
||||
(epoch 75)
|
||||
(eval "(er-lists-reverse (list 1 2 3))")
|
||||
(epoch 76)
|
||||
(eval "(er-lists-nth 2 (list 10 20 30))")
|
||||
(epoch 77)
|
||||
(eval "(er-lists-foldl + 0 (list 1 2 3 4 5))")
|
||||
(epoch 78)
|
||||
(eval "(er-lists-seq 1 5)")
|
||||
(epoch 79)
|
||||
(eval "(er-lists-flatten (list 1 (list 2 3) (list 4 (list 5))))")
|
||||
|
||||
;; --- Type conversions ---
|
||||
(epoch 80)
|
||||
(eval "(er-integer-to-list 42)")
|
||||
(epoch 81)
|
||||
(eval "(er-list-to-integer \"42\")")
|
||||
(epoch 82)
|
||||
(eval "(er-integer-to-list-radix 255 16)")
|
||||
(epoch 83)
|
||||
(eval "(er-atom-to-list (make-symbol \"hello\"))")
|
||||
(epoch 84)
|
||||
(eval "(= (type-of (er-list-to-atom \"foo\")) \"symbol\")")
|
||||
|
||||
;; --- ok/error tuples ---
|
||||
(epoch 90)
|
||||
(eval "(er-is-ok? (er-ok 42))")
|
||||
(epoch 91)
|
||||
(eval "(er-is-error? (er-error \"reason\"))")
|
||||
(epoch 92)
|
||||
(eval "(er-unwrap (er-ok 42))")
|
||||
(epoch 93)
|
||||
(eval "(er-is-ok? (er-error \"bad\"))")
|
||||
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 30 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
check() {
|
||||
local epoch="$1" desc="$2" expected="$3"
|
||||
local actual
|
||||
actual=$(echo "$OUTPUT" | grep -A1 "^(ok-len $epoch " | tail -1 || true)
|
||||
if echo "$actual" | grep -q "^(ok-len"; then actual=""; fi
|
||||
if [ -z "$actual" ]; then
|
||||
actual=$(echo "$OUTPUT" | grep "^(ok $epoch " | head -1 || true)
|
||||
fi
|
||||
if [ -z "$actual" ]; then
|
||||
actual=$(echo "$OUTPUT" | grep "^(error $epoch " | head -1 || true)
|
||||
fi
|
||||
[ -z "$actual" ] && actual="<no output for epoch $epoch>"
|
||||
|
||||
if echo "$actual" | grep -qF -- "$expected"; then
|
||||
PASS=$((PASS+1))
|
||||
[ "$VERBOSE" = "-v" ] && echo " ok $desc"
|
||||
else
|
||||
FAIL=$((FAIL+1))
|
||||
ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
# Numeric tower
|
||||
check 10 "is-integer? 42" "true"
|
||||
check 11 "is-integer? float" "false"
|
||||
check 12 "is-float? 3.14" "true"
|
||||
check 13 "is-float? int" "false"
|
||||
check 14 "is-number? int" "true"
|
||||
check 15 "is-number? float" "true"
|
||||
check 16 "float 5" "5"
|
||||
check 17 "trunc 3.9" "3"
|
||||
check 18 "round 3.5" "4"
|
||||
check 19 "abs -7" "7"
|
||||
check 20 "max 3 7" "7"
|
||||
check 21 "min 3 7" "3"
|
||||
|
||||
# div + rem
|
||||
check 30 "div 10 3" "3"
|
||||
check 31 "div -10 3" "-3"
|
||||
check 32 "rem 10 3" "1"
|
||||
check 33 "rem -10 3" "-1"
|
||||
check 34 "gcd 12 8" "4"
|
||||
|
||||
# Bitwise
|
||||
check 40 "band 12 10" "8"
|
||||
check 41 "bor 12 10" "14"
|
||||
check 42 "bxor 12 10" "6"
|
||||
check 43 "bnot 0" "-1"
|
||||
check 44 "bsl 1 4" "16"
|
||||
check 45 "bsr 16 2" "4"
|
||||
|
||||
# Sets
|
||||
check 50 "sets-new is-set?" "true"
|
||||
check 51 "sets add+member" "true"
|
||||
check 52 "member empty" "false"
|
||||
check 53 "from-list member" "true"
|
||||
check 54 "sets-size" "3"
|
||||
check 55 "sets-to-list len" "3"
|
||||
|
||||
# Regexp
|
||||
check 60 "re-run match" "true"
|
||||
check 61 "re-run no match" "true"
|
||||
check 62 "re-run match text" '"ll"'
|
||||
check 63 "re-replace first" '"herlo"'
|
||||
check 64 "re-replace-all" '"herro"'
|
||||
check 65 "re-match-groups" '"hello"'
|
||||
check 66 "re-split count" "3"
|
||||
|
||||
# List BIFs
|
||||
check 70 "hd" "1"
|
||||
check 71 "tl" "(2 3)"
|
||||
check 72 "length" "3"
|
||||
check 73 "member hit" "true"
|
||||
check 74 "member miss" "false"
|
||||
check 75 "reverse" "(3 2 1)"
|
||||
check 76 "nth 2" "20"
|
||||
check 77 "foldl sum" "15"
|
||||
check 78 "seq 1..5" "(1 2 3 4 5)"
|
||||
check 79 "flatten" "(1 2 3 4 5)"
|
||||
|
||||
# Type conversions
|
||||
check 80 "integer-to-list" '"42"'
|
||||
check 81 "list-to-integer" "42"
|
||||
check 82 "integer-to-list hex" '"ff"'
|
||||
check 83 "atom-to-list" '"hello"'
|
||||
check 84 "list-to-atom" "true"
|
||||
|
||||
# ok/error
|
||||
check 90 "ok? ok-tuple" "true"
|
||||
check 91 "error? error-tuple" "true"
|
||||
check 92 "unwrap ok" "42"
|
||||
check 93 "ok? error-tuple" "false"
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "ok $PASS/$TOTAL lib/erlang tests passed"
|
||||
else
|
||||
echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
|
||||
echo "$ERRORS"
|
||||
fi
|
||||
[ $FAIL -eq 0 ]
|
||||
@@ -1,433 +1,175 @@
|
||||
;; Forth runtime — state, stacks, dictionary, output buffer.
|
||||
;; Data stack: mutable SX list, TOS = first.
|
||||
;; Return stack: separate mutable list.
|
||||
;; Dictionary: SX dict {lowercased-name -> word-record}.
|
||||
;; Word record: {"kind" "body" "immediate?"}; kind is "primitive" or "colon-def".
|
||||
;; Output buffer: mutable string appended to by `.`, `EMIT`, `CR`, etc.
|
||||
;; Compile-mode flag: "compiling" on the state.
|
||||
;; lib/forth/runtime.sx — Forth primitives on SX
|
||||
;;
|
||||
;; Provides Forth-idiomatic wrappers over SX built-ins.
|
||||
;; Primitives used:
|
||||
;; bitwise-and/or/xor/not/arithmetic-shift/bit-count (Phase 7)
|
||||
;; make-bytevector/bytevector-u8-ref/u8-set!/... (Phase 20)
|
||||
;; quotient/remainder/modulo (Phase 15 / builtin)
|
||||
;;
|
||||
;; Naming: SX identifiers can't include @ or !-alone, so Forth words are:
|
||||
;; C@ → forth-cfetch C! → forth-cstore
|
||||
;; @ → forth-fetch ! → forth-store
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Bitwise operations — Forth core words
|
||||
;; Forth TRUE = -1 (all bits set), FALSE = 0.
|
||||
;; All ops coerce to integer via truncate.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (forth-and a b) (bitwise-and (truncate a) (truncate b)))
|
||||
(define (forth-or a b) (bitwise-or (truncate a) (truncate b)))
|
||||
(define (forth-xor a b) (bitwise-xor (truncate a) (truncate b)))
|
||||
|
||||
;; INVERT — bitwise NOT (Forth NOT is logical; INVERT is bitwise)
|
||||
(define (forth-invert a) (bitwise-not (truncate a)))
|
||||
|
||||
;; LSHIFT RSHIFT — n bit — shift a by n positions
|
||||
(define (forth-lshift a n) (arithmetic-shift (truncate a) (truncate n)))
|
||||
(define
|
||||
(forth-rshift a n)
|
||||
(arithmetic-shift (truncate a) (- 0 (truncate n))))
|
||||
|
||||
;; 2* 2/ — multiply/divide by 2 via bit shift
|
||||
(define (forth-2* a) (arithmetic-shift (truncate a) 1))
|
||||
(define (forth-2/ a) (arithmetic-shift (truncate a) -1))
|
||||
|
||||
;; BIT-COUNT — number of set bits (Kernighan popcount)
|
||||
(define (forth-bit-count a) (bit-count (truncate a)))
|
||||
|
||||
;; INTEGER-LENGTH — index of highest set bit (0 for zero)
|
||||
(define (forth-integer-length a) (integer-length (truncate a)))
|
||||
|
||||
;; WITHIN — ( u ul uh -- flag ) true if ul <= u < uh
|
||||
(define (forth-within u ul uh) (and (>= u ul) (< u uh)))
|
||||
|
||||
;; Arithmetic complements commonly used alongside bitwise ops
|
||||
(define (forth-negate a) (- 0 (truncate a)))
|
||||
(define (forth-abs a) (abs (truncate a)))
|
||||
(define (forth-min a b) (if (< a b) a b))
|
||||
(define (forth-max a b) (if (> a b) a b))
|
||||
(define (forth-mod a b) (modulo (truncate a) (truncate b)))
|
||||
|
||||
;; /MOD — ( n1 n2 -- rem quot ) returns list (remainder quotient)
|
||||
(define
|
||||
(forth-divmod a b)
|
||||
(list
|
||||
(remainder (truncate a) (truncate b))
|
||||
(quotient (truncate a) (truncate b))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. String buffer — word-definition / string accumulation
|
||||
;; EMIT appends one char; TYPE appends a string.
|
||||
;; Value is retrieved with forth-sb-value.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
forth-make-state
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((s (dict)))
|
||||
(dict-set! s "dstack" (list))
|
||||
(dict-set! s "rstack" (list))
|
||||
(dict-set! s "dict" (dict))
|
||||
(dict-set! s "output" "")
|
||||
(dict-set! s "compiling" false)
|
||||
(dict-set! s "current-def" nil)
|
||||
(dict-set! s "base" 10)
|
||||
(dict-set! s "vars" (dict))
|
||||
s)))
|
||||
(forth-sb-new)
|
||||
(let
|
||||
((sb (dict)))
|
||||
(dict-set! sb "_forth_sb" true)
|
||||
(dict-set! sb "_chars" (list))
|
||||
sb))
|
||||
|
||||
(define (forth-sb? v) (and (dict? v) (dict-has? v "_forth_sb")))
|
||||
|
||||
;; EMIT — append one character
|
||||
(define
|
||||
(forth-sb-emit! sb c)
|
||||
(dict-set! sb "_chars" (append (get sb "_chars") (list c)))
|
||||
sb)
|
||||
|
||||
;; TYPE — append a string
|
||||
(define
|
||||
(forth-sb-type! sb s)
|
||||
(dict-set! sb "_chars" (append (get sb "_chars") (string->list s)))
|
||||
sb)
|
||||
|
||||
(define (forth-sb-value sb) (list->string (get sb "_chars")))
|
||||
|
||||
(define (forth-sb-length sb) (len (get sb "_chars")))
|
||||
|
||||
(define (forth-sb-clear! sb) (dict-set! sb "_chars" (list)) sb)
|
||||
|
||||
;; Emit integer as decimal digits
|
||||
(define (forth-sb-emit-int! sb n) (forth-sb-type! sb (str (truncate n))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Memory / Bytevectors — Forth raw memory model
|
||||
;; ALLOT allocates a bytevector. Byte and cell (32-bit LE) access.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; ALLOT — allocate n bytes zero-initialised
|
||||
(define (forth-mem-new n) (make-bytevector (truncate n) 0))
|
||||
|
||||
(define (forth-mem? v) (bytevector? v))
|
||||
|
||||
(define (forth-mem-size v) (bytevector-length v))
|
||||
|
||||
;; C@ C! — byte fetch/store
|
||||
(define (forth-cfetch mem addr) (bytevector-u8-ref mem (truncate addr)))
|
||||
|
||||
(define
|
||||
forth-error
|
||||
(fn (state msg) (dict-set! state "error" msg) (raise msg)))
|
||||
(forth-cstore mem addr val)
|
||||
(bytevector-u8-set!
|
||||
mem
|
||||
(truncate addr)
|
||||
(modulo (truncate val) 256))
|
||||
mem)
|
||||
|
||||
;; @ ! — 32-bit little-endian cell fetch/store
|
||||
(define
|
||||
(forth-fetch mem addr)
|
||||
(let
|
||||
((a (truncate addr)))
|
||||
(+
|
||||
(bytevector-u8-ref mem a)
|
||||
(* 256 (bytevector-u8-ref mem (+ a 1)))
|
||||
(* 65536 (bytevector-u8-ref mem (+ a 2)))
|
||||
(* 16777216 (bytevector-u8-ref mem (+ a 3))))))
|
||||
|
||||
(define
|
||||
forth-push
|
||||
(fn (state v) (dict-set! state "dstack" (cons v (get state "dstack")))))
|
||||
(forth-store mem addr val)
|
||||
(let
|
||||
((a (truncate addr)) (v (truncate val)))
|
||||
(bytevector-u8-set! mem a (modulo v 256))
|
||||
(bytevector-u8-set!
|
||||
mem
|
||||
(+ a 1)
|
||||
(modulo (quotient v 256) 256))
|
||||
(bytevector-u8-set!
|
||||
mem
|
||||
(+ a 2)
|
||||
(modulo (quotient v 65536) 256))
|
||||
(bytevector-u8-set!
|
||||
mem
|
||||
(+ a 3)
|
||||
(modulo (quotient v 16777216) 256)))
|
||||
mem)
|
||||
|
||||
;; MOVE — copy count bytes from src[src-addr] to dst[dst-addr]
|
||||
(define
|
||||
forth-pop
|
||||
(fn
|
||||
(state)
|
||||
(let
|
||||
((st (get state "dstack")))
|
||||
(if
|
||||
(= (len st) 0)
|
||||
(forth-error state "stack underflow")
|
||||
(let ((top (first st))) (dict-set! state "dstack" (rest st)) top)))))
|
||||
(forth-move! src src-addr dst dst-addr count)
|
||||
(letrec
|
||||
((go (fn (i) (when (< i (truncate count)) (bytevector-u8-set! dst (+ (truncate dst-addr) i) (bytevector-u8-ref src (+ (truncate src-addr) i))) (go (+ i 1))))))
|
||||
(go 0))
|
||||
dst)
|
||||
|
||||
;; FILL — fill count bytes at addr with byte value
|
||||
(define
|
||||
forth-peek
|
||||
(fn
|
||||
(state)
|
||||
(let
|
||||
((st (get state "dstack")))
|
||||
(if (= (len st) 0) (forth-error state "stack underflow") (first st)))))
|
||||
|
||||
(define forth-depth (fn (state) (len (get state "dstack"))))
|
||||
(forth-fill! mem addr count byte)
|
||||
(letrec
|
||||
((go (fn (i) (when (< i (truncate count)) (bytevector-u8-set! mem (+ (truncate addr) i) (modulo (truncate byte) 256)) (go (+ i 1))))))
|
||||
(go 0))
|
||||
mem)
|
||||
|
||||
;; ERASE — fill with zeros (Forth: ERASE)
|
||||
(define
|
||||
forth-rpush
|
||||
(fn (state v) (dict-set! state "rstack" (cons v (get state "rstack")))))
|
||||
(forth-erase! mem addr count)
|
||||
(forth-fill! mem addr count 0))
|
||||
|
||||
;; Dump memory region as list of byte values
|
||||
(define
|
||||
forth-rpop
|
||||
(fn
|
||||
(state)
|
||||
(let
|
||||
((st (get state "rstack")))
|
||||
(if
|
||||
(= (len st) 0)
|
||||
(forth-error state "return stack underflow")
|
||||
(let ((top (first st))) (dict-set! state "rstack" (rest st)) top)))))
|
||||
|
||||
(define
|
||||
forth-rpeek
|
||||
(fn
|
||||
(state)
|
||||
(let
|
||||
((st (get state "rstack")))
|
||||
(if
|
||||
(= (len st) 0)
|
||||
(forth-error state "return stack underflow")
|
||||
(first st)))))
|
||||
|
||||
(define
|
||||
forth-emit-str
|
||||
(fn (state s) (dict-set! state "output" (str (get state "output") s))))
|
||||
|
||||
(define
|
||||
forth-make-word
|
||||
(fn
|
||||
(kind body immediate?)
|
||||
(let
|
||||
((w (dict)))
|
||||
(dict-set! w "kind" kind)
|
||||
(dict-set! w "body" body)
|
||||
(dict-set! w "immediate?" immediate?)
|
||||
w)))
|
||||
|
||||
(define
|
||||
forth-def-prim!
|
||||
(fn
|
||||
(state name body)
|
||||
(dict-set!
|
||||
(get state "dict")
|
||||
(downcase name)
|
||||
(forth-make-word "primitive" body false))))
|
||||
|
||||
(define
|
||||
forth-def-prim-imm!
|
||||
(fn
|
||||
(state name body)
|
||||
(dict-set!
|
||||
(get state "dict")
|
||||
(downcase name)
|
||||
(forth-make-word "primitive" body true))))
|
||||
|
||||
(define
|
||||
forth-lookup
|
||||
(fn (state name) (get (get state "dict") (downcase name))))
|
||||
|
||||
(define
|
||||
forth-binop
|
||||
(fn
|
||||
(op)
|
||||
(fn
|
||||
(state)
|
||||
(let
|
||||
((b (forth-pop state)) (a (forth-pop state)))
|
||||
(forth-push state (op a b))))))
|
||||
|
||||
(define
|
||||
forth-unop
|
||||
(fn
|
||||
(op)
|
||||
(fn (state) (let ((a (forth-pop state))) (forth-push state (op a))))))
|
||||
|
||||
(define
|
||||
forth-cmp
|
||||
(fn
|
||||
(op)
|
||||
(fn
|
||||
(state)
|
||||
(let
|
||||
((b (forth-pop state)) (a (forth-pop state)))
|
||||
(forth-push state (if (op a b) -1 0))))))
|
||||
|
||||
(define
|
||||
forth-cmp0
|
||||
(fn
|
||||
(op)
|
||||
(fn
|
||||
(state)
|
||||
(let ((a (forth-pop state))) (forth-push state (if (op a) -1 0))))))
|
||||
|
||||
(define
|
||||
forth-trunc
|
||||
(fn (x) (if (< x 0) (- 0 (floor (- 0 x))) (floor x))))
|
||||
|
||||
(define
|
||||
forth-div
|
||||
(fn
|
||||
(a b)
|
||||
(if (= b 0) (raise "division by zero") (forth-trunc (/ a b)))))
|
||||
|
||||
(define
|
||||
forth-mod
|
||||
(fn
|
||||
(a b)
|
||||
(if (= b 0) (raise "division by zero") (- a (* b (forth-div a b))))))
|
||||
|
||||
(define forth-bits-width 32)
|
||||
|
||||
(define
|
||||
forth-to-unsigned
|
||||
(fn (n w) (let ((m (pow 2 w))) (mod (+ (mod n m) m) m))))
|
||||
|
||||
(define
|
||||
forth-from-unsigned
|
||||
(fn
|
||||
(n w)
|
||||
(let ((half (pow 2 (- w 1)))) (if (>= n half) (- n (pow 2 w)) n))))
|
||||
|
||||
(define
|
||||
forth-bitwise-step
|
||||
(fn
|
||||
(op ua ub out place i w)
|
||||
(if
|
||||
(>= i w)
|
||||
out
|
||||
(let
|
||||
((da (mod ua 2)) (db (mod ub 2)))
|
||||
(forth-bitwise-step
|
||||
op
|
||||
(floor (/ ua 2))
|
||||
(floor (/ ub 2))
|
||||
(+ out (* place (op da db)))
|
||||
(* place 2)
|
||||
(+ i 1)
|
||||
w)))))
|
||||
|
||||
(define
|
||||
forth-bitwise-uu
|
||||
(fn
|
||||
(op)
|
||||
(fn
|
||||
(a b)
|
||||
(let
|
||||
((ua (forth-to-unsigned a forth-bits-width))
|
||||
(ub (forth-to-unsigned b forth-bits-width)))
|
||||
(forth-from-unsigned
|
||||
(forth-bitwise-step op ua ub 0 1 0 forth-bits-width)
|
||||
forth-bits-width)))))
|
||||
|
||||
(define
|
||||
forth-bit-and
|
||||
(forth-bitwise-uu (fn (x y) (if (and (= x 1) (= y 1)) 1 0))))
|
||||
|
||||
(define
|
||||
forth-bit-or
|
||||
(forth-bitwise-uu (fn (x y) (if (or (= x 1) (= y 1)) 1 0))))
|
||||
|
||||
(define forth-bit-xor (forth-bitwise-uu (fn (x y) (if (= x y) 0 1))))
|
||||
|
||||
(define forth-bit-invert (fn (a) (- 0 (+ a 1))))
|
||||
|
||||
(define
|
||||
forth-install-primitives!
|
||||
(fn
|
||||
(state)
|
||||
(forth-def-prim! state "DUP" (fn (s) (forth-push s (forth-peek s))))
|
||||
(forth-def-prim! state "DROP" (fn (s) (forth-pop s)))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"SWAP"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((b (forth-pop s)) (a (forth-pop s)))
|
||||
(forth-push s b)
|
||||
(forth-push s a))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"OVER"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((b (forth-pop s)) (a (forth-pop s)))
|
||||
(forth-push s a)
|
||||
(forth-push s b)
|
||||
(forth-push s a))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"ROT"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((c (forth-pop s)) (b (forth-pop s)) (a (forth-pop s)))
|
||||
(forth-push s b)
|
||||
(forth-push s c)
|
||||
(forth-push s a))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"-ROT"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((c (forth-pop s)) (b (forth-pop s)) (a (forth-pop s)))
|
||||
(forth-push s c)
|
||||
(forth-push s a)
|
||||
(forth-push s b))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"NIP"
|
||||
(fn (s) (let ((b (forth-pop s))) (forth-pop s) (forth-push s b))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"TUCK"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((b (forth-pop s)) (a (forth-pop s)))
|
||||
(forth-push s b)
|
||||
(forth-push s a)
|
||||
(forth-push s b))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"?DUP"
|
||||
(fn
|
||||
(s)
|
||||
(let ((a (forth-peek s))) (when (not (= a 0)) (forth-push s a)))))
|
||||
(forth-def-prim! state "DEPTH" (fn (s) (forth-push s (forth-depth s))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"PICK"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((n (forth-pop s)) (st (get s "dstack")))
|
||||
(if
|
||||
(or (< n 0) (>= n (len st)))
|
||||
(forth-error s "PICK out of range")
|
||||
(forth-push s (nth st n))))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"ROLL"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((n (forth-pop s)) (st (get s "dstack")))
|
||||
(if
|
||||
(or (< n 0) (>= n (len st)))
|
||||
(forth-error s "ROLL out of range")
|
||||
(let
|
||||
((taken (nth st n))
|
||||
(before (take st n))
|
||||
(after (drop st (+ n 1))))
|
||||
(dict-set! s "dstack" (concat before after))
|
||||
(forth-push s taken))))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"2DUP"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((b (forth-pop s)) (a (forth-pop s)))
|
||||
(forth-push s a)
|
||||
(forth-push s b)
|
||||
(forth-push s a)
|
||||
(forth-push s b))))
|
||||
(forth-def-prim! state "2DROP" (fn (s) (forth-pop s) (forth-pop s)))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"2SWAP"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((d (forth-pop s))
|
||||
(c (forth-pop s))
|
||||
(b (forth-pop s))
|
||||
(a (forth-pop s)))
|
||||
(forth-push s c)
|
||||
(forth-push s d)
|
||||
(forth-push s a)
|
||||
(forth-push s b))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"2OVER"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((d (forth-pop s))
|
||||
(c (forth-pop s))
|
||||
(b (forth-pop s))
|
||||
(a (forth-pop s)))
|
||||
(forth-push s a)
|
||||
(forth-push s b)
|
||||
(forth-push s c)
|
||||
(forth-push s d)
|
||||
(forth-push s a)
|
||||
(forth-push s b))))
|
||||
(forth-def-prim! state "+" (forth-binop (fn (a b) (+ a b))))
|
||||
(forth-def-prim! state "-" (forth-binop (fn (a b) (- a b))))
|
||||
(forth-def-prim! state "*" (forth-binop (fn (a b) (* a b))))
|
||||
(forth-def-prim! state "/" (forth-binop forth-div))
|
||||
(forth-def-prim! state "MOD" (forth-binop forth-mod))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"/MOD"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((b (forth-pop s)) (a (forth-pop s)))
|
||||
(forth-push s (forth-mod a b))
|
||||
(forth-push s (forth-div a b)))))
|
||||
(forth-def-prim! state "NEGATE" (forth-unop (fn (a) (- 0 a))))
|
||||
(forth-def-prim! state "ABS" (forth-unop abs))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"MIN"
|
||||
(forth-binop (fn (a b) (if (< a b) a b))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"MAX"
|
||||
(forth-binop (fn (a b) (if (> a b) a b))))
|
||||
(forth-def-prim! state "1+" (forth-unop (fn (a) (+ a 1))))
|
||||
(forth-def-prim! state "1-" (forth-unop (fn (a) (- a 1))))
|
||||
(forth-def-prim! state "2+" (forth-unop (fn (a) (+ a 2))))
|
||||
(forth-def-prim! state "2-" (forth-unop (fn (a) (- a 2))))
|
||||
(forth-def-prim! state "2*" (forth-unop (fn (a) (* a 2))))
|
||||
(forth-def-prim! state "2/" (forth-unop (fn (a) (floor (/ a 2)))))
|
||||
(forth-def-prim! state "=" (forth-cmp (fn (a b) (= a b))))
|
||||
(forth-def-prim! state "<>" (forth-cmp (fn (a b) (not (= a b)))))
|
||||
(forth-def-prim! state "<" (forth-cmp (fn (a b) (< a b))))
|
||||
(forth-def-prim! state ">" (forth-cmp (fn (a b) (> a b))))
|
||||
(forth-def-prim! state "<=" (forth-cmp (fn (a b) (<= a b))))
|
||||
(forth-def-prim! state ">=" (forth-cmp (fn (a b) (>= a b))))
|
||||
(forth-def-prim! state "0=" (forth-cmp0 (fn (a) (= a 0))))
|
||||
(forth-def-prim! state "0<>" (forth-cmp0 (fn (a) (not (= a 0)))))
|
||||
(forth-def-prim! state "0<" (forth-cmp0 (fn (a) (< a 0))))
|
||||
(forth-def-prim! state "0>" (forth-cmp0 (fn (a) (> a 0))))
|
||||
(forth-def-prim! state "AND" (forth-binop forth-bit-and))
|
||||
(forth-def-prim! state "OR" (forth-binop forth-bit-or))
|
||||
(forth-def-prim! state "XOR" (forth-binop forth-bit-xor))
|
||||
(forth-def-prim! state "INVERT" (forth-unop forth-bit-invert))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"."
|
||||
(fn (s) (forth-emit-str s (str (forth-pop s) " "))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
".S"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((st (reverse (get s "dstack"))))
|
||||
(forth-emit-str s "<")
|
||||
(forth-emit-str s (str (len st)))
|
||||
(forth-emit-str s "> ")
|
||||
(for-each (fn (v) (forth-emit-str s (str v " "))) st))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"EMIT"
|
||||
(fn (s) (forth-emit-str s (code-char (forth-pop s)))))
|
||||
(forth-def-prim! state "CR" (fn (s) (forth-emit-str s "\n")))
|
||||
(forth-def-prim! state "SPACE" (fn (s) (forth-emit-str s " ")))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"SPACES"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((n (forth-pop s)))
|
||||
(when
|
||||
(> n 0)
|
||||
(for-each (fn (_) (forth-emit-str s " ")) (range 0 n))))))
|
||||
(forth-def-prim! state "BL" (fn (s) (forth-push s 32)))
|
||||
state))
|
||||
(forth-mem->list mem addr count)
|
||||
(letrec
|
||||
((go (fn (i acc) (if (= i 0) acc (go (- i 1) (cons (bytevector-u8-ref mem (+ (truncate addr) (- i 1))) acc))))))
|
||||
(go (truncate count) (list))))
|
||||
|
||||
62
lib/forth/test.sh
Executable file
62
lib/forth/test.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/forth/test.sh — smoke-test the Forth runtime layer.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "lib/forth/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/forth/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(list forth-test-pass forth-test-fail)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}')
|
||||
if [ -z "$LINE" ]; then
|
||||
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 3 //; s/\)$//')
|
||||
fi
|
||||
if [ -z "$LINE" ]; then
|
||||
echo "ERROR: could not extract summary"
|
||||
echo "$OUTPUT" | tail -20
|
||||
exit 1
|
||||
fi
|
||||
|
||||
P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/')
|
||||
F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/')
|
||||
TOTAL=$((P + F))
|
||||
|
||||
if [ "$F" -eq 0 ]; then
|
||||
echo "ok $P/$TOTAL lib/forth tests passed"
|
||||
else
|
||||
echo "FAIL $P/$TOTAL passed, $F failed"
|
||||
TMPFILE2=$(mktemp)
|
||||
cat > "$TMPFILE2" << 'EPOCHS2'
|
||||
(epoch 1)
|
||||
(load "lib/forth/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/forth/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(map (fn (f) (list (get f :name) (get f :got) (get f :expected))) forth-test-fails)")
|
||||
EPOCHS2
|
||||
FAILS=$(timeout 60 "$SX_SERVER" < "$TMPFILE2" 2>/dev/null | grep -E '^\(ok-len 3' -A1 | tail -1 || true)
|
||||
echo " Details: $FAILS"
|
||||
rm -f "$TMPFILE2"
|
||||
fi
|
||||
|
||||
[ "$F" -eq 0 ]
|
||||
201
lib/forth/tests/runtime.sx
Normal file
201
lib/forth/tests/runtime.sx
Normal file
@@ -0,0 +1,201 @@
|
||||
;; lib/forth/tests/runtime.sx — Tests for lib/forth/runtime.sx
|
||||
|
||||
(define forth-test-pass 0)
|
||||
(define forth-test-fail 0)
|
||||
(define forth-test-fails (list))
|
||||
|
||||
(define
|
||||
(forth-test name got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! forth-test-pass (+ forth-test-pass 1))
|
||||
(begin
|
||||
(set! forth-test-fail (+ forth-test-fail 1))
|
||||
(set! forth-test-fails (append forth-test-fails (list {:got got :expected expected :name name}))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Bitwise operations
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; AND
|
||||
(forth-test "and 0b1100 0b1010" (forth-and 12 10) 8)
|
||||
(forth-test "and 0xFF 0x0F" (forth-and 255 15) 15)
|
||||
(forth-test "and 0 any" (forth-and 0 42) 0)
|
||||
|
||||
;; OR
|
||||
(forth-test "or 0b1100 0b1010" (forth-or 12 10) 14)
|
||||
(forth-test "or 0 x" (forth-or 0 7) 7)
|
||||
|
||||
;; XOR
|
||||
(forth-test "xor 0b1100 0b1010" (forth-xor 12 10) 6)
|
||||
(forth-test "xor x x" (forth-xor 42 42) 0)
|
||||
|
||||
;; INVERT
|
||||
(forth-test "invert 0" (forth-invert 0) -1)
|
||||
(forth-test "invert -1" (forth-invert -1) 0)
|
||||
(forth-test "invert 1" (forth-invert 1) -2)
|
||||
|
||||
;; LSHIFT RSHIFT
|
||||
(forth-test "lshift 1 3" (forth-lshift 1 3) 8)
|
||||
(forth-test "lshift 3 2" (forth-lshift 3 2) 12)
|
||||
(forth-test "rshift 8 3" (forth-rshift 8 3) 1)
|
||||
(forth-test "rshift 16 2" (forth-rshift 16 2) 4)
|
||||
|
||||
;; 2* 2/
|
||||
(forth-test "2* 5" (forth-2* 5) 10)
|
||||
(forth-test "2/ 10" (forth-2/ 10) 5)
|
||||
(forth-test "2/ 7" (forth-2/ 7) 3)
|
||||
|
||||
;; BIT-COUNT
|
||||
(forth-test "bit-count 0" (forth-bit-count 0) 0)
|
||||
(forth-test "bit-count 1" (forth-bit-count 1) 1)
|
||||
(forth-test "bit-count 7" (forth-bit-count 7) 3)
|
||||
(forth-test "bit-count 255" (forth-bit-count 255) 8)
|
||||
(forth-test "bit-count 256" (forth-bit-count 256) 1)
|
||||
|
||||
;; INTEGER-LENGTH
|
||||
(forth-test "integer-length 0" (forth-integer-length 0) 0)
|
||||
(forth-test "integer-length 1" (forth-integer-length 1) 1)
|
||||
(forth-test "integer-length 4" (forth-integer-length 4) 3)
|
||||
(forth-test "integer-length 255" (forth-integer-length 255) 8)
|
||||
|
||||
;; WITHIN
|
||||
(forth-test
|
||||
"within 5 0 10"
|
||||
(forth-within 5 0 10)
|
||||
true)
|
||||
(forth-test
|
||||
"within 0 0 10"
|
||||
(forth-within 0 0 10)
|
||||
true)
|
||||
(forth-test
|
||||
"within 10 0 10"
|
||||
(forth-within 10 0 10)
|
||||
false)
|
||||
(forth-test
|
||||
"within -1 0 10"
|
||||
(forth-within -1 0 10)
|
||||
false)
|
||||
|
||||
;; Arithmetic ops
|
||||
(forth-test "negate 5" (forth-negate 5) -5)
|
||||
(forth-test "negate -3" (forth-negate -3) 3)
|
||||
(forth-test "abs -7" (forth-abs -7) 7)
|
||||
(forth-test "min 3 5" (forth-min 3 5) 3)
|
||||
(forth-test "max 3 5" (forth-max 3 5) 5)
|
||||
(forth-test "mod 7 3" (forth-mod 7 3) 1)
|
||||
(forth-test
|
||||
"divmod 7 3"
|
||||
(forth-divmod 7 3)
|
||||
(list 1 2))
|
||||
(forth-test
|
||||
"divmod 10 5"
|
||||
(forth-divmod 10 5)
|
||||
(list 0 2))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. String buffer
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define sb1 (forth-sb-new))
|
||||
(forth-test "sb? new" (forth-sb? sb1) true)
|
||||
(forth-test "sb? non-sb" (forth-sb? 42) false)
|
||||
(forth-test "sb value empty" (forth-sb-value sb1) "")
|
||||
(forth-test "sb length empty" (forth-sb-length sb1) 0)
|
||||
|
||||
(forth-sb-type! sb1 "HELLO")
|
||||
(forth-test "sb type" (forth-sb-value sb1) "HELLO")
|
||||
(forth-test "sb length after type" (forth-sb-length sb1) 5)
|
||||
|
||||
;; EMIT one char
|
||||
(define sb2 (forth-sb-new))
|
||||
(forth-sb-emit! sb2 (nth (string->list "A") 0))
|
||||
(forth-sb-emit! sb2 (nth (string->list "B") 0))
|
||||
(forth-sb-emit! sb2 (nth (string->list "C") 0))
|
||||
(forth-test "sb emit chars" (forth-sb-value sb2) "ABC")
|
||||
|
||||
;; Emit integer
|
||||
(define sb3 (forth-sb-new))
|
||||
(forth-sb-type! sb3 "n=")
|
||||
(forth-sb-emit-int! sb3 42)
|
||||
(forth-test "sb emit-int" (forth-sb-value sb3) "n=42")
|
||||
|
||||
(forth-sb-clear! sb1)
|
||||
(forth-test "sb clear" (forth-sb-value sb1) "")
|
||||
(forth-test "sb length after clear" (forth-sb-length sb1) 0)
|
||||
|
||||
;; Build a word definition-style name
|
||||
(define sb4 (forth-sb-new))
|
||||
(forth-sb-type! sb4 ": ")
|
||||
(forth-sb-type! sb4 "SQUARE")
|
||||
(forth-sb-type! sb4 " DUP * ;")
|
||||
(forth-test "sb word def" (forth-sb-value sb4) ": SQUARE DUP * ;")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Memory / Bytevectors
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define m1 (forth-mem-new 8))
|
||||
(forth-test "mem? yes" (forth-mem? m1) true)
|
||||
(forth-test "mem? no" (forth-mem? 42) false)
|
||||
(forth-test "mem size" (forth-mem-size m1) 8)
|
||||
(forth-test "mem cfetch zero" (forth-cfetch m1 0) 0)
|
||||
|
||||
;; C! C@
|
||||
(forth-cstore m1 0 65)
|
||||
(forth-cstore m1 1 66)
|
||||
(forth-test "mem cstore/cfetch 0" (forth-cfetch m1 0) 65)
|
||||
(forth-test "mem cstore/cfetch 1" (forth-cfetch m1 1) 66)
|
||||
(forth-cstore m1 2 256)
|
||||
(forth-test
|
||||
"mem cstore wraps 256→0"
|
||||
(forth-cfetch m1 2)
|
||||
0)
|
||||
(forth-cstore m1 2 257)
|
||||
(forth-test
|
||||
"mem cstore wraps 257→1"
|
||||
(forth-cfetch m1 2)
|
||||
1)
|
||||
|
||||
;; @ ! (32-bit LE cell)
|
||||
(define m2 (forth-mem-new 8))
|
||||
(forth-store m2 0 305419896)
|
||||
(forth-test "mem store/fetch" (forth-fetch m2 0) 305419896)
|
||||
(forth-store m2 4 1)
|
||||
(forth-test "mem fetch byte 4" (forth-cfetch m2 4) 1)
|
||||
(forth-test "mem fetch byte 5" (forth-cfetch m2 5) 0)
|
||||
|
||||
;; FILL ERASE
|
||||
(define m3 (forth-mem-new 4))
|
||||
(forth-fill! m3 0 4 42)
|
||||
(forth-test
|
||||
"mem fill"
|
||||
(forth-mem->list m3 0 4)
|
||||
(list 42 42 42 42))
|
||||
(forth-erase! m3 1 2)
|
||||
(forth-test
|
||||
"mem erase middle"
|
||||
(forth-mem->list m3 0 4)
|
||||
(list 42 0 0 42))
|
||||
|
||||
;; MOVE
|
||||
(define m4 (forth-mem-new 4))
|
||||
(forth-cstore m4 0 1)
|
||||
(forth-cstore m4 1 2)
|
||||
(forth-cstore m4 2 3)
|
||||
(define m5 (forth-mem-new 4))
|
||||
(forth-move! m4 0 m5 0 3)
|
||||
(forth-test
|
||||
"mem move"
|
||||
(forth-mem->list m5 0 3)
|
||||
(list 1 2 3))
|
||||
|
||||
;; mem->list
|
||||
(define m6 (forth-mem-new 3))
|
||||
(forth-cstore m6 0 10)
|
||||
(forth-cstore m6 1 20)
|
||||
(forth-cstore m6 2 30)
|
||||
(forth-test
|
||||
"mem->list"
|
||||
(forth-mem->list m6 0 3)
|
||||
(list 10 20 30))
|
||||
507
lib/haskell/runtime.sx
Normal file
507
lib/haskell/runtime.sx
Normal file
@@ -0,0 +1,507 @@
|
||||
;; lib/haskell/runtime.sx — Haskell-on-SX runtime layer
|
||||
;;
|
||||
;; Covers the Haskell primitives now reachable via SX spec:
|
||||
;; 1. Numeric type class helpers (Num / Integral / Fractional)
|
||||
;; 2. Rational numbers (dict-based: {:_rational true :num n :den d})
|
||||
;; 3. Lazy evaluation — hk-force for promises created by delay
|
||||
;; 4. Char utilities (Data.Char)
|
||||
;; 5. Data.Set wrappers
|
||||
;; 6. Data.List utilities
|
||||
;; 7. Maybe / Either ADTs
|
||||
;; 8. Tuples (lists, since list->vector unreliable in sx_server)
|
||||
;; 9. String helpers (words/lines/isPrefixOf/etc.)
|
||||
;; 10. Show helper
|
||||
|
||||
;; ===========================================================================
|
||||
;; 1. Numeric type class helpers
|
||||
;; ===========================================================================
|
||||
|
||||
(define hk-is-integer? integer?)
|
||||
(define hk-is-float? float?)
|
||||
(define hk-is-num? number?)
|
||||
|
||||
;; fromIntegral — coerce integer to Float
|
||||
(define (hk-to-float x) (exact->inexact x))
|
||||
|
||||
;; truncate / round toward zero
|
||||
(define hk-to-integer truncate)
|
||||
(define hk-from-integer (fn (n) n))
|
||||
|
||||
;; Haskell div: floor division (rounds toward -inf)
|
||||
(define
|
||||
(hk-div a b)
|
||||
(let
|
||||
((q (quotient a b)) (r (remainder a b)))
|
||||
(if
|
||||
(and
|
||||
(not (= r 0))
|
||||
(or
|
||||
(and (< a 0) (> b 0))
|
||||
(and (> a 0) (< b 0))))
|
||||
(- q 1)
|
||||
q)))
|
||||
|
||||
;; Haskell mod: result has same sign as divisor
|
||||
(define hk-mod modulo)
|
||||
|
||||
;; Haskell rem: result has same sign as dividend
|
||||
(define hk-rem remainder)
|
||||
|
||||
;; Haskell quot: truncation division
|
||||
(define hk-quot quotient)
|
||||
|
||||
;; divMod and quotRem return pairs (lists)
|
||||
(define (hk-div-mod a b) (list (hk-div a b) (hk-mod a b)))
|
||||
(define (hk-quot-rem a b) (list (hk-quot a b) (hk-rem a b)))
|
||||
|
||||
(define (hk-abs x) (if (< x 0) (- 0 x) x))
|
||||
(define
|
||||
(hk-signum x)
|
||||
(cond
|
||||
((> x 0) 1)
|
||||
((< x 0) -1)
|
||||
(else 0)))
|
||||
|
||||
(define hk-gcd gcd)
|
||||
(define hk-lcm lcm)
|
||||
|
||||
(define (hk-even? n) (= (modulo n 2) 0))
|
||||
(define (hk-odd? n) (not (= (modulo n 2) 0)))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 2. Rational numbers (dict implementation — no built-in rational in sx_server)
|
||||
;; ===========================================================================
|
||||
|
||||
(define
|
||||
(hk-make-rational n d)
|
||||
(let
|
||||
((g (gcd (hk-abs n) (hk-abs d))))
|
||||
(if (< d 0) {:num (quotient (- 0 n) g) :den (quotient (- 0 d) g) :_rational true} {:num (quotient n g) :den (quotient d g) :_rational true})))
|
||||
|
||||
(define
|
||||
(hk-rational? x)
|
||||
(and (dict? x) (not (= (get x :_rational) nil))))
|
||||
(define (hk-numerator r) (get r :num))
|
||||
(define (hk-denominator r) (get r :den))
|
||||
|
||||
(define
|
||||
(hk-rational-add r1 r2)
|
||||
(hk-make-rational
|
||||
(+
|
||||
(* (hk-numerator r1) (hk-denominator r2))
|
||||
(* (hk-numerator r2) (hk-denominator r1)))
|
||||
(* (hk-denominator r1) (hk-denominator r2))))
|
||||
|
||||
(define
|
||||
(hk-rational-sub r1 r2)
|
||||
(hk-make-rational
|
||||
(-
|
||||
(* (hk-numerator r1) (hk-denominator r2))
|
||||
(* (hk-numerator r2) (hk-denominator r1)))
|
||||
(* (hk-denominator r1) (hk-denominator r2))))
|
||||
|
||||
(define
|
||||
(hk-rational-mul r1 r2)
|
||||
(hk-make-rational
|
||||
(* (hk-numerator r1) (hk-numerator r2))
|
||||
(* (hk-denominator r1) (hk-denominator r2))))
|
||||
|
||||
(define
|
||||
(hk-rational-div r1 r2)
|
||||
(hk-make-rational
|
||||
(* (hk-numerator r1) (hk-denominator r2))
|
||||
(* (hk-denominator r1) (hk-numerator r2))))
|
||||
|
||||
(define
|
||||
(hk-rational-to-float r)
|
||||
(exact->inexact (/ (hk-numerator r) (hk-denominator r))))
|
||||
|
||||
(define (hk-show-rational r) (str (hk-numerator r) "%" (hk-denominator r)))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 3. Lazy evaluation — promises (created via SX delay)
|
||||
;; ===========================================================================
|
||||
|
||||
(define
|
||||
(hk-force p)
|
||||
(if
|
||||
(and (dict? p) (not (= (get p :_promise) nil)))
|
||||
(if (get p :forced) (get p :value) ((get p :thunk)))
|
||||
p))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 4. Char utilities (Data.Char)
|
||||
;; ===========================================================================
|
||||
|
||||
(define hk-ord char->integer)
|
||||
(define hk-chr integer->char)
|
||||
|
||||
;; Inline ASCII predicates — char-alphabetic?/char-numeric? unreliable in sx_server
|
||||
(define
|
||||
(hk-is-alpha? c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(or
|
||||
(and (>= n 65) (<= n 90))
|
||||
(and (>= n 97) (<= n 122)))))
|
||||
|
||||
(define
|
||||
(hk-is-digit? c)
|
||||
(let ((n (char->integer c))) (and (>= n 48) (<= n 57))))
|
||||
|
||||
(define
|
||||
(hk-is-alnum? c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(or
|
||||
(and (>= n 48) (<= n 57))
|
||||
(and (>= n 65) (<= n 90))
|
||||
(and (>= n 97) (<= n 122)))))
|
||||
|
||||
(define
|
||||
(hk-is-upper? c)
|
||||
(let ((n (char->integer c))) (and (>= n 65) (<= n 90))))
|
||||
|
||||
(define
|
||||
(hk-is-lower? c)
|
||||
(let ((n (char->integer c))) (and (>= n 97) (<= n 122))))
|
||||
|
||||
(define
|
||||
(hk-is-space? c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(or
|
||||
(= n 32)
|
||||
(= n 9)
|
||||
(= n 10)
|
||||
(= n 13)
|
||||
(= n 12)
|
||||
(= n 11))))
|
||||
|
||||
(define hk-to-upper char-upcase)
|
||||
(define hk-to-lower char-downcase)
|
||||
|
||||
;; digitToInt: '0'-'9' → 0-9, 'a'-'f'/'A'-'F' → 10-15
|
||||
(define
|
||||
(hk-digit-to-int c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(cond
|
||||
((and (>= n 48) (<= n 57)) (- n 48))
|
||||
((and (>= n 65) (<= n 70)) (- n 55))
|
||||
((and (>= n 97) (<= n 102)) (- n 87))
|
||||
(else (error (str "hk-digit-to-int: not a hex digit: " c))))))
|
||||
|
||||
;; intToDigit: 0-15 → char
|
||||
(define
|
||||
(hk-int-to-digit n)
|
||||
(cond
|
||||
((and (>= n 0) (<= n 9))
|
||||
(integer->char (+ n 48)))
|
||||
((and (>= n 10) (<= n 15))
|
||||
(integer->char (+ n 87)))
|
||||
(else (error (str "hk-int-to-digit: out of range: " n)))))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 5. Data.Set wrappers
|
||||
;; ===========================================================================
|
||||
|
||||
(define (hk-set-empty) (make-set))
|
||||
(define hk-set? set?)
|
||||
(define hk-set-member? set-member?)
|
||||
|
||||
(define (hk-set-insert x s) (begin (set-add! s x) s))
|
||||
|
||||
(define (hk-set-delete x s) (begin (set-remove! s x) s))
|
||||
|
||||
(define hk-set-union set-union)
|
||||
(define hk-set-intersection set-intersection)
|
||||
(define hk-set-difference set-difference)
|
||||
(define hk-set-from-list list->set)
|
||||
(define hk-set-to-list set->list)
|
||||
(define (hk-set-null? s) (= (len (set->list s)) 0))
|
||||
(define (hk-set-size s) (len (set->list s)))
|
||||
|
||||
(define (hk-set-singleton x) (let ((s (make-set))) (set-add! s x) s))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 6. Data.List utilities
|
||||
;; ===========================================================================
|
||||
|
||||
(define hk-head first)
|
||||
(define hk-tail rest)
|
||||
(define (hk-null? lst) (= (len lst) 0))
|
||||
(define hk-length len)
|
||||
|
||||
(define
|
||||
(hk-take n lst)
|
||||
(if
|
||||
(or (= n 0) (= (len lst) 0))
|
||||
(list)
|
||||
(cons (first lst) (hk-take (- n 1) (rest lst)))))
|
||||
|
||||
(define
|
||||
(hk-drop n lst)
|
||||
(if
|
||||
(or (= n 0) (= (len lst) 0))
|
||||
lst
|
||||
(hk-drop (- n 1) (rest lst))))
|
||||
|
||||
(define
|
||||
(hk-take-while pred lst)
|
||||
(if
|
||||
(or (= (len lst) 0) (not (pred (first lst))))
|
||||
(list)
|
||||
(cons (first lst) (hk-take-while pred (rest lst)))))
|
||||
|
||||
(define
|
||||
(hk-drop-while pred lst)
|
||||
(if
|
||||
(or (= (len lst) 0) (not (pred (first lst))))
|
||||
lst
|
||||
(hk-drop-while pred (rest lst))))
|
||||
|
||||
(define
|
||||
(hk-zip a b)
|
||||
(if
|
||||
(or (= (len a) 0) (= (len b) 0))
|
||||
(list)
|
||||
(cons (list (first a) (first b)) (hk-zip (rest a) (rest b)))))
|
||||
|
||||
(define
|
||||
(hk-zip-with f a b)
|
||||
(if
|
||||
(or (= (len a) 0) (= (len b) 0))
|
||||
(list)
|
||||
(cons (f (first a) (first b)) (hk-zip-with f (rest a) (rest b)))))
|
||||
|
||||
(define
|
||||
(hk-unzip pairs)
|
||||
(list
|
||||
(map (fn (p) (first p)) pairs)
|
||||
(map (fn (p) (nth p 1)) pairs)))
|
||||
|
||||
(define
|
||||
(hk-elem x lst)
|
||||
(cond
|
||||
((= (len lst) 0) false)
|
||||
((= x (first lst)) true)
|
||||
(else (hk-elem x (rest lst)))))
|
||||
|
||||
(define (hk-not-elem x lst) (not (hk-elem x lst)))
|
||||
|
||||
(define
|
||||
(hk-nub lst)
|
||||
(letrec
|
||||
((go (fn (seen acc items) (if (= (len items) 0) (reverse acc) (let ((h (first items)) (t (rest items))) (if (hk-elem h seen) (go seen acc t) (go (cons h seen) (cons h acc) t)))))))
|
||||
(go (list) (list) lst)))
|
||||
|
||||
(define (hk-sum lst) (reduce + 0 lst))
|
||||
(define (hk-product lst) (reduce * 1 lst))
|
||||
|
||||
(define
|
||||
(hk-maximum lst)
|
||||
(reduce (fn (a b) (if (> a b) a b)) (first lst) (rest lst)))
|
||||
|
||||
(define
|
||||
(hk-minimum lst)
|
||||
(reduce (fn (a b) (if (< a b) a b)) (first lst) (rest lst)))
|
||||
|
||||
(define (hk-concat lsts) (reduce append (list) lsts))
|
||||
|
||||
(define (hk-concat-map f lst) (hk-concat (map f lst)))
|
||||
|
||||
(define hk-sort sort)
|
||||
|
||||
(define
|
||||
(hk-span pred lst)
|
||||
(list (hk-take-while pred lst) (hk-drop-while pred lst)))
|
||||
|
||||
(define (hk-break pred lst) (hk-span (fn (x) (not (pred x))) lst))
|
||||
|
||||
(define
|
||||
(hk-foldl f acc lst)
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
acc
|
||||
(hk-foldl f (f acc (first lst)) (rest lst))))
|
||||
|
||||
(define
|
||||
(hk-foldr f z lst)
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
z
|
||||
(f (first lst) (hk-foldr f z (rest lst)))))
|
||||
|
||||
(define
|
||||
(hk-scanl f acc lst)
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
(list acc)
|
||||
(cons acc (hk-scanl f (f acc (first lst)) (rest lst)))))
|
||||
|
||||
(define
|
||||
(hk-replicate n x)
|
||||
(if (= n 0) (list) (cons x (hk-replicate (- n 1) x))))
|
||||
|
||||
(define
|
||||
(hk-intersperse sep lst)
|
||||
(if
|
||||
(or (= (len lst) 0) (= (len lst) 1))
|
||||
lst
|
||||
(cons (first lst) (cons sep (hk-intersperse sep (rest lst))))))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 7. Maybe / Either ADTs
|
||||
;; ===========================================================================
|
||||
|
||||
(define hk-nothing {:_maybe true :_tag "nothing"})
|
||||
(define (hk-just x) {:_maybe true :value x :_tag "just"})
|
||||
(define (hk-is-nothing? m) (= (get m :_tag) "nothing"))
|
||||
(define (hk-is-just? m) (= (get m :_tag) "just"))
|
||||
(define (hk-from-just m) (get m :value))
|
||||
(define (hk-from-maybe def m) (if (hk-is-nothing? m) def (hk-from-just m)))
|
||||
(define
|
||||
(hk-maybe def f m)
|
||||
(if (hk-is-nothing? m) def (f (hk-from-just m))))
|
||||
|
||||
(define (hk-left x) {:value x :_either true :_tag "left"})
|
||||
(define (hk-right x) {:value x :_either true :_tag "right"})
|
||||
(define (hk-is-left? e) (= (get e :_tag) "left"))
|
||||
(define (hk-is-right? e) (= (get e :_tag) "right"))
|
||||
(define (hk-from-left e) (get e :value))
|
||||
(define (hk-from-right e) (get e :value))
|
||||
(define
|
||||
(hk-either f g e)
|
||||
(if (hk-is-left? e) (f (hk-from-left e)) (g (hk-from-right e))))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 8. Tuples (lists — list->vector unreliable in sx_server)
|
||||
;; ===========================================================================
|
||||
|
||||
(define (hk-pair a b) (list a b))
|
||||
(define hk-fst first)
|
||||
(define (hk-snd t) (nth t 1))
|
||||
|
||||
(define (hk-triple a b c) (list a b c))
|
||||
(define hk-fst3 first)
|
||||
(define (hk-snd3 t) (nth t 1))
|
||||
(define (hk-thd3 t) (nth t 2))
|
||||
|
||||
(define (hk-curry f) (fn (a) (fn (b) (f a b))))
|
||||
(define (hk-uncurry f) (fn (p) (f (hk-fst p) (hk-snd p))))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 9. String helpers (Data.List / Data.Char for strings)
|
||||
;; ===========================================================================
|
||||
|
||||
;; words: split on whitespace
|
||||
(define
|
||||
(hk-words s)
|
||||
(letrec
|
||||
((slen (len s))
|
||||
(skip-ws
|
||||
(fn
|
||||
(i)
|
||||
(if
|
||||
(>= i slen)
|
||||
(list)
|
||||
(let
|
||||
((c (substring s i (+ i 1))))
|
||||
(if
|
||||
(or (= c " ") (= c "\t") (= c "\n"))
|
||||
(skip-ws (+ i 1))
|
||||
(collect-word i (+ i 1)))))))
|
||||
(collect-word
|
||||
(fn
|
||||
(start i)
|
||||
(if
|
||||
(>= i slen)
|
||||
(list (substring s start i))
|
||||
(let
|
||||
((c (substring s i (+ i 1))))
|
||||
(if
|
||||
(or (= c " ") (= c "\t") (= c "\n"))
|
||||
(cons (substring s start i) (skip-ws (+ i 1)))
|
||||
(collect-word start (+ i 1))))))))
|
||||
(skip-ws 0)))
|
||||
|
||||
;; unwords: join with spaces
|
||||
(define
|
||||
(hk-unwords lst)
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
""
|
||||
(reduce (fn (a b) (str a " " b)) (first lst) (rest lst))))
|
||||
|
||||
;; lines: split on newline
|
||||
(define
|
||||
(hk-lines s)
|
||||
(letrec
|
||||
((slen (len s))
|
||||
(go
|
||||
(fn
|
||||
(start i acc)
|
||||
(if
|
||||
(>= i slen)
|
||||
(reverse (cons (substring s start i) acc))
|
||||
(if
|
||||
(= (substring s i (+ i 1)) "\n")
|
||||
(go
|
||||
(+ i 1)
|
||||
(+ i 1)
|
||||
(cons (substring s start i) acc))
|
||||
(go start (+ i 1) acc))))))
|
||||
(if (= slen 0) (list) (go 0 0 (list)))))
|
||||
|
||||
;; unlines: join, each with trailing newline
|
||||
(define (hk-unlines lst) (reduce (fn (a b) (str a b "\n")) "" lst))
|
||||
|
||||
;; isPrefixOf
|
||||
(define
|
||||
(hk-is-prefix-of pre s)
|
||||
(and (<= (len pre) (len s)) (= pre (substring s 0 (len pre)))))
|
||||
|
||||
;; isSuffixOf
|
||||
(define
|
||||
(hk-is-suffix-of suf s)
|
||||
(let
|
||||
((sl (len suf)) (tl (len s)))
|
||||
(and (<= sl tl) (= suf (substring s (- tl sl) tl)))))
|
||||
|
||||
;; isInfixOf — linear scan
|
||||
(define
|
||||
(hk-is-infix-of pat s)
|
||||
(let
|
||||
((plen (len pat)) (slen (len s)))
|
||||
(letrec
|
||||
((go (fn (i) (if (> (+ i plen) slen) false (if (= pat (substring s i (+ i plen))) true (go (+ i 1)))))))
|
||||
(if (= plen 0) true (go 0)))))
|
||||
|
||||
;; ===========================================================================
|
||||
;; 10. Show helper
|
||||
;; ===========================================================================
|
||||
|
||||
(define
|
||||
(hk-show x)
|
||||
(cond
|
||||
((= x nil) "Nothing")
|
||||
((= x true) "True")
|
||||
((= x false) "False")
|
||||
((hk-rational? x) (hk-show-rational x))
|
||||
((integer? x) (str x))
|
||||
((float? x) (str x))
|
||||
((= (type-of x) "string") (str "\"" x "\""))
|
||||
((= (type-of x) "char") (str "'" (str x) "'"))
|
||||
((list? x)
|
||||
(str
|
||||
"["
|
||||
(if
|
||||
(= (len x) 0)
|
||||
""
|
||||
(reduce
|
||||
(fn (a b) (str a "," (hk-show b)))
|
||||
(hk-show (first x))
|
||||
(rest x)))
|
||||
"]"))
|
||||
(else (str x))))
|
||||
@@ -46,6 +46,7 @@ for FILE in "${FILES[@]}"; do
|
||||
cat > "$TMPFILE" <<EPOCHS
|
||||
(epoch 1)
|
||||
(load "lib/haskell/tokenizer.sx")
|
||||
(load "lib/haskell/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "$FILE")
|
||||
(epoch 3)
|
||||
@@ -81,6 +82,7 @@ EPOCHS
|
||||
cat > "$TMPFILE2" <<EPOCHS
|
||||
(epoch 1)
|
||||
(load "lib/haskell/tokenizer.sx")
|
||||
(load "lib/haskell/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "$FILE")
|
||||
(epoch 3)
|
||||
|
||||
451
lib/haskell/tests/runtime.sx
Normal file
451
lib/haskell/tests/runtime.sx
Normal file
@@ -0,0 +1,451 @@
|
||||
;; lib/haskell/tests/runtime.sx — smoke-tests for lib/haskell/runtime.sx
|
||||
;;
|
||||
;; Uses the same hk-test framework as tests/parse.sx.
|
||||
;; Loaded by test.sh after: tokenizer.sx + runtime.sx are pre-loaded.
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Test framework boilerplate (mirrors parse.sx)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define hk-test-pass 0)
|
||||
(define hk-test-fail 0)
|
||||
(define hk-test-fails (list))
|
||||
|
||||
(define
|
||||
(hk-test name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! hk-test-pass (+ hk-test-pass 1))
|
||||
(do
|
||||
(set! hk-test-fail (+ hk-test-fail 1))
|
||||
(append! hk-test-fails {:actual actual :expected expected :name name}))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Numeric type class helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(hk-test "is-integer? int" (hk-is-integer? 42) true)
|
||||
(hk-test "is-integer? float" (hk-is-integer? 1.5) false)
|
||||
(hk-test "is-float? float" (hk-is-float? 3.14) true)
|
||||
(hk-test "is-float? int" (hk-is-float? 3) false)
|
||||
(hk-test "is-num? int" (hk-is-num? 10) true)
|
||||
(hk-test "is-num? float" (hk-is-num? 1) true)
|
||||
|
||||
(hk-test "to-float" (hk-to-float 5) 5)
|
||||
(hk-test "to-integer trunc" (hk-to-integer 3.7) 3)
|
||||
|
||||
(hk-test "div pos pos" (hk-div 7 2) 3)
|
||||
(hk-test "div neg pos" (hk-div -7 2) -4)
|
||||
(hk-test "div pos neg" (hk-div 7 -2) -4)
|
||||
(hk-test "div neg neg" (hk-div -7 -2) 3)
|
||||
(hk-test "div exact" (hk-div 6 2) 3)
|
||||
|
||||
(hk-test "mod pos pos" (hk-mod 10 3) 1)
|
||||
(hk-test "mod neg pos" (hk-mod -7 3) 2)
|
||||
(hk-test "rem pos pos" (hk-rem 10 3) 1)
|
||||
(hk-test "rem neg pos" (hk-rem -7 3) -1)
|
||||
|
||||
(hk-test "abs pos" (hk-abs 5) 5)
|
||||
(hk-test "abs neg" (hk-abs -5) 5)
|
||||
(hk-test "signum pos" (hk-signum 42) 1)
|
||||
(hk-test "signum neg" (hk-signum -7) -1)
|
||||
(hk-test "signum zero" (hk-signum 0) 0)
|
||||
|
||||
(hk-test "gcd" (hk-gcd 12 8) 4)
|
||||
(hk-test "lcm" (hk-lcm 4 6) 12)
|
||||
(hk-test "even?" (hk-even? 4) true)
|
||||
(hk-test "even? odd" (hk-even? 3) false)
|
||||
(hk-test "odd?" (hk-odd? 7) true)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Rational numbers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(let
|
||||
((r (hk-make-rational 1 2)))
|
||||
(do
|
||||
(hk-test "rational?" (hk-rational? r) true)
|
||||
(hk-test "numerator" (hk-numerator r) 1)
|
||||
(hk-test "denominator" (hk-denominator r) 2)))
|
||||
|
||||
(let
|
||||
((r (hk-make-rational 2 4)))
|
||||
(do
|
||||
(hk-test "rat normalise num" (hk-numerator r) 1)
|
||||
(hk-test "rat normalise den" (hk-denominator r) 2)))
|
||||
|
||||
(let
|
||||
((sum (hk-rational-add (hk-make-rational 1 2) (hk-make-rational 1 3))))
|
||||
(do
|
||||
(hk-test "rat-add num" (hk-numerator sum) 5)
|
||||
(hk-test "rat-add den" (hk-denominator sum) 6)))
|
||||
|
||||
(hk-test
|
||||
"rat-to-float"
|
||||
(hk-rational-to-float (hk-make-rational 1 2))
|
||||
0.5)
|
||||
(hk-test "rational? int" (hk-rational? 42) false)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Lazy evaluation (promises via SX delay)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(let
|
||||
((p (delay 42)))
|
||||
(hk-test "force promise" (hk-force p) 42))
|
||||
|
||||
(hk-test "force non-promise" (hk-force 99) 99)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Char utilities — compare via hk-ord to avoid = on char type
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(hk-test "ord A" (hk-ord (integer->char 65)) 65)
|
||||
(hk-test "chr 65" (hk-ord (hk-chr 65)) 65)
|
||||
(hk-test "is-alpha? A" (hk-is-alpha? (integer->char 65)) true)
|
||||
(hk-test "is-alpha? 0" (hk-is-alpha? (integer->char 48)) false)
|
||||
(hk-test "is-digit? 5" (hk-is-digit? (integer->char 53)) true)
|
||||
(hk-test "is-digit? A" (hk-is-digit? (integer->char 65)) false)
|
||||
(hk-test "is-upper? A" (hk-is-upper? (integer->char 65)) true)
|
||||
(hk-test "is-upper? a" (hk-is-upper? (integer->char 97)) false)
|
||||
(hk-test "is-lower? a" (hk-is-lower? (integer->char 97)) true)
|
||||
(hk-test "is-space? spc" (hk-is-space? (integer->char 32)) true)
|
||||
(hk-test "is-space? A" (hk-is-space? (integer->char 65)) false)
|
||||
(hk-test
|
||||
"to-upper a"
|
||||
(hk-ord (hk-to-upper (integer->char 97)))
|
||||
65)
|
||||
(hk-test
|
||||
"to-lower A"
|
||||
(hk-ord (hk-to-lower (integer->char 65)))
|
||||
97)
|
||||
(hk-test
|
||||
"digit-to-int 0"
|
||||
(hk-digit-to-int (integer->char 48))
|
||||
0)
|
||||
(hk-test
|
||||
"digit-to-int 9"
|
||||
(hk-digit-to-int (integer->char 57))
|
||||
9)
|
||||
(hk-test
|
||||
"digit-to-int a"
|
||||
(hk-digit-to-int (integer->char 97))
|
||||
10)
|
||||
(hk-test
|
||||
"digit-to-int F"
|
||||
(hk-digit-to-int (integer->char 70))
|
||||
15)
|
||||
(hk-test "int-to-digit 0" (hk-ord (hk-int-to-digit 0)) 48)
|
||||
(hk-test "int-to-digit 10" (hk-ord (hk-int-to-digit 10)) 97)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Data.Set
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(hk-test "set-empty is set?" (hk-set? (hk-set-empty)) true)
|
||||
(hk-test "set-null? empty" (hk-set-null? (hk-set-empty)) true)
|
||||
|
||||
(let
|
||||
((s (hk-set-singleton 42)))
|
||||
(do
|
||||
(hk-test "singleton member" (hk-set-member? 42 s) true)
|
||||
(hk-test "singleton size" (hk-set-size s) 1)))
|
||||
|
||||
(let
|
||||
((s (hk-set-from-list (list 1 2 3))))
|
||||
(do
|
||||
(hk-test "from-list member" (hk-set-member? 2 s) true)
|
||||
(hk-test "from-list absent" (hk-set-member? 9 s) false)
|
||||
(hk-test "from-list size" (hk-set-size s) 3)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. Data.List
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(hk-test "head" (hk-head (list 1 2 3)) 1)
|
||||
(hk-test
|
||||
"tail length"
|
||||
(len (hk-tail (list 1 2 3)))
|
||||
2)
|
||||
(hk-test "null? empty" (hk-null? (list)) true)
|
||||
(hk-test "null? non-empty" (hk-null? (list 1)) false)
|
||||
(hk-test
|
||||
"length"
|
||||
(hk-length (list 1 2 3))
|
||||
3)
|
||||
|
||||
(hk-test
|
||||
"take 2"
|
||||
(hk-take 2 (list 1 2 3))
|
||||
(list 1 2))
|
||||
(hk-test "take 0" (hk-take 0 (list 1 2)) (list))
|
||||
(hk-test
|
||||
"take overflow"
|
||||
(hk-take 5 (list 1 2))
|
||||
(list 1 2))
|
||||
(hk-test
|
||||
"drop 1"
|
||||
(hk-drop 1 (list 1 2 3))
|
||||
(list 2 3))
|
||||
(hk-test
|
||||
"drop 0"
|
||||
(hk-drop 0 (list 1 2))
|
||||
(list 1 2))
|
||||
|
||||
(hk-test
|
||||
"take-while"
|
||||
(hk-take-while
|
||||
(fn (x) (< x 3))
|
||||
(list 1 2 3 4))
|
||||
(list 1 2))
|
||||
(hk-test
|
||||
"drop-while"
|
||||
(hk-drop-while
|
||||
(fn (x) (< x 3))
|
||||
(list 1 2 3 4))
|
||||
(list 3 4))
|
||||
|
||||
(hk-test
|
||||
"zip"
|
||||
(hk-zip (list 1 2) (list 3 4))
|
||||
(list (list 1 3) (list 2 4)))
|
||||
(hk-test
|
||||
"zip uneven"
|
||||
(hk-zip
|
||||
(list 1 2 3)
|
||||
(list 4 5))
|
||||
(list (list 1 4) (list 2 5)))
|
||||
|
||||
(hk-test
|
||||
"zip-with +"
|
||||
(hk-zip-with
|
||||
+
|
||||
(list 1 2 3)
|
||||
(list 10 20 30))
|
||||
(list 11 22 33))
|
||||
|
||||
(hk-test
|
||||
"unzip fst"
|
||||
(first
|
||||
(hk-unzip
|
||||
(list (list 1 3) (list 2 4))))
|
||||
(list 1 2))
|
||||
(hk-test
|
||||
"unzip snd"
|
||||
(nth
|
||||
(hk-unzip
|
||||
(list (list 1 3) (list 2 4)))
|
||||
1)
|
||||
(list 3 4))
|
||||
|
||||
(hk-test
|
||||
"elem hit"
|
||||
(hk-elem 2 (list 1 2 3))
|
||||
true)
|
||||
(hk-test
|
||||
"elem miss"
|
||||
(hk-elem 9 (list 1 2 3))
|
||||
false)
|
||||
(hk-test
|
||||
"not-elem"
|
||||
(hk-not-elem 9 (list 1 2 3))
|
||||
true)
|
||||
|
||||
(hk-test
|
||||
"nub"
|
||||
(hk-nub (list 1 2 1 3 2))
|
||||
(list 1 2 3))
|
||||
|
||||
(hk-test
|
||||
"sum"
|
||||
(hk-sum (list 1 2 3 4))
|
||||
10)
|
||||
(hk-test
|
||||
"product"
|
||||
(hk-product (list 1 2 3 4))
|
||||
24)
|
||||
(hk-test
|
||||
"maximum"
|
||||
(hk-maximum (list 3 1 4 1 5))
|
||||
5)
|
||||
(hk-test
|
||||
"minimum"
|
||||
(hk-minimum (list 3 1 4 1 5))
|
||||
1)
|
||||
|
||||
(hk-test
|
||||
"concat"
|
||||
(hk-concat
|
||||
(list (list 1 2) (list 3 4)))
|
||||
(list 1 2 3 4))
|
||||
(hk-test
|
||||
"concat-map"
|
||||
(hk-concat-map
|
||||
(fn (x) (list x (* x x)))
|
||||
(list 1 2 3))
|
||||
(list 1 1 2 4 3 9))
|
||||
|
||||
(hk-test
|
||||
"sort"
|
||||
(hk-sort (list 3 1 4 1 5))
|
||||
(list 1 1 3 4 5))
|
||||
(hk-test
|
||||
"replicate"
|
||||
(hk-replicate 3 0)
|
||||
(list 0 0 0))
|
||||
(hk-test "replicate 0" (hk-replicate 0 99) (list))
|
||||
|
||||
(hk-test
|
||||
"intersperse"
|
||||
(hk-intersperse 0 (list 1 2 3))
|
||||
(list 1 0 2 0 3))
|
||||
(hk-test
|
||||
"intersperse 1"
|
||||
(hk-intersperse 0 (list 1))
|
||||
(list 1))
|
||||
(hk-test "intersperse empty" (hk-intersperse 0 (list)) (list))
|
||||
|
||||
(hk-test
|
||||
"span"
|
||||
(hk-span
|
||||
(fn (x) (< x 3))
|
||||
(list 1 2 3 4))
|
||||
(list (list 1 2) (list 3 4)))
|
||||
(hk-test
|
||||
"break"
|
||||
(hk-break
|
||||
(fn (x) (>= x 3))
|
||||
(list 1 2 3 4))
|
||||
(list (list 1 2) (list 3 4)))
|
||||
|
||||
(hk-test
|
||||
"foldl"
|
||||
(hk-foldl
|
||||
(fn (a b) (- a b))
|
||||
10
|
||||
(list 1 2 3))
|
||||
4)
|
||||
(hk-test
|
||||
"foldr"
|
||||
(hk-foldr cons (list) (list 1 2 3))
|
||||
(list 1 2 3))
|
||||
|
||||
(hk-test
|
||||
"scanl"
|
||||
(hk-scanl + 0 (list 1 2 3))
|
||||
(list 0 1 3 6))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 7. Maybe / Either
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(hk-test "nothing is-nothing?" (hk-is-nothing? hk-nothing) true)
|
||||
(hk-test "nothing is-just?" (hk-is-just? hk-nothing) false)
|
||||
(hk-test "just is-just?" (hk-is-just? (hk-just 42)) true)
|
||||
(hk-test "just is-nothing?" (hk-is-nothing? (hk-just 42)) false)
|
||||
(hk-test "from-just" (hk-from-just (hk-just 99)) 99)
|
||||
(hk-test
|
||||
"from-maybe nothing"
|
||||
(hk-from-maybe 0 hk-nothing)
|
||||
0)
|
||||
(hk-test
|
||||
"from-maybe just"
|
||||
(hk-from-maybe 0 (hk-just 42))
|
||||
42)
|
||||
(hk-test
|
||||
"maybe nothing"
|
||||
(hk-maybe 0 (fn (x) (* x 2)) hk-nothing)
|
||||
0)
|
||||
(hk-test
|
||||
"maybe just"
|
||||
(hk-maybe 0 (fn (x) (* x 2)) (hk-just 5))
|
||||
10)
|
||||
|
||||
(hk-test "left is-left?" (hk-is-left? (hk-left "e")) true)
|
||||
(hk-test "right is-right?" (hk-is-right? (hk-right 42)) true)
|
||||
(hk-test "from-right" (hk-from-right (hk-right 7)) 7)
|
||||
(hk-test
|
||||
"either left"
|
||||
(hk-either (fn (x) (str "L" x)) (fn (x) (str "R" x)) (hk-left "err"))
|
||||
"Lerr")
|
||||
(hk-test
|
||||
"either right"
|
||||
(hk-either
|
||||
(fn (x) (str "L" x))
|
||||
(fn (x) (str "R" x))
|
||||
(hk-right 42))
|
||||
"R42")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 8. Tuples
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(hk-test "pair" (hk-pair 1 2) (list 1 2))
|
||||
(hk-test "fst" (hk-fst (hk-pair 3 4)) 3)
|
||||
(hk-test "snd" (hk-snd (hk-pair 3 4)) 4)
|
||||
(hk-test
|
||||
"triple"
|
||||
(hk-triple 1 2 3)
|
||||
(list 1 2 3))
|
||||
(hk-test
|
||||
"fst3"
|
||||
(hk-fst3 (hk-triple 7 8 9))
|
||||
7)
|
||||
(hk-test
|
||||
"thd3"
|
||||
(hk-thd3 (hk-triple 7 8 9))
|
||||
9)
|
||||
|
||||
(hk-test "curry" ((hk-curry +) 3 4) 7)
|
||||
(hk-test
|
||||
"uncurry"
|
||||
((hk-uncurry (fn (a b) (* a b))) (list 3 4))
|
||||
12)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 9. String helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(hk-test "words" (hk-words "hello world") (list "hello" "world"))
|
||||
(hk-test "words leading ws" (hk-words " foo bar") (list "foo" "bar"))
|
||||
(hk-test "words empty" (hk-words "") (list))
|
||||
(hk-test "unwords" (hk-unwords (list "a" "b" "c")) "a b c")
|
||||
(hk-test "unwords single" (hk-unwords (list "x")) "x")
|
||||
|
||||
(hk-test "lines" (hk-lines "a\nb\nc") (list "a" "b" "c"))
|
||||
(hk-test "lines single" (hk-lines "hello") (list "hello"))
|
||||
(hk-test "unlines" (hk-unlines (list "a" "b")) "a\nb\n")
|
||||
|
||||
(hk-test "is-prefix-of yes" (hk-is-prefix-of "he" "hello") true)
|
||||
(hk-test "is-prefix-of no" (hk-is-prefix-of "wo" "hello") false)
|
||||
(hk-test "is-prefix-of eq" (hk-is-prefix-of "hi" "hi") true)
|
||||
(hk-test "is-prefix-of empty" (hk-is-prefix-of "" "hi") true)
|
||||
|
||||
(hk-test "is-suffix-of yes" (hk-is-suffix-of "lo" "hello") true)
|
||||
(hk-test "is-suffix-of no" (hk-is-suffix-of "he" "hello") false)
|
||||
(hk-test "is-suffix-of empty" (hk-is-suffix-of "" "hi") true)
|
||||
|
||||
(hk-test "is-infix-of yes" (hk-is-infix-of "ell" "hello") true)
|
||||
(hk-test "is-infix-of no" (hk-is-infix-of "xyz" "hello") false)
|
||||
(hk-test "is-infix-of empty" (hk-is-infix-of "" "hello") true)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 10. Show
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(hk-test "show nil" (hk-show nil) "Nothing")
|
||||
(hk-test "show true" (hk-show true) "True")
|
||||
(hk-test "show false" (hk-show false) "False")
|
||||
(hk-test "show int" (hk-show 42) "42")
|
||||
(hk-test "show string" (hk-show "hi") "\"hi\"")
|
||||
(hk-test
|
||||
"show list"
|
||||
(hk-show (list 1 2 3))
|
||||
"[1,2,3]")
|
||||
(hk-test "show empty list" (hk-show (list)) "[]")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Summary (required by test.sh — last expression is the return value)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(list hk-test-pass hk-test-fail)
|
||||
@@ -49,6 +49,8 @@ trap "rm -f $TMPFILE" EXIT
|
||||
echo '(load "lib/js/transpile.sx")'
|
||||
echo '(epoch 5)'
|
||||
echo '(load "lib/js/runtime.sx")'
|
||||
echo '(epoch 6)'
|
||||
echo '(load "lib/js/regex.sx")'
|
||||
|
||||
epoch=100
|
||||
for f in "${FIXTURES[@]}"; do
|
||||
|
||||
943
lib/js/regex.sx
Normal file
943
lib/js/regex.sx
Normal file
@@ -0,0 +1,943 @@
|
||||
;; lib/js/regex.sx — pure-SX recursive backtracking regex engine
|
||||
;;
|
||||
;; Installed via (js-regex-platform-override! ...) at load time.
|
||||
;; Covers: character classes (\d\w\s . [abc] [^abc] [a-z]),
|
||||
;; anchors (^ $ \b \B), quantifiers (* + ? {n,m} lazy variants),
|
||||
;; groups (capturing + non-capturing), alternation (a|b),
|
||||
;; flags: i (case-insensitive), g (global), m (multiline).
|
||||
;;
|
||||
;; Architecture:
|
||||
;; 1. rx-parse-pattern — pattern string → compiled node list
|
||||
;; 2. rx-match-nodes — recursive backtracker
|
||||
;; 3. rx-exec / rx-test — public interface
|
||||
;; 4. Install as {:test rx-test :exec rx-exec}
|
||||
|
||||
;; ── Utilities ─────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
rx-char-at
|
||||
(fn (s i) (if (and (>= i 0) (< i (len s))) (char-at s i) "")))
|
||||
|
||||
(define
|
||||
rx-digit?
|
||||
(fn
|
||||
(c)
|
||||
(and (not (= c "")) (>= (char-code c) 48) (<= (char-code c) 57))))
|
||||
|
||||
(define
|
||||
rx-word?
|
||||
(fn
|
||||
(c)
|
||||
(and
|
||||
(not (= c ""))
|
||||
(or
|
||||
(and (>= (char-code c) 65) (<= (char-code c) 90))
|
||||
(and (>= (char-code c) 97) (<= (char-code c) 122))
|
||||
(and (>= (char-code c) 48) (<= (char-code c) 57))
|
||||
(= c "_")))))
|
||||
|
||||
(define
|
||||
rx-space?
|
||||
(fn
|
||||
(c)
|
||||
(or (= c " ") (= c "\t") (= c "\n") (= c "\r") (= c "\\f") (= c ""))))
|
||||
|
||||
(define rx-newline? (fn (c) (or (= c "\n") (= c "\r"))))
|
||||
|
||||
(define
|
||||
rx-downcase-char
|
||||
(fn
|
||||
(c)
|
||||
(let
|
||||
((cc (char-code c)))
|
||||
(if (and (>= cc 65) (<= cc 90)) (char-from-code (+ cc 32)) c))))
|
||||
|
||||
(define
|
||||
rx-char-eq?
|
||||
(fn
|
||||
(a b ci?)
|
||||
(if ci? (= (rx-downcase-char a) (rx-downcase-char b)) (= a b))))
|
||||
|
||||
(define
|
||||
rx-parse-int
|
||||
(fn
|
||||
(pat i acc)
|
||||
(let
|
||||
((c (rx-char-at pat i)))
|
||||
(if
|
||||
(rx-digit? c)
|
||||
(rx-parse-int pat (+ i 1) (+ (* acc 10) (- (char-code c) 48)))
|
||||
(list acc i)))))
|
||||
|
||||
(define
|
||||
rx-hex-digit-val
|
||||
(fn
|
||||
(c)
|
||||
(cond
|
||||
((and (>= (char-code c) 48) (<= (char-code c) 57))
|
||||
(- (char-code c) 48))
|
||||
((and (>= (char-code c) 65) (<= (char-code c) 70))
|
||||
(+ 10 (- (char-code c) 65)))
|
||||
((and (>= (char-code c) 97) (<= (char-code c) 102))
|
||||
(+ 10 (- (char-code c) 97)))
|
||||
(else -1))))
|
||||
|
||||
(define
|
||||
rx-parse-hex-n
|
||||
(fn
|
||||
(pat i n acc)
|
||||
(if
|
||||
(= n 0)
|
||||
(list (char-from-code acc) i)
|
||||
(let
|
||||
((v (rx-hex-digit-val (rx-char-at pat i))))
|
||||
(if
|
||||
(< v 0)
|
||||
(list (char-from-code acc) i)
|
||||
(rx-parse-hex-n pat (+ i 1) (- n 1) (+ (* acc 16) v)))))))
|
||||
|
||||
;; ── Pattern compiler ──────────────────────────────────────────────
|
||||
|
||||
;; Node types (stored in dicts with "__t__" key):
|
||||
;; literal : {:__t__ "literal" :__c__ char}
|
||||
;; any : {:__t__ "any"}
|
||||
;; class-d : {:__t__ "class-d" :__neg__ bool}
|
||||
;; class-w : {:__t__ "class-w" :__neg__ bool}
|
||||
;; class-s : {:__t__ "class-s" :__neg__ bool}
|
||||
;; char-class: {:__t__ "char-class" :__neg__ bool :__items__ list}
|
||||
;; anchor-start / anchor-end / anchor-word / anchor-nonword
|
||||
;; quant : {:__t__ "quant" :__node__ n :__min__ m :__max__ mx :__lazy__ bool}
|
||||
;; group : {:__t__ "group" :__idx__ i :__nodes__ list}
|
||||
;; ncgroup : {:__t__ "ncgroup" :__nodes__ list}
|
||||
;; alt : {:__t__ "alt" :__branches__ list-of-node-lists}
|
||||
|
||||
;; parse one escape after `\`, returns (node new-i)
|
||||
(define
|
||||
rx-parse-escape
|
||||
(fn
|
||||
(pat i)
|
||||
(let
|
||||
((c (rx-char-at pat i)))
|
||||
(cond
|
||||
((= c "d") (list (dict "__t__" "class-d" "__neg__" false) (+ i 1)))
|
||||
((= c "D") (list (dict "__t__" "class-d" "__neg__" true) (+ i 1)))
|
||||
((= c "w") (list (dict "__t__" "class-w" "__neg__" false) (+ i 1)))
|
||||
((= c "W") (list (dict "__t__" "class-w" "__neg__" true) (+ i 1)))
|
||||
((= c "s") (list (dict "__t__" "class-s" "__neg__" false) (+ i 1)))
|
||||
((= c "S") (list (dict "__t__" "class-s" "__neg__" true) (+ i 1)))
|
||||
((= c "b") (list (dict "__t__" "anchor-word") (+ i 1)))
|
||||
((= c "B") (list (dict "__t__" "anchor-nonword") (+ i 1)))
|
||||
((= c "n") (list (dict "__t__" "literal" "__c__" "\n") (+ i 1)))
|
||||
((= c "r") (list (dict "__t__" "literal" "__c__" "\r") (+ i 1)))
|
||||
((= c "t") (list (dict "__t__" "literal" "__c__" "\t") (+ i 1)))
|
||||
((= c "f") (list (dict "__t__" "literal" "__c__" "\\f") (+ i 1)))
|
||||
((= c "v") (list (dict "__t__" "literal" "__c__" "") (+ i 1)))
|
||||
((= c "u")
|
||||
(let
|
||||
((res (rx-parse-hex-n pat (+ i 1) 4 0)))
|
||||
(list (dict "__t__" "literal" "__c__" (nth res 0)) (nth res 1))))
|
||||
((= c "x")
|
||||
(let
|
||||
((res (rx-parse-hex-n pat (+ i 1) 2 0)))
|
||||
(list (dict "__t__" "literal" "__c__" (nth res 0)) (nth res 1))))
|
||||
(else (list (dict "__t__" "literal" "__c__" c) (+ i 1)))))))
|
||||
|
||||
;; parse a char-class item inside [...], returns (item new-i)
|
||||
(define
|
||||
rx-parse-class-item
|
||||
(fn
|
||||
(pat i)
|
||||
(let
|
||||
((c (rx-char-at pat i)))
|
||||
(cond
|
||||
((= c "\\")
|
||||
(let
|
||||
((esc (rx-parse-escape pat (+ i 1))))
|
||||
(let
|
||||
((node (nth esc 0)) (ni (nth esc 1)))
|
||||
(let
|
||||
((t (get node "__t__")))
|
||||
(cond
|
||||
((= t "class-d")
|
||||
(list
|
||||
(dict "kind" "class-d" "neg" (get node "__neg__"))
|
||||
ni))
|
||||
((= t "class-w")
|
||||
(list
|
||||
(dict "kind" "class-w" "neg" (get node "__neg__"))
|
||||
ni))
|
||||
((= t "class-s")
|
||||
(list
|
||||
(dict "kind" "class-s" "neg" (get node "__neg__"))
|
||||
ni))
|
||||
(else
|
||||
(let
|
||||
((lc (get node "__c__")))
|
||||
(if
|
||||
(and
|
||||
(= (rx-char-at pat ni) "-")
|
||||
(not (= (rx-char-at pat (+ ni 1)) "]")))
|
||||
(let
|
||||
((hi-c (rx-char-at pat (+ ni 1))))
|
||||
(list
|
||||
(dict "kind" "range" "lo" lc "hi" hi-c)
|
||||
(+ ni 2)))
|
||||
(list (dict "kind" "lit" "c" lc) ni)))))))))
|
||||
(else
|
||||
(if
|
||||
(and
|
||||
(not (= c ""))
|
||||
(= (rx-char-at pat (+ i 1)) "-")
|
||||
(not (= (rx-char-at pat (+ i 2)) "]"))
|
||||
(not (= (rx-char-at pat (+ i 2)) "")))
|
||||
(let
|
||||
((hi-c (rx-char-at pat (+ i 2))))
|
||||
(list (dict "kind" "range" "lo" c "hi" hi-c) (+ i 3)))
|
||||
(list (dict "kind" "lit" "c" c) (+ i 1))))))))
|
||||
|
||||
(define
|
||||
rx-parse-class-items
|
||||
(fn
|
||||
(pat i items)
|
||||
(let
|
||||
((c (rx-char-at pat i)))
|
||||
(if
|
||||
(or (= c "]") (= c ""))
|
||||
(list items i)
|
||||
(let
|
||||
((res (rx-parse-class-item pat i)))
|
||||
(begin
|
||||
(append! items (nth res 0))
|
||||
(rx-parse-class-items pat (nth res 1) items)))))))
|
||||
|
||||
;; parse a sequence until stop-ch or EOF; returns (nodes new-i groups-count)
|
||||
(define
|
||||
rx-parse-seq
|
||||
(fn
|
||||
(pat i stop-ch ds)
|
||||
(let
|
||||
((c (rx-char-at pat i)))
|
||||
(cond
|
||||
((= c "") (list (get ds "nodes") i (get ds "groups")))
|
||||
((= c stop-ch) (list (get ds "nodes") i (get ds "groups")))
|
||||
((= c "|") (rx-parse-alt-rest pat i ds))
|
||||
(else
|
||||
(let
|
||||
((res (rx-parse-atom pat i ds)))
|
||||
(let
|
||||
((node (nth res 0)) (ni (nth res 1)) (ds2 (nth res 2)))
|
||||
(let
|
||||
((qres (rx-parse-quant pat ni node)))
|
||||
(begin
|
||||
(append! (get ds2 "nodes") (nth qres 0))
|
||||
(rx-parse-seq pat (nth qres 1) stop-ch ds2))))))))))
|
||||
|
||||
;; when we hit | inside a sequence, collect all alternatives
|
||||
(define
|
||||
rx-parse-alt-rest
|
||||
(fn
|
||||
(pat i ds)
|
||||
(let
|
||||
((left-branch (get ds "nodes")) (branches (list)))
|
||||
(begin
|
||||
(append! branches left-branch)
|
||||
(rx-parse-alt-branches pat i (get ds "groups") branches)))))
|
||||
|
||||
(define
|
||||
rx-parse-alt-branches
|
||||
(fn
|
||||
(pat i n-groups branches)
|
||||
(let
|
||||
((new-nodes (list)) (ds2 (dict "groups" n-groups "nodes" new-nodes)))
|
||||
(let
|
||||
((res (rx-parse-seq pat (+ i 1) "|" ds2)))
|
||||
(begin
|
||||
(append! branches (nth res 0))
|
||||
(let
|
||||
((ni2 (nth res 1)) (g2 (nth res 2)))
|
||||
(if
|
||||
(= (rx-char-at pat ni2) "|")
|
||||
(rx-parse-alt-branches pat ni2 g2 branches)
|
||||
(list
|
||||
(list (dict "__t__" "alt" "__branches__" branches))
|
||||
ni2
|
||||
g2))))))))
|
||||
|
||||
;; parse quantifier suffix, returns (node new-i)
|
||||
(define
|
||||
rx-parse-quant
|
||||
(fn
|
||||
(pat i node)
|
||||
(let
|
||||
((c (rx-char-at pat i)))
|
||||
(cond
|
||||
((= c "*")
|
||||
(let
|
||||
((lazy? (= (rx-char-at pat (+ i 1)) "?")))
|
||||
(list
|
||||
(dict
|
||||
"__t__"
|
||||
"quant"
|
||||
"__node__"
|
||||
node
|
||||
"__min__"
|
||||
0
|
||||
"__max__"
|
||||
-1
|
||||
"__lazy__"
|
||||
lazy?)
|
||||
(if lazy? (+ i 2) (+ i 1)))))
|
||||
((= c "+")
|
||||
(let
|
||||
((lazy? (= (rx-char-at pat (+ i 1)) "?")))
|
||||
(list
|
||||
(dict
|
||||
"__t__"
|
||||
"quant"
|
||||
"__node__"
|
||||
node
|
||||
"__min__"
|
||||
1
|
||||
"__max__"
|
||||
-1
|
||||
"__lazy__"
|
||||
lazy?)
|
||||
(if lazy? (+ i 2) (+ i 1)))))
|
||||
((= c "?")
|
||||
(let
|
||||
((lazy? (= (rx-char-at pat (+ i 1)) "?")))
|
||||
(list
|
||||
(dict
|
||||
"__t__"
|
||||
"quant"
|
||||
"__node__"
|
||||
node
|
||||
"__min__"
|
||||
0
|
||||
"__max__"
|
||||
1
|
||||
"__lazy__"
|
||||
lazy?)
|
||||
(if lazy? (+ i 2) (+ i 1)))))
|
||||
((= c "{")
|
||||
(let
|
||||
((mres (rx-parse-int pat (+ i 1) 0)))
|
||||
(let
|
||||
((mn (nth mres 0)) (mi (nth mres 1)))
|
||||
(let
|
||||
((sep (rx-char-at pat mi)))
|
||||
(cond
|
||||
((= sep "}")
|
||||
(let
|
||||
((lazy? (= (rx-char-at pat (+ mi 1)) "?")))
|
||||
(list
|
||||
(dict
|
||||
"__t__"
|
||||
"quant"
|
||||
"__node__"
|
||||
node
|
||||
"__min__"
|
||||
mn
|
||||
"__max__"
|
||||
mn
|
||||
"__lazy__"
|
||||
lazy?)
|
||||
(if lazy? (+ mi 2) (+ mi 1)))))
|
||||
((= sep ",")
|
||||
(let
|
||||
((c2 (rx-char-at pat (+ mi 1))))
|
||||
(if
|
||||
(= c2 "}")
|
||||
(let
|
||||
((lazy? (= (rx-char-at pat (+ mi 2)) "?")))
|
||||
(list
|
||||
(dict
|
||||
"__t__"
|
||||
"quant"
|
||||
"__node__"
|
||||
node
|
||||
"__min__"
|
||||
mn
|
||||
"__max__"
|
||||
-1
|
||||
"__lazy__"
|
||||
lazy?)
|
||||
(if lazy? (+ mi 3) (+ mi 2))))
|
||||
(let
|
||||
((mxres (rx-parse-int pat (+ mi 1) 0)))
|
||||
(let
|
||||
((mx (nth mxres 0)) (mxi (nth mxres 1)))
|
||||
(let
|
||||
((lazy? (= (rx-char-at pat (+ mxi 1)) "?")))
|
||||
(list
|
||||
(dict
|
||||
"__t__"
|
||||
"quant"
|
||||
"__node__"
|
||||
node
|
||||
"__min__"
|
||||
mn
|
||||
"__max__"
|
||||
mx
|
||||
"__lazy__"
|
||||
lazy?)
|
||||
(if lazy? (+ mxi 2) (+ mxi 1)))))))))
|
||||
(else (list node i)))))))
|
||||
(else (list node i))))))
|
||||
|
||||
;; parse one atom, returns (node new-i new-ds)
|
||||
(define
|
||||
rx-parse-atom
|
||||
(fn
|
||||
(pat i ds)
|
||||
(let
|
||||
((c (rx-char-at pat i)))
|
||||
(cond
|
||||
((= c ".") (list (dict "__t__" "any") (+ i 1) ds))
|
||||
((= c "^") (list (dict "__t__" "anchor-start") (+ i 1) ds))
|
||||
((= c "$") (list (dict "__t__" "anchor-end") (+ i 1) ds))
|
||||
((= c "\\")
|
||||
(let
|
||||
((esc (rx-parse-escape pat (+ i 1))))
|
||||
(list (nth esc 0) (nth esc 1) ds)))
|
||||
((= c "[")
|
||||
(let
|
||||
((neg? (= (rx-char-at pat (+ i 1)) "^")))
|
||||
(let
|
||||
((start (if neg? (+ i 2) (+ i 1))) (items (list)))
|
||||
(let
|
||||
((res (rx-parse-class-items pat start items)))
|
||||
(let
|
||||
((ci (nth res 1)))
|
||||
(list
|
||||
(dict
|
||||
"__t__"
|
||||
"char-class"
|
||||
"__neg__"
|
||||
neg?
|
||||
"__items__"
|
||||
items)
|
||||
(+ ci 1)
|
||||
ds))))))
|
||||
((= c "(")
|
||||
(let
|
||||
((c2 (rx-char-at pat (+ i 1))))
|
||||
(if
|
||||
(and (= c2 "?") (= (rx-char-at pat (+ i 2)) ":"))
|
||||
(let
|
||||
((inner-nodes (list))
|
||||
(inner-ds
|
||||
(dict "groups" (get ds "groups") "nodes" inner-nodes)))
|
||||
(let
|
||||
((res (rx-parse-seq pat (+ i 3) ")" inner-ds)))
|
||||
(list
|
||||
(dict "__t__" "ncgroup" "__nodes__" (nth res 0))
|
||||
(+ (nth res 1) 1)
|
||||
(dict "groups" (nth res 2) "nodes" (get ds "nodes")))))
|
||||
(let
|
||||
((gidx (+ (get ds "groups") 1)) (inner-nodes (list)))
|
||||
(let
|
||||
((inner-ds (dict "groups" gidx "nodes" inner-nodes)))
|
||||
(let
|
||||
((res (rx-parse-seq pat (+ i 1) ")" inner-ds)))
|
||||
(list
|
||||
(dict
|
||||
"__t__"
|
||||
"group"
|
||||
"__idx__"
|
||||
gidx
|
||||
"__nodes__"
|
||||
(nth res 0))
|
||||
(+ (nth res 1) 1)
|
||||
(dict "groups" (nth res 2) "nodes" (get ds "nodes")))))))))
|
||||
(else (list (dict "__t__" "literal" "__c__" c) (+ i 1) ds))))))
|
||||
|
||||
;; top-level compile
|
||||
(define
|
||||
rx-compile
|
||||
(fn
|
||||
(pattern)
|
||||
(let
|
||||
((nodes (list)) (ds (dict "groups" 0 "nodes" nodes)))
|
||||
(let
|
||||
((res (rx-parse-seq pattern 0 "" ds)))
|
||||
(dict "nodes" (nth res 0) "ngroups" (nth res 2))))))
|
||||
|
||||
;; ── Matcher ───────────────────────────────────────────────────────
|
||||
|
||||
;; Match a char-class item against character c
|
||||
(define
|
||||
rx-item-matches?
|
||||
(fn
|
||||
(item c ci?)
|
||||
(let
|
||||
((kind (get item "kind")))
|
||||
(cond
|
||||
((= kind "lit") (rx-char-eq? c (get item "c") ci?))
|
||||
((= kind "range")
|
||||
(let
|
||||
((lo (if ci? (rx-downcase-char (get item "lo")) (get item "lo")))
|
||||
(hi
|
||||
(if ci? (rx-downcase-char (get item "hi")) (get item "hi")))
|
||||
(dc (if ci? (rx-downcase-char c) c)))
|
||||
(and
|
||||
(>= (char-code dc) (char-code lo))
|
||||
(<= (char-code dc) (char-code hi)))))
|
||||
((= kind "class-d")
|
||||
(let ((m (rx-digit? c))) (if (get item "neg") (not m) m)))
|
||||
((= kind "class-w")
|
||||
(let ((m (rx-word? c))) (if (get item "neg") (not m) m)))
|
||||
((= kind "class-s")
|
||||
(let ((m (rx-space? c))) (if (get item "neg") (not m) m)))
|
||||
(else false)))))
|
||||
|
||||
(define
|
||||
rx-class-items-any?
|
||||
(fn
|
||||
(items c ci?)
|
||||
(if
|
||||
(empty? items)
|
||||
false
|
||||
(if
|
||||
(rx-item-matches? (first items) c ci?)
|
||||
true
|
||||
(rx-class-items-any? (rest items) c ci?)))))
|
||||
|
||||
(define
|
||||
rx-class-matches?
|
||||
(fn
|
||||
(node c ci?)
|
||||
(let
|
||||
((neg? (get node "__neg__")) (items (get node "__items__")))
|
||||
(let
|
||||
((hit (rx-class-items-any? items c ci?)))
|
||||
(if neg? (not hit) hit)))))
|
||||
|
||||
;; Word boundary check
|
||||
(define
|
||||
rx-is-word-boundary?
|
||||
(fn
|
||||
(s i slen)
|
||||
(let
|
||||
((before (if (> i 0) (rx-word? (char-at s (- i 1))) false))
|
||||
(after (if (< i slen) (rx-word? (char-at s i)) false)))
|
||||
(not (= before after)))))
|
||||
|
||||
;; ── Core matcher ──────────────────────────────────────────────────
|
||||
;;
|
||||
;; rx-match-nodes : nodes s i slen ci? mi? groups → end-pos or -1
|
||||
;;
|
||||
;; Matches `nodes` starting at position `i` in string `s`.
|
||||
;; Returns the position after the last character consumed, or -1 on failure.
|
||||
;; Mutates `groups` dict to record captures.
|
||||
|
||||
(define
|
||||
rx-match-nodes
|
||||
(fn
|
||||
(nodes s i slen ci? mi? groups)
|
||||
(if
|
||||
(empty? nodes)
|
||||
i
|
||||
(let
|
||||
((node (first nodes)) (rest-nodes (rest nodes)))
|
||||
(let
|
||||
((t (get node "__t__")))
|
||||
(cond
|
||||
((= t "literal")
|
||||
(if
|
||||
(and
|
||||
(< i slen)
|
||||
(rx-char-eq? (char-at s i) (get node "__c__") ci?))
|
||||
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
|
||||
-1))
|
||||
((= t "any")
|
||||
(if
|
||||
(and (< i slen) (not (rx-newline? (char-at s i))))
|
||||
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
|
||||
-1))
|
||||
((= t "class-d")
|
||||
(let
|
||||
((m (and (< i slen) (rx-digit? (char-at s i)))))
|
||||
(if
|
||||
(if (get node "__neg__") (not m) m)
|
||||
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
|
||||
-1)))
|
||||
((= t "class-w")
|
||||
(let
|
||||
((m (and (< i slen) (rx-word? (char-at s i)))))
|
||||
(if
|
||||
(if (get node "__neg__") (not m) m)
|
||||
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
|
||||
-1)))
|
||||
((= t "class-s")
|
||||
(let
|
||||
((m (and (< i slen) (rx-space? (char-at s i)))))
|
||||
(if
|
||||
(if (get node "__neg__") (not m) m)
|
||||
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
|
||||
-1)))
|
||||
((= t "char-class")
|
||||
(if
|
||||
(and (< i slen) (rx-class-matches? node (char-at s i) ci?))
|
||||
(rx-match-nodes rest-nodes s (+ i 1) slen ci? mi? groups)
|
||||
-1))
|
||||
((= t "anchor-start")
|
||||
(if
|
||||
(or
|
||||
(= i 0)
|
||||
(and mi? (rx-newline? (rx-char-at s (- i 1)))))
|
||||
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
|
||||
-1))
|
||||
((= t "anchor-end")
|
||||
(if
|
||||
(or (= i slen) (and mi? (rx-newline? (rx-char-at s i))))
|
||||
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
|
||||
-1))
|
||||
((= t "anchor-word")
|
||||
(if
|
||||
(rx-is-word-boundary? s i slen)
|
||||
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
|
||||
-1))
|
||||
((= t "anchor-nonword")
|
||||
(if
|
||||
(not (rx-is-word-boundary? s i slen))
|
||||
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
|
||||
-1))
|
||||
((= t "group")
|
||||
(let
|
||||
((gidx (get node "__idx__"))
|
||||
(inner (get node "__nodes__")))
|
||||
(let
|
||||
((g-end (rx-match-nodes inner s i slen ci? mi? groups)))
|
||||
(if
|
||||
(>= g-end 0)
|
||||
(begin
|
||||
(dict-set!
|
||||
groups
|
||||
(js-to-string gidx)
|
||||
(substring s i g-end))
|
||||
(let
|
||||
((final-end (rx-match-nodes rest-nodes s g-end slen ci? mi? groups)))
|
||||
(if
|
||||
(>= final-end 0)
|
||||
final-end
|
||||
(begin
|
||||
(dict-set! groups (js-to-string gidx) nil)
|
||||
-1))))
|
||||
-1))))
|
||||
((= t "ncgroup")
|
||||
(let
|
||||
((inner (get node "__nodes__")))
|
||||
(rx-match-nodes
|
||||
(append inner rest-nodes)
|
||||
s
|
||||
i
|
||||
slen
|
||||
ci?
|
||||
mi?
|
||||
groups)))
|
||||
((= t "alt")
|
||||
(let
|
||||
((branches (get node "__branches__")))
|
||||
(rx-try-branches branches rest-nodes s i slen ci? mi? groups)))
|
||||
((= t "quant")
|
||||
(let
|
||||
((inner-node (get node "__node__"))
|
||||
(mn (get node "__min__"))
|
||||
(mx (get node "__max__"))
|
||||
(lazy? (get node "__lazy__")))
|
||||
(if
|
||||
lazy?
|
||||
(rx-quant-lazy
|
||||
inner-node
|
||||
mn
|
||||
mx
|
||||
rest-nodes
|
||||
s
|
||||
i
|
||||
slen
|
||||
ci?
|
||||
mi?
|
||||
groups
|
||||
0)
|
||||
(rx-quant-greedy
|
||||
inner-node
|
||||
mn
|
||||
mx
|
||||
rest-nodes
|
||||
s
|
||||
i
|
||||
slen
|
||||
ci?
|
||||
mi?
|
||||
groups
|
||||
0))))
|
||||
(else -1)))))))
|
||||
|
||||
(define
|
||||
rx-try-branches
|
||||
(fn
|
||||
(branches rest-nodes s i slen ci? mi? groups)
|
||||
(if
|
||||
(empty? branches)
|
||||
-1
|
||||
(let
|
||||
((res (rx-match-nodes (append (first branches) rest-nodes) s i slen ci? mi? groups)))
|
||||
(if
|
||||
(>= res 0)
|
||||
res
|
||||
(rx-try-branches (rest branches) rest-nodes s i slen ci? mi? groups))))))
|
||||
|
||||
;; Greedy: expand as far as possible, then try rest from the longest match
|
||||
;; Strategy: recurse forward (extend first); only try rest when extension fails
|
||||
(define
|
||||
rx-quant-greedy
|
||||
(fn
|
||||
(inner-node mn mx rest-nodes s i slen ci? mi? groups count)
|
||||
(let
|
||||
((can-extend (and (< i slen) (or (= mx -1) (< count mx)))))
|
||||
(if
|
||||
can-extend
|
||||
(let
|
||||
((ni (rx-match-one inner-node s i slen ci? mi? groups)))
|
||||
(if
|
||||
(>= ni 0)
|
||||
(let
|
||||
((res (rx-quant-greedy inner-node mn mx rest-nodes s ni slen ci? mi? groups (+ count 1))))
|
||||
(if
|
||||
(>= res 0)
|
||||
res
|
||||
(if
|
||||
(>= count mn)
|
||||
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
|
||||
-1)))
|
||||
(if
|
||||
(>= count mn)
|
||||
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
|
||||
-1)))
|
||||
(if
|
||||
(>= count mn)
|
||||
(rx-match-nodes rest-nodes s i slen ci? mi? groups)
|
||||
-1)))))
|
||||
|
||||
;; Lazy: try rest first, extend only if rest fails
|
||||
(define
|
||||
rx-quant-lazy
|
||||
(fn
|
||||
(inner-node mn mx rest-nodes s i slen ci? mi? groups count)
|
||||
(if
|
||||
(>= count mn)
|
||||
(let
|
||||
((res (rx-match-nodes rest-nodes s i slen ci? mi? groups)))
|
||||
(if
|
||||
(>= res 0)
|
||||
res
|
||||
(if
|
||||
(and (< i slen) (or (= mx -1) (< count mx)))
|
||||
(let
|
||||
((ni (rx-match-one inner-node s i slen ci? mi? groups)))
|
||||
(if
|
||||
(>= ni 0)
|
||||
(rx-quant-lazy
|
||||
inner-node
|
||||
mn
|
||||
mx
|
||||
rest-nodes
|
||||
s
|
||||
ni
|
||||
slen
|
||||
ci?
|
||||
mi?
|
||||
groups
|
||||
(+ count 1))
|
||||
-1))
|
||||
-1)))
|
||||
(if
|
||||
(< i slen)
|
||||
(let
|
||||
((ni (rx-match-one inner-node s i slen ci? mi? groups)))
|
||||
(if
|
||||
(>= ni 0)
|
||||
(rx-quant-lazy
|
||||
inner-node
|
||||
mn
|
||||
mx
|
||||
rest-nodes
|
||||
s
|
||||
ni
|
||||
slen
|
||||
ci?
|
||||
mi?
|
||||
groups
|
||||
(+ count 1))
|
||||
-1))
|
||||
-1))))
|
||||
|
||||
;; Match a single node at position i, return new pos or -1
|
||||
(define
|
||||
rx-match-one
|
||||
(fn
|
||||
(node s i slen ci? mi? groups)
|
||||
(rx-match-nodes (list node) s i slen ci? mi? groups)))
|
||||
|
||||
;; ── Engine entry points ───────────────────────────────────────────
|
||||
|
||||
;; Try matching at exactly position i. Returns result dict or nil.
|
||||
(define
|
||||
rx-try-at
|
||||
(fn
|
||||
(compiled s i slen ci? mi?)
|
||||
(let
|
||||
((nodes (get compiled "nodes")) (ngroups (get compiled "ngroups")))
|
||||
(let
|
||||
((groups (dict)))
|
||||
(let
|
||||
((end (rx-match-nodes nodes s i slen ci? mi? groups)))
|
||||
(if
|
||||
(>= end 0)
|
||||
(dict "start" i "end" end "groups" groups "ngroups" ngroups)
|
||||
nil))))))
|
||||
|
||||
;; Find first match scanning from search-start.
|
||||
(define
|
||||
rx-find-from
|
||||
(fn
|
||||
(compiled s search-start slen ci? mi?)
|
||||
(if
|
||||
(> search-start slen)
|
||||
nil
|
||||
(let
|
||||
((res (rx-try-at compiled s search-start slen ci? mi?)))
|
||||
(if
|
||||
res
|
||||
res
|
||||
(rx-find-from compiled s (+ search-start 1) slen ci? mi?))))))
|
||||
|
||||
;; Build exec result dict from raw match result
|
||||
(define
|
||||
rx-build-exec-result
|
||||
(fn
|
||||
(s match-res)
|
||||
(let
|
||||
((start (get match-res "start"))
|
||||
(end (get match-res "end"))
|
||||
(groups (get match-res "groups"))
|
||||
(ngroups (get match-res "ngroups")))
|
||||
(let
|
||||
((matched (substring s start end))
|
||||
(caps (rx-build-captures groups ngroups 1)))
|
||||
(dict "match" matched "index" start "input" s "groups" caps)))))
|
||||
|
||||
(define
|
||||
rx-build-captures
|
||||
(fn
|
||||
(groups ngroups idx)
|
||||
(if
|
||||
(> idx ngroups)
|
||||
(list)
|
||||
(let
|
||||
((cap (get groups (js-to-string idx))))
|
||||
(cons
|
||||
(if (= cap nil) :js-undefined cap)
|
||||
(rx-build-captures groups ngroups (+ idx 1)))))))
|
||||
|
||||
;; ── Public interface ──────────────────────────────────────────────
|
||||
|
||||
;; Lazy compile: build NFA on first use, cache under "__compiled__"
|
||||
(define
|
||||
rx-ensure-compiled!
|
||||
(fn
|
||||
(rx)
|
||||
(if
|
||||
(dict-has? rx "__compiled__")
|
||||
(get rx "__compiled__")
|
||||
(let
|
||||
((c (rx-compile (get rx "source"))))
|
||||
(begin (dict-set! rx "__compiled__" c) c)))))
|
||||
|
||||
(define
|
||||
rx-test
|
||||
(fn
|
||||
(rx s)
|
||||
(let
|
||||
((compiled (rx-ensure-compiled! rx))
|
||||
(ci? (get rx "ignoreCase"))
|
||||
(mi? (get rx "multiline"))
|
||||
(slen (len s)))
|
||||
(let
|
||||
((start (if (get rx "global") (let ((li (get rx "lastIndex"))) (if (number? li) li 0)) 0)))
|
||||
(let
|
||||
((res (rx-find-from compiled s start slen ci? mi?)))
|
||||
(if
|
||||
(get rx "global")
|
||||
(begin
|
||||
(dict-set! rx "lastIndex" (if res (get res "end") 0))
|
||||
(if res true false))
|
||||
(if res true false)))))))
|
||||
|
||||
(define
|
||||
rx-exec
|
||||
(fn
|
||||
(rx s)
|
||||
(let
|
||||
((compiled (rx-ensure-compiled! rx))
|
||||
(ci? (get rx "ignoreCase"))
|
||||
(mi? (get rx "multiline"))
|
||||
(slen (len s)))
|
||||
(let
|
||||
((start (if (get rx "global") (let ((li (get rx "lastIndex"))) (if (number? li) li 0)) 0)))
|
||||
(let
|
||||
((res (rx-find-from compiled s start slen ci? mi?)))
|
||||
(if
|
||||
res
|
||||
(begin
|
||||
(when
|
||||
(get rx "global")
|
||||
(dict-set! rx "lastIndex" (get res "end")))
|
||||
(rx-build-exec-result s res))
|
||||
(begin
|
||||
(when (get rx "global") (dict-set! rx "lastIndex" 0))
|
||||
nil)))))))
|
||||
|
||||
;; match-all for String.prototype.matchAll
|
||||
(define
|
||||
js-regex-match-all
|
||||
(fn
|
||||
(rx s)
|
||||
(let
|
||||
((compiled (rx-ensure-compiled! rx))
|
||||
(ci? (get rx "ignoreCase"))
|
||||
(mi? (get rx "multiline"))
|
||||
(slen (len s))
|
||||
(results (list)))
|
||||
(rx-match-all-loop compiled s 0 slen ci? mi? results))))
|
||||
|
||||
(define
|
||||
rx-match-all-loop
|
||||
(fn
|
||||
(compiled s i slen ci? mi? results)
|
||||
(if
|
||||
(> i slen)
|
||||
results
|
||||
(let
|
||||
((res (rx-find-from compiled s i slen ci? mi?)))
|
||||
(if
|
||||
res
|
||||
(begin
|
||||
(append! results (rx-build-exec-result s res))
|
||||
(let
|
||||
((next (get res "end")))
|
||||
(rx-match-all-loop
|
||||
compiled
|
||||
s
|
||||
(if (= next i) (+ i 1) next)
|
||||
slen
|
||||
ci?
|
||||
mi?
|
||||
results)))
|
||||
results)))))
|
||||
|
||||
;; ── Install platform ──────────────────────────────────────────────
|
||||
|
||||
(js-regex-platform-override! "test" rx-test)
|
||||
(js-regex-platform-override! "exec" rx-exec)
|
||||
@@ -2032,7 +2032,15 @@
|
||||
(&rest args)
|
||||
(cond
|
||||
((= (len args) 0) nil)
|
||||
((js-regex? (nth args 0)) (js-regex-stub-exec (nth args 0) s))
|
||||
((js-regex? (nth args 0))
|
||||
(let
|
||||
((rx (nth args 0)))
|
||||
(let
|
||||
((impl (get __js_regex_platform__ "exec")))
|
||||
(if
|
||||
(js-undefined? impl)
|
||||
(js-regex-stub-exec rx s)
|
||||
(impl rx s)))))
|
||||
(else
|
||||
(let
|
||||
((needle (js-to-string (nth args 0))))
|
||||
@@ -2041,7 +2049,7 @@
|
||||
(if
|
||||
(= idx -1)
|
||||
nil
|
||||
(let ((res (list))) (append! res needle) res))))))))
|
||||
(let ((res (list))) (begin (append! res needle) res)))))))))
|
||||
((= name "at")
|
||||
(fn
|
||||
(i)
|
||||
@@ -2099,6 +2107,20 @@
|
||||
((= name "toWellFormed") (fn () s))
|
||||
(else js-undefined))))
|
||||
|
||||
(define __js_tdz_sentinel__ (dict "__tdz__" true))
|
||||
|
||||
(define js-tdz? (fn (v) (and (dict? v) (dict-has? v "__tdz__"))))
|
||||
|
||||
(define
|
||||
js-tdz-check
|
||||
(fn
|
||||
(name val)
|
||||
(if
|
||||
(js-tdz? val)
|
||||
(raise
|
||||
(TypeError (str "Cannot access '" name "' before initialization")))
|
||||
val)))
|
||||
|
||||
(define
|
||||
js-string-slice
|
||||
(fn
|
||||
|
||||
239
lib/js/stdlib.sx
Normal file
239
lib/js/stdlib.sx
Normal file
@@ -0,0 +1,239 @@
|
||||
;; lib/js/stdlib.sx — Phase 22 JS additions
|
||||
;;
|
||||
;; Adds to lib/js/runtime.sx (already loaded):
|
||||
;; 1. Bitwise binary ops (js-bitand/bitor/bitxor/lshift/rshift/urshift/bitnot)
|
||||
;; 2. Map class (arbitrary-key hash map via list of pairs)
|
||||
;; 3. Set class (uniqueness collection via SX make-set)
|
||||
;; 4. RegExp constructor (wraps js-regex-new already in runtime)
|
||||
;; 5. Wires Map / Set / RegExp into js-global
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Bitwise binary ops
|
||||
;; JS coerces operands to 32-bit signed int before applying the op.
|
||||
;; Use truncate (not js-num-to-int) since integer / 0 crashes the evaluator.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(js-bitand a b)
|
||||
(bitwise-and (truncate (js-to-number a)) (truncate (js-to-number b))))
|
||||
|
||||
(define
|
||||
(js-bitor a b)
|
||||
(bitwise-or (truncate (js-to-number a)) (truncate (js-to-number b))))
|
||||
|
||||
(define
|
||||
(js-bitxor a b)
|
||||
(bitwise-xor (truncate (js-to-number a)) (truncate (js-to-number b))))
|
||||
|
||||
;; << : left-shift by (b mod 32) positions
|
||||
(define
|
||||
(js-lshift a b)
|
||||
(arithmetic-shift
|
||||
(truncate (js-to-number a))
|
||||
(modulo (truncate (js-to-number b)) 32)))
|
||||
|
||||
;; >> : arithmetic right-shift (sign-extending)
|
||||
(define
|
||||
(js-rshift a b)
|
||||
(arithmetic-shift
|
||||
(truncate (js-to-number a))
|
||||
(- 0 (modulo (truncate (js-to-number b)) 32))))
|
||||
|
||||
;; >>> : logical right-shift (zero-extending)
|
||||
;; Convert to uint32 first, then divide by 2^n.
|
||||
(define
|
||||
(js-urshift a b)
|
||||
(let
|
||||
((u32 (modulo (truncate (js-to-number a)) 4294967296))
|
||||
(n (modulo (truncate (js-to-number b)) 32)))
|
||||
(quotient u32 (arithmetic-shift 1 n))))
|
||||
|
||||
;; ~ : bitwise NOT — equivalent to -(n+1) in 32-bit signed arithmetic
|
||||
(define (js-bitnot a) (bitwise-not (truncate (js-to-number a))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Map class
|
||||
;; Stored as {:__js_map__ true :size N :_pairs (list (list key val) ...)}
|
||||
;; Mutation via dict-set! on the underlying dict.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(js-map-new)
|
||||
(let
|
||||
((m (dict)))
|
||||
(dict-set! m "__js_map__" true)
|
||||
(dict-set! m "size" 0)
|
||||
(dict-set! m "_pairs" (list))
|
||||
m))
|
||||
|
||||
(define (js-map? v) (and (dict? v) (dict-has? v "__js_map__")))
|
||||
|
||||
;; Linear scan for key using ===; returns index or -1
|
||||
(define
|
||||
(js-map-find-idx pairs k)
|
||||
(letrec
|
||||
((go (fn (ps i) (cond ((= (len ps) 0) -1) ((js-strict-eq (first (first ps)) k) i) (else (go (rest ps) (+ i 1)))))))
|
||||
(go pairs 0)))
|
||||
|
||||
(define
|
||||
(js-map-get m k)
|
||||
(letrec
|
||||
((go (fn (ps) (if (= (len ps) 0) js-undefined (if (js-strict-eq (first (first ps)) k) (nth (first ps) 1) (go (rest ps)))))))
|
||||
(go (get m "_pairs"))))
|
||||
|
||||
;; Replace element at index i in list
|
||||
(define
|
||||
(js-list-set-nth lst i newval)
|
||||
(letrec
|
||||
((go (fn (ps j) (if (= (len ps) 0) (list) (cons (if (= j i) newval (first ps)) (go (rest ps) (+ j 1)))))))
|
||||
(go lst 0)))
|
||||
|
||||
;; Remove element at index i from list
|
||||
(define
|
||||
(js-list-remove-nth lst i)
|
||||
(letrec
|
||||
((go (fn (ps j) (if (= (len ps) 0) (list) (if (= j i) (go (rest ps) (+ j 1)) (cons (first ps) (go (rest ps) (+ j 1))))))))
|
||||
(go lst 0)))
|
||||
|
||||
(define
|
||||
(js-map-set! m k v)
|
||||
(let
|
||||
((pairs (get m "_pairs")) (idx (js-map-find-idx (get m "_pairs") k)))
|
||||
(if
|
||||
(= idx -1)
|
||||
(begin
|
||||
(dict-set! m "_pairs" (append pairs (list (list k v))))
|
||||
(dict-set! m "size" (+ (get m "size") 1)))
|
||||
(dict-set! m "_pairs" (js-list-set-nth pairs idx (list k v)))))
|
||||
m)
|
||||
|
||||
(define
|
||||
(js-map-has m k)
|
||||
(not (= (js-map-find-idx (get m "_pairs") k) -1)))
|
||||
|
||||
(define
|
||||
(js-map-delete! m k)
|
||||
(let
|
||||
((idx (js-map-find-idx (get m "_pairs") k)))
|
||||
(when
|
||||
(not (= idx -1))
|
||||
(dict-set! m "_pairs" (js-list-remove-nth (get m "_pairs") idx))
|
||||
(dict-set! m "size" (- (get m "size") 1))))
|
||||
m)
|
||||
|
||||
(define
|
||||
(js-map-clear! m)
|
||||
(dict-set! m "_pairs" (list))
|
||||
(dict-set! m "size" 0)
|
||||
m)
|
||||
|
||||
(define (js-map-keys m) (map first (get m "_pairs")))
|
||||
(define
|
||||
(js-map-vals m)
|
||||
(map (fn (p) (nth p 1)) (get m "_pairs")))
|
||||
(define (js-map-entries m) (get m "_pairs"))
|
||||
|
||||
(define
|
||||
(js-map-for-each m cb)
|
||||
(for-each
|
||||
(fn (p) (cb (nth p 1) (first p) m))
|
||||
(get m "_pairs"))
|
||||
js-undefined)
|
||||
|
||||
;; Map method dispatch (called from js-object-method-call in runtime)
|
||||
(define
|
||||
(js-map-method m name args)
|
||||
(cond
|
||||
((= name "set")
|
||||
(js-map-set! m (nth args 0) (nth args 1)))
|
||||
((= name "get") (js-map-get m (nth args 0)))
|
||||
((= name "has") (js-map-has m (nth args 0)))
|
||||
((= name "delete") (js-map-delete! m (nth args 0)))
|
||||
((= name "clear") (js-map-clear! m))
|
||||
((= name "keys") (js-map-keys m))
|
||||
((= name "values") (js-map-vals m))
|
||||
((= name "entries") (js-map-entries m))
|
||||
((= name "forEach") (js-map-for-each m (nth args 0)))
|
||||
((= name "toString") "[object Map]")
|
||||
(else js-undefined)))
|
||||
|
||||
(define Map {:__callable__ (fn (&rest args) (let ((m (js-map-new))) (when (and (> (len args) 0) (list? (nth args 0))) (for-each (fn (entry) (js-map-set! m (nth entry 0) (nth entry 1))) (nth args 0))) m)) :prototype {:entries (fn (&rest a) (js-map-entries (js-this))) :delete (fn (&rest a) (js-map-delete! (js-this) (nth a 0))) :get (fn (&rest a) (js-map-get (js-this) (nth a 0))) :values (fn (&rest a) (js-map-vals (js-this))) :toString (fn () "[object Map]") :has (fn (&rest a) (js-map-has (js-this) (nth a 0))) :set (fn (&rest a) (js-map-set! (js-this) (nth a 0) (nth a 1))) :forEach (fn (&rest a) (js-map-for-each (js-this) (nth a 0))) :clear (fn (&rest a) (js-map-clear! (js-this))) :keys (fn (&rest a) (js-map-keys (js-this)))}})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Set class
|
||||
;; {:__js_set__ true :size N :_set <sx-set>}
|
||||
;; Note: set-member?/set-add!/set-remove! all take (set item) order.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(js-set-new)
|
||||
(let
|
||||
((s (dict)))
|
||||
(dict-set! s "__js_set__" true)
|
||||
(dict-set! s "size" 0)
|
||||
(dict-set! s "_set" (make-set))
|
||||
s))
|
||||
|
||||
(define (js-set? v) (and (dict? v) (dict-has? v "__js_set__")))
|
||||
|
||||
(define
|
||||
(js-set-add! s v)
|
||||
(let
|
||||
((sx (get s "_set")))
|
||||
(when
|
||||
(not (set-member? sx v))
|
||||
(set-add! sx v)
|
||||
(dict-set! s "size" (+ (get s "size") 1))))
|
||||
s)
|
||||
|
||||
(define (js-set-has s v) (set-member? (get s "_set") v))
|
||||
|
||||
(define
|
||||
(js-set-delete! s v)
|
||||
(let
|
||||
((sx (get s "_set")))
|
||||
(when
|
||||
(set-member? sx v)
|
||||
(set-remove! sx v)
|
||||
(dict-set! s "size" (- (get s "size") 1))))
|
||||
s)
|
||||
|
||||
(define
|
||||
(js-set-clear! s)
|
||||
(dict-set! s "_set" (make-set))
|
||||
(dict-set! s "size" 0)
|
||||
s)
|
||||
|
||||
(define (js-set-vals s) (set->list (get s "_set")))
|
||||
|
||||
(define
|
||||
(js-set-for-each s cb)
|
||||
(for-each (fn (v) (cb v v s)) (set->list (get s "_set")))
|
||||
js-undefined)
|
||||
|
||||
(define Set {:__callable__ (fn (&rest args) (let ((s (js-set-new))) (when (and (> (len args) 0) (list? (nth args 0))) (for-each (fn (v) (js-set-add! s v)) (nth args 0))) s)) :prototype {:entries (fn (&rest a) (map (fn (v) (list v v)) (js-set-vals (js-this)))) :delete (fn (&rest a) (js-set-delete! (js-this) (nth a 0))) :values (fn (&rest a) (js-set-vals (js-this))) :add (fn (&rest a) (js-set-add! (js-this) (nth a 0))) :toString (fn () "[object Set]") :has (fn (&rest a) (js-set-has (js-this) (nth a 0))) :forEach (fn (&rest a) (js-set-for-each (js-this) (nth a 0))) :clear (fn (&rest a) (js-set-clear! (js-this))) :keys (fn (&rest a) (js-set-vals (js-this)))}})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. RegExp constructor — callable lambda wrapping js-regex-new
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
RegExp
|
||||
(fn
|
||||
(&rest args)
|
||||
(cond
|
||||
((= (len args) 0) (js-regex-new "" ""))
|
||||
((= (len args) 1)
|
||||
(js-regex-new (js-to-string (nth args 0)) ""))
|
||||
(else
|
||||
(js-regex-new
|
||||
(js-to-string (nth args 0))
|
||||
(js-to-string (nth args 1)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Wire new globals into js-global
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(dict-set! js-global "Map" Map)
|
||||
(dict-set! js-global "Set" Set)
|
||||
(dict-set! js-global "RegExp" RegExp)
|
||||
239
lib/js/test.sh
239
lib/js/test.sh
@@ -33,6 +33,10 @@ cat > "$TMPFILE" << 'EPOCHS'
|
||||
(load "lib/js/transpile.sx")
|
||||
(epoch 5)
|
||||
(load "lib/js/runtime.sx")
|
||||
(epoch 6)
|
||||
(load "lib/js/regex.sx")
|
||||
(epoch 7)
|
||||
(load "lib/js/stdlib.sx")
|
||||
|
||||
;; ── Phase 0: stubs still behave ─────────────────────────────────
|
||||
(epoch 10)
|
||||
@@ -1323,6 +1327,166 @@ cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 3505)
|
||||
(eval "(js-eval \"var a = {length: 3, 0: 10, 1: 20, 2: 30}; var sum = 0; Array.prototype.forEach.call(a, function(x){sum += x;}); sum\")")
|
||||
|
||||
;; ── Phase 12: Regex engine ────────────────────────────────────────
|
||||
;; Platform is installed (test key is a function, not undefined)
|
||||
(epoch 5000)
|
||||
(eval "(js-undefined? (get __js_regex_platform__ \"test\"))")
|
||||
(epoch 5001)
|
||||
(eval "(js-eval \"/foo/.test('hi foo bar')\")")
|
||||
(epoch 5002)
|
||||
(eval "(js-eval \"/foo/.test('hi bar')\")")
|
||||
;; Case-insensitive flag
|
||||
(epoch 5003)
|
||||
(eval "(js-eval \"/FOO/i.test('hello foo world')\")")
|
||||
;; Anchors
|
||||
(epoch 5004)
|
||||
(eval "(js-eval \"/^hello/.test('hello world')\")")
|
||||
(epoch 5005)
|
||||
(eval "(js-eval \"/^hello/.test('say hello')\")")
|
||||
(epoch 5006)
|
||||
(eval "(js-eval \"/world$/.test('hello world')\")")
|
||||
;; Character classes
|
||||
(epoch 5007)
|
||||
(eval "(js-eval \"/\\\\d+/.test('abc 123')\")")
|
||||
(epoch 5008)
|
||||
(eval "(js-eval \"/\\\\w+/.test('hello')\")")
|
||||
(epoch 5009)
|
||||
(eval "(js-eval \"/[abc]/.test('dog')\")")
|
||||
(epoch 5010)
|
||||
(eval "(js-eval \"/[abc]/.test('cat')\")")
|
||||
;; Quantifiers
|
||||
(epoch 5011)
|
||||
(eval "(js-eval \"/a*b/.test('b')\")")
|
||||
(epoch 5012)
|
||||
(eval "(js-eval \"/a+b/.test('b')\")")
|
||||
(epoch 5013)
|
||||
(eval "(js-eval \"/a{2,3}/.test('aa')\")")
|
||||
(epoch 5014)
|
||||
(eval "(js-eval \"/a{2,3}/.test('a')\")")
|
||||
;; Dot
|
||||
(epoch 5015)
|
||||
(eval "(js-eval \"/h.llo/.test('hello')\")")
|
||||
(epoch 5016)
|
||||
(eval "(js-eval \"/h.llo/.test('hllo')\")")
|
||||
;; exec result
|
||||
(epoch 5017)
|
||||
(eval "(js-eval \"var m = /foo(\\\\w+)/.exec('foobar'); m.match\")")
|
||||
(epoch 5018)
|
||||
(eval "(js-eval \"var m = /foo(\\\\w+)/.exec('foobar'); m.index\")")
|
||||
(epoch 5019)
|
||||
(eval "(js-eval \"var m = /foo(\\\\w+)/.exec('foobar'); m.groups[0]\")")
|
||||
;; Alternation
|
||||
(epoch 5020)
|
||||
(eval "(js-eval \"/cat|dog/.test('I have a dog')\")")
|
||||
(epoch 5021)
|
||||
(eval "(js-eval \"/cat|dog/.test('I have a fish')\")")
|
||||
;; Non-capturing group
|
||||
(epoch 5022)
|
||||
(eval "(js-eval \"/(?:foo)+/.test('foofoo')\")")
|
||||
;; Negated char class
|
||||
(epoch 5023)
|
||||
(eval "(js-eval \"/[^abc]/.test('d')\")")
|
||||
(epoch 5024)
|
||||
(eval "(js-eval \"/[^abc]/.test('a')\")")
|
||||
;; Range inside char class
|
||||
(epoch 5025)
|
||||
(eval "(js-eval \"/[a-z]+/.test('hello')\")")
|
||||
;; Word boundary
|
||||
(epoch 5026)
|
||||
(eval "(js-eval \"/\\\\bword\\\\b/.test('a word here')\")")
|
||||
(epoch 5027)
|
||||
(eval "(js-eval \"/\\\\bword\\\\b/.test('password')\")")
|
||||
;; Lazy quantifier
|
||||
(epoch 5028)
|
||||
(eval "(js-eval \"var m = /a+?/.exec('aaa'); m.match\")")
|
||||
;; Global flag exec
|
||||
(epoch 5029)
|
||||
(eval "(js-eval \"var r=/\\\\d+/g; r.exec('a1b2'); r.exec('a1b2').match\")")
|
||||
;; String.prototype.match with regex
|
||||
(epoch 5030)
|
||||
(eval "(js-eval \"'hello world'.match(/\\\\w+/).match\")")
|
||||
;; String.prototype.search
|
||||
(epoch 5031)
|
||||
(eval "(js-eval \"'hello world'.search(/world/)\")")
|
||||
;; String.prototype.replace with regex
|
||||
(epoch 5032)
|
||||
(eval "(js-eval \"'hello world'.replace(/world/, 'there')\")")
|
||||
;; multiline anchor
|
||||
(epoch 5033)
|
||||
(eval "(js-eval \"/^bar/m.test('foo\\nbar')\")")
|
||||
|
||||
;; ── Phase 13: let/const TDZ infrastructure ───────────────────────
|
||||
;; The TDZ sentinel and checker are defined in runtime.sx.
|
||||
;; let/const bindings work normally after initialization.
|
||||
(epoch 5100)
|
||||
(eval "(js-eval \"let x = 5; x\")")
|
||||
(epoch 5101)
|
||||
(eval "(js-eval \"const y = 42; y\")")
|
||||
;; TDZ sentinel exists and is detectable
|
||||
(epoch 5102)
|
||||
(eval "(js-tdz? __js_tdz_sentinel__)")
|
||||
;; js-tdz-check passes through non-sentinel values
|
||||
(epoch 5103)
|
||||
(eval "(js-tdz-check \"x\" 42)")
|
||||
|
||||
;; ── Phase 22: Bitwise ops ────────────────────────────────────────
|
||||
(epoch 6000)
|
||||
(eval "(js-bitand 5 3)")
|
||||
(epoch 6001)
|
||||
(eval "(js-bitor 5 3)")
|
||||
(epoch 6002)
|
||||
(eval "(js-bitxor 5 3)")
|
||||
(epoch 6003)
|
||||
(eval "(js-lshift 1 4)")
|
||||
(epoch 6004)
|
||||
(eval "(js-rshift 32 2)")
|
||||
(epoch 6005)
|
||||
(eval "(js-rshift -8 1)")
|
||||
(epoch 6006)
|
||||
(eval "(js-urshift 4294967292 2)")
|
||||
(epoch 6007)
|
||||
(eval "(js-bitnot 0)")
|
||||
|
||||
;; ── Phase 22: Map ─────────────────────────────────────────────────
|
||||
(epoch 6010)
|
||||
(eval "(js-map? (js-map-new))")
|
||||
(epoch 6011)
|
||||
(eval "(get (js-map-set! (js-map-new) \"k\" 42) \"size\")")
|
||||
(epoch 6012)
|
||||
(eval "(let ((m (js-map-new))) (js-map-set! m \"a\" 1) (js-map-get m \"a\"))")
|
||||
(epoch 6013)
|
||||
(eval "(let ((m (js-map-new))) (js-map-set! m \"x\" 9) (js-map-has m \"x\"))")
|
||||
(epoch 6014)
|
||||
(eval "(let ((m (js-map-new))) (js-map-set! m \"x\" 9) (js-map-has m \"y\"))")
|
||||
(epoch 6015)
|
||||
(eval "(let ((m (js-map-new))) (js-map-set! m \"a\" 1) (js-map-set! m \"b\" 2) (get m \"size\"))")
|
||||
(epoch 6016)
|
||||
(eval "(let ((m (js-map-new))) (js-map-set! m \"a\" 1) (js-map-delete! m \"a\") (get m \"size\"))")
|
||||
(epoch 6017)
|
||||
(eval "(let ((m (js-map-new))) (js-map-set! m \"a\" 1) (js-map-set! m \"a\" 99) (js-map-get m \"a\"))")
|
||||
|
||||
;; ── Phase 22: Set ─────────────────────────────────────────────────
|
||||
(epoch 6020)
|
||||
(eval "(js-set? (js-set-new))")
|
||||
(epoch 6021)
|
||||
(eval "(let ((s (js-set-new))) (js-set-add! s 1) (js-set-has s 1))")
|
||||
(epoch 6022)
|
||||
(eval "(let ((s (js-set-new))) (js-set-add! s 1) (js-set-has s 2))")
|
||||
(epoch 6023)
|
||||
(eval "(let ((s (js-set-new))) (js-set-add! s 1) (js-set-add! s 1) (get s \"size\"))")
|
||||
(epoch 6024)
|
||||
(eval "(let ((s (js-set-new))) (js-set-add! s 1) (js-set-add! s 2) (get s \"size\"))")
|
||||
(epoch 6025)
|
||||
(eval "(let ((s (js-set-new))) (js-set-add! s 1) (js-set-delete! s 1) (get s \"size\"))")
|
||||
|
||||
;; ── Phase 22: RegExp constructor ──────────────────────────────────
|
||||
(epoch 6030)
|
||||
(eval "(js-regex? (RegExp \"ab\" \"i\"))")
|
||||
(epoch 6031)
|
||||
(eval "(get (RegExp \"hello\" \"gi\") \"global\")")
|
||||
(epoch 6032)
|
||||
(eval "(get (RegExp \"foo\" \"i\") \"ignoreCase\")")
|
||||
|
||||
EPOCHS
|
||||
|
||||
|
||||
@@ -2042,6 +2206,81 @@ check 3503 "indexOf.call arrLike" '1'
|
||||
check 3504 "filter.call arrLike" '"2,3"'
|
||||
check 3505 "forEach.call arrLike sum" '60'
|
||||
|
||||
# ── Phase 12: Regex engine ────────────────────────────────────────
|
||||
check 5000 "regex platform installed" 'false'
|
||||
check 5001 "/foo/ matches" 'true'
|
||||
check 5002 "/foo/ no match" 'false'
|
||||
check 5003 "/FOO/i case-insensitive" 'true'
|
||||
check 5004 "/^hello/ anchor match" 'true'
|
||||
check 5005 "/^hello/ anchor no-match" 'false'
|
||||
check 5006 "/world$/ end anchor" 'true'
|
||||
check 5007 "/\\d+/ digit class" 'true'
|
||||
check 5008 "/\\w+/ word class" 'true'
|
||||
check 5009 "/[abc]/ class no-match" 'false'
|
||||
check 5010 "/[abc]/ class match" 'true'
|
||||
check 5011 "/a*b/ zero-or-more" 'true'
|
||||
check 5012 "/a+b/ one-or-more no-match" 'false'
|
||||
check 5013 "/a{2,3}/ quant match" 'true'
|
||||
check 5014 "/a{2,3}/ quant no-match" 'false'
|
||||
check 5015 "dot matches any" 'true'
|
||||
check 5016 "dot requires char" 'false'
|
||||
check 5017 "exec match string" '"foobar"'
|
||||
check 5018 "exec match index" '0'
|
||||
check 5019 "exec capture group" '"bar"'
|
||||
check 5020 "alternation cat|dog match" 'true'
|
||||
check 5021 "alternation cat|dog no-match" 'false'
|
||||
check 5022 "non-capturing group" 'true'
|
||||
check 5023 "negated class match" 'true'
|
||||
check 5024 "negated class no-match" 'false'
|
||||
check 5025 "range [a-z]+" 'true'
|
||||
check 5026 "word boundary match" 'true'
|
||||
check 5027 "word boundary no-match" 'false'
|
||||
check 5028 "lazy quantifier" '"a"'
|
||||
check 5029 "global exec advances" '"2"'
|
||||
check 5030 "String.match regex" '"hello"'
|
||||
check 5031 "String.search regex" '6'
|
||||
check 5032 "String.replace regex" '"hello there"'
|
||||
check 5033 "multiline anchor" 'true'
|
||||
|
||||
# ── Phase 13: let/const TDZ infrastructure ───────────────────────
|
||||
check 5100 "let binding initialized" '5'
|
||||
check 5101 "const binding initialized" '42'
|
||||
check 5102 "TDZ sentinel is detectable" 'true'
|
||||
check 5103 "tdz-check passes non-sentinel" '42'
|
||||
|
||||
# ── Phase 22: Bitwise ops ─────────────────────────────────────────
|
||||
check 6000 "bitand 5&3" '1'
|
||||
check 6001 "bitor 5|3" '7'
|
||||
check 6002 "bitxor 5^3" '6'
|
||||
check 6003 "lshift 1<<4" '16'
|
||||
check 6004 "rshift 32>>2" '8'
|
||||
check 6005 "rshift -8>>1" '-4'
|
||||
check 6006 "urshift >>>" '1073741823'
|
||||
check 6007 "bitnot ~0" '-1'
|
||||
|
||||
# ── Phase 22: Map ─────────────────────────────────────────────────
|
||||
check 6010 "map? new map" 'true'
|
||||
check 6011 "map set→size 1" '1'
|
||||
check 6012 "map get existing" '1'
|
||||
check 6013 "map has key yes" 'true'
|
||||
check 6014 "map has key no" 'false'
|
||||
check 6015 "map size 2 entries" '2'
|
||||
check 6016 "map delete→size 0" '0'
|
||||
check 6017 "map set overwrites" '99'
|
||||
|
||||
# ── Phase 22: Set ─────────────────────────────────────────────────
|
||||
check 6020 "set? new set" 'true'
|
||||
check 6021 "set has after add" 'true'
|
||||
check 6022 "set has absent" 'false'
|
||||
check 6023 "set dedup size" '1'
|
||||
check 6024 "set size 2" '2'
|
||||
check 6025 "set delete→size 0" '0'
|
||||
|
||||
# ── Phase 22: RegExp ──────────────────────────────────────────────
|
||||
check 6030 "RegExp? result" 'true'
|
||||
check 6031 "RegExp global flag" 'true'
|
||||
check 6032 "RegExp ignoreCase" 'true'
|
||||
|
||||
TOTAL=$((PASS + FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "✓ $PASS/$TOTAL JS-on-SX tests passed"
|
||||
|
||||
@@ -798,6 +798,7 @@ class ServerSession:
|
||||
self._run_and_collect(3, '(load "lib/js/parser.sx")', timeout=60.0)
|
||||
self._run_and_collect(4, '(load "lib/js/transpile.sx")', timeout=60.0)
|
||||
self._run_and_collect(5, '(load "lib/js/runtime.sx")', timeout=60.0)
|
||||
self._run_and_collect(50, '(load "lib/js/regex.sx")', timeout=60.0)
|
||||
# Preload the stub harness — use precomputed SX cache when available
|
||||
# (huge win: ~15s js-eval HARNESS_STUB → ~0s load precomputed .sx).
|
||||
cache_rel = _harness_cache_rel_path()
|
||||
|
||||
@@ -935,12 +935,12 @@
|
||||
|
||||
(define
|
||||
js-transpile-var
|
||||
(fn (kind decls) (cons (js-sym "begin") (js-vardecl-forms decls))))
|
||||
(fn (kind decls) (cons (js-sym "begin") (js-vardecl-forms kind decls))))
|
||||
|
||||
(define
|
||||
js-vardecl-forms
|
||||
(fn
|
||||
(decls)
|
||||
(kind decls)
|
||||
(cond
|
||||
((empty? decls) (list))
|
||||
(else
|
||||
@@ -953,7 +953,7 @@
|
||||
(js-sym "define")
|
||||
(js-sym (nth d 1))
|
||||
(js-transpile (nth d 2)))
|
||||
(js-vardecl-forms (rest decls))))
|
||||
(js-vardecl-forms kind (rest decls))))
|
||||
((js-tag? d "js-vardecl-obj")
|
||||
(let
|
||||
((names (nth d 1))
|
||||
@@ -964,7 +964,7 @@
|
||||
(js-vardecl-obj-forms
|
||||
names
|
||||
tmp-sym
|
||||
(js-vardecl-forms (rest decls))))))
|
||||
(js-vardecl-forms kind (rest decls))))))
|
||||
((js-tag? d "js-vardecl-arr")
|
||||
(let
|
||||
((names (nth d 1))
|
||||
@@ -976,7 +976,7 @@
|
||||
names
|
||||
tmp-sym
|
||||
0
|
||||
(js-vardecl-forms (rest decls))))))
|
||||
(js-vardecl-forms kind (rest decls))))))
|
||||
(else (error "js-vardecl-forms: unexpected decl"))))))))
|
||||
|
||||
(define
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
(fn
|
||||
(i)
|
||||
(if
|
||||
(has? a (str i))
|
||||
(not (= (get a (str i)) nil))
|
||||
(begin (set! n i) (count-loop (+ i 1)))
|
||||
n)))
|
||||
(count-loop 1))))
|
||||
@@ -152,7 +152,9 @@
|
||||
(cond
|
||||
((= (first f) "pos")
|
||||
(begin
|
||||
(set! t (assoc t (str array-idx) (nth f 1)))
|
||||
(set!
|
||||
t
|
||||
(assoc t (str array-idx) (nth f 1)))
|
||||
(set! array-idx (+ array-idx 1))))
|
||||
((= (first f) "kv")
|
||||
(let
|
||||
@@ -169,3 +171,108 @@
|
||||
(if (= t nil) nil (let ((v (get t (str k)))) (if (= v nil) nil v)))))
|
||||
|
||||
(define lua-set! (fn (t k v) (assoc t (str k) v)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Helpers for stdlib
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Apply a char function to every character in a string
|
||||
(define (lua-str-map s fn) (list->string (map fn (string->list s))))
|
||||
|
||||
;; Repeat string s n times
|
||||
(define
|
||||
(lua-str-rep s n)
|
||||
(letrec
|
||||
((go (fn (acc i) (if (= i 0) acc (go (str acc s) (- i 1))))))
|
||||
(go "" n)))
|
||||
|
||||
;; Force a promise created by delay
|
||||
(define
|
||||
(lua-force p)
|
||||
(if
|
||||
(and (dict? p) (get p :_promise))
|
||||
(if (get p :forced) (get p :value) ((get p :thunk)))
|
||||
p))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; math — Lua math library
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define math {:asin asin :floor floor :exp exp :huge 1e+308 :tan tan :sqrt sqrt :log log :abs abs :ceil ceil :sin sin :max (fn (a b) (if (> a b) a b)) :acos acos :min (fn (a b) (if (< a b) a b)) :cos cos :pi 3.14159 :atan atan})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; string — Lua string library
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(lua-string-find s pat)
|
||||
(let
|
||||
((m (regexp-match (make-regexp pat) s)))
|
||||
(if (= m nil) nil (list (+ (get m :start) 1) (get m :end)))))
|
||||
|
||||
(define
|
||||
(lua-string-match s pat)
|
||||
(let
|
||||
((m (regexp-match (make-regexp pat) s)))
|
||||
(if
|
||||
(= m nil)
|
||||
nil
|
||||
(let
|
||||
((groups (get m :groups)))
|
||||
(if (= (len groups) 0) (get m :match) (first groups))))))
|
||||
|
||||
(define
|
||||
(lua-string-gmatch s pat)
|
||||
(map (fn (m) (get m :match)) (regexp-match-all (make-regexp pat) s)))
|
||||
|
||||
(define
|
||||
(lua-string-gsub s pat repl)
|
||||
(regexp-replace-all (make-regexp pat) s repl))
|
||||
|
||||
(define string {:rep lua-str-rep :sub (fn (s i &rest j-args) (let ((slen (len s)) (j (if (= (len j-args) 0) -1 (first j-args)))) (let ((from (if (< i 0) (let ((r (+ slen i))) (if (< r 0) 0 r)) (- i 1))) (to (if (< j 0) (let ((r (+ slen j 1))) (if (< r 0) 0 r)) (if (> j slen) slen j)))) (if (> from to) "" (substring s from to))))) :len (fn (s) (len s)) :upper (fn (s) (lua-str-map s char-upcase)) :char (fn (&rest codes) (list->string (map (fn (c) (integer->char (truncate c))) codes))) :gmatch lua-string-gmatch :gsub lua-string-gsub :lower (fn (s) (lua-str-map s char-downcase)) :byte (fn (s &rest args) (char->integer (nth (string->list s) (- (if (= (len args) 0) 1 (first args)) 1)))) :match lua-string-match :find lua-string-find :reverse (fn (s) (list->string (reverse (string->list s))))})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; table — Lua table library
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(lua-table-insert t v)
|
||||
(assoc t (str (+ (lua-len t) 1)) v))
|
||||
|
||||
(define
|
||||
(lua-table-remove t &rest args)
|
||||
(let
|
||||
((n (lua-len t))
|
||||
(pos (if (= (len args) 0) (lua-len t) (first args))))
|
||||
(letrec
|
||||
((slide (fn (t i) (if (< i n) (assoc (slide t (+ i 1)) (str i) (lua-get t (+ i 1))) (assoc t (str n) nil)))))
|
||||
(slide t pos))))
|
||||
|
||||
(define
|
||||
(lua-table-concat t &rest args)
|
||||
(let
|
||||
((sep (if (= (len args) 0) "" (first args)))
|
||||
(n (lua-len t)))
|
||||
(letrec
|
||||
((go (fn (acc i) (if (> i n) acc (go (str acc (if (= i 1) "" sep) (lua-to-string (lua-get t i))) (+ i 1))))))
|
||||
(go "" 1))))
|
||||
|
||||
(define
|
||||
(lua-table-sort t)
|
||||
(let
|
||||
((n (lua-len t)))
|
||||
(letrec
|
||||
((collect (fn (i acc) (if (< i 1) acc (collect (- i 1) (cons (lua-get t i) acc)))))
|
||||
(rebuild
|
||||
(fn
|
||||
(t i items)
|
||||
(if
|
||||
(= (len items) 0)
|
||||
t
|
||||
(rebuild
|
||||
(assoc t (str i) (first items))
|
||||
(+ i 1)
|
||||
(rest items))))))
|
||||
(rebuild t 1 (sort (collect n (list)))))))
|
||||
|
||||
(define table {:sort lua-table-sort :concat lua-table-concat :insert lua-table-insert :remove lua-table-remove})
|
||||
|
||||
110
lib/lua/test.sh
110
lib/lua/test.sh
@@ -633,6 +633,116 @@ check 482 "while i<5 count" '5'
|
||||
check 483 "repeat until i>=3" '3'
|
||||
check 484 "for 1..100 sum" '5050'
|
||||
|
||||
# ── Phase 3: stdlib — math, string, table ──────────────────────────────────
|
||||
|
||||
cat >> "$TMPFILE" << 'EPOCHS2'
|
||||
|
||||
;; ── math library ───────────────────────────────────────────────
|
||||
(epoch 500)
|
||||
(eval "(lua-eval-ast \"return math.abs(-7)\")")
|
||||
(epoch 501)
|
||||
(eval "(lua-eval-ast \"return math.floor(3.9)\")")
|
||||
(epoch 502)
|
||||
(eval "(lua-eval-ast \"return math.ceil(3.1)\")")
|
||||
(epoch 503)
|
||||
(eval "(lua-eval-ast \"return math.sqrt(9)\")")
|
||||
(epoch 504)
|
||||
(eval "(lua-eval-ast \"return math.sin(0)\")")
|
||||
(epoch 505)
|
||||
(eval "(lua-eval-ast \"return math.cos(0)\")")
|
||||
(epoch 506)
|
||||
(eval "(lua-eval-ast \"return math.max(3, 7)\")")
|
||||
(epoch 507)
|
||||
(eval "(lua-eval-ast \"return math.min(3, 7)\")")
|
||||
(epoch 508)
|
||||
(eval "(lua-eval-ast \"return math.pi > 3\")")
|
||||
(epoch 509)
|
||||
(eval "(lua-eval-ast \"return math.huge > 0\")")
|
||||
|
||||
;; ── string library ─────────────────────────────────────────────
|
||||
(epoch 520)
|
||||
(eval "(lua-eval-ast \"return string.len(\\\"hello\\\")\")")
|
||||
(epoch 521)
|
||||
(eval "(lua-eval-ast \"return string.upper(\\\"hello\\\")\")")
|
||||
(epoch 522)
|
||||
(eval "(lua-eval-ast \"return string.lower(\\\"WORLD\\\")\")")
|
||||
(epoch 523)
|
||||
(eval "(lua-eval-ast \"return string.sub(\\\"hello\\\", 2, 4)\")")
|
||||
(epoch 524)
|
||||
(eval "(lua-eval-ast \"return string.rep(\\\"ab\\\", 3)\")")
|
||||
(epoch 525)
|
||||
(eval "(lua-eval-ast \"return string.reverse(\\\"hello\\\")\")")
|
||||
(epoch 526)
|
||||
(eval "(lua-eval-ast \"return string.byte(\\\"A\\\")\")")
|
||||
(epoch 527)
|
||||
(eval "(lua-eval-ast \"return string.char(72, 105)\")")
|
||||
(epoch 528)
|
||||
(eval "(lua-eval-ast \"return string.find(\\\"hello\\\", \\\"ll\\\")\")")
|
||||
(epoch 529)
|
||||
(eval "(lua-eval-ast \"return string.match(\\\"hello\\\", \\\"ell\\\")\")")
|
||||
(epoch 530)
|
||||
(eval "(lua-eval-ast \"return string.gsub(\\\"hello\\\", \\\"l\\\", \\\"r\\\")\")")
|
||||
|
||||
;; ── table library ──────────────────────────────────────────────
|
||||
(epoch 540)
|
||||
(eval "(lua-eval-ast \"local t = {10, 20, 30} t = table.insert(t, 40) return t[4]\")")
|
||||
(epoch 541)
|
||||
(eval "(lua-eval-ast \"local t = {10, 20, 30} t = table.remove(t) return t[3]\")")
|
||||
(epoch 542)
|
||||
(eval "(lua-eval-ast \"local t = {\\\"a\\\", \\\"b\\\", \\\"c\\\"} return table.concat(t, \\\",\\\")\")")
|
||||
(epoch 543)
|
||||
(eval "(lua-eval-ast \"local t = {3, 1, 2} t = table.sort(t) return t[1]\")")
|
||||
(epoch 544)
|
||||
(eval "(lua-eval-ast \"local t = {3, 1, 2} t = table.sort(t) return t[3]\")")
|
||||
|
||||
;; ── delay / force ──────────────────────────────────────────────
|
||||
(epoch 550)
|
||||
(eval "(lua-force (delay (+ 10 5)))")
|
||||
(epoch 551)
|
||||
(eval "(lua-force 42)")
|
||||
|
||||
EPOCHS2
|
||||
|
||||
OUTPUT2=$(timeout 30 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
OUTPUT="$OUTPUT
|
||||
$OUTPUT2"
|
||||
|
||||
# math
|
||||
check 500 "math.abs(-7)" '7'
|
||||
check 501 "math.floor(3.9)" '3'
|
||||
check 502 "math.ceil(3.1)" '4'
|
||||
check 503 "math.sqrt(9)" '3'
|
||||
check 504 "math.sin(0)" '0'
|
||||
check 505 "math.cos(0)" '1'
|
||||
check 506 "math.max(3,7)" '7'
|
||||
check 507 "math.min(3,7)" '3'
|
||||
check 508 "math.pi > 3" 'true'
|
||||
check 509 "math.huge > 0" 'true'
|
||||
|
||||
# string
|
||||
check 520 "string.len" '5'
|
||||
check 521 "string.upper" '"HELLO"'
|
||||
check 522 "string.lower" '"world"'
|
||||
check 523 "string.sub(2,4)" '"ell"'
|
||||
check 524 "string.rep(ab,3)" '"ababab"'
|
||||
check 525 "string.reverse" '"olleh"'
|
||||
check 526 "string.byte(A)" '65'
|
||||
check 527 "string.char(72,105)" '"Hi"'
|
||||
check 528 "string.find ll" '3'
|
||||
check 529 "string.match ell" '"ell"'
|
||||
check 530 "string.gsub l->r" '"herro"'
|
||||
|
||||
# table
|
||||
check 540 "table.insert" '40'
|
||||
check 541 "table.remove" 'nil'
|
||||
check 542 "table.concat ," '"a,b,c"'
|
||||
check 543 "table.sort [1]" '1'
|
||||
check 544 "table.sort [3]" '3'
|
||||
|
||||
# delay/force
|
||||
check 550 "lua-force delay" '15'
|
||||
check 551 "lua-force non-promise" '42'
|
||||
|
||||
TOTAL=$((PASS + FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "ok $PASS/$TOTAL Lua-on-SX tests passed"
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
;; lib/prolog/compiler.sx — clause compiler: parse-AST clauses → SX closures
|
||||
;;
|
||||
;; Each compiled clause is a lambda (fn (goal trail db cut-box k) bool)
|
||||
;; that creates fresh vars, builds the instantiated head/body, and calls
|
||||
;; pl-unify! + pl-solve! directly — no AST walk at solve time.
|
||||
;;
|
||||
;; Usage:
|
||||
;; (pl-db-load! db (pl-parse src))
|
||||
;; (pl-compile-db! db)
|
||||
;; ; pl-solve-user! in runtime.sx automatically prefers compiled clauses
|
||||
;; (pl-solve-once! db goal trail)
|
||||
|
||||
;; Collect unique variable names from a parse-AST clause into a dict.
|
||||
(define
|
||||
pl-cmp-vars-into!
|
||||
(fn
|
||||
(ast seen)
|
||||
(cond
|
||||
((not (list? ast)) nil)
|
||||
((empty? ast) nil)
|
||||
((= (first ast) "var")
|
||||
(let
|
||||
((name (nth ast 1)))
|
||||
(when
|
||||
(and (not (= name "_")) (not (dict-has? seen name)))
|
||||
(dict-set! seen name true))))
|
||||
((= (first ast) "compound")
|
||||
(for-each (fn (a) (pl-cmp-vars-into! a seen)) (nth ast 2)))
|
||||
((= (first ast) "clause")
|
||||
(begin
|
||||
(pl-cmp-vars-into! (nth ast 1) seen)
|
||||
(pl-cmp-vars-into! (nth ast 2) seen))))))
|
||||
|
||||
;; Return list of unique var names in a clause (head + body, excluding _).
|
||||
(define
|
||||
pl-cmp-collect-vars
|
||||
(fn
|
||||
(clause)
|
||||
(let ((seen {})) (pl-cmp-vars-into! clause seen) (keys seen))))
|
||||
|
||||
;; Create a fresh runtime var for each name in the list; return name->var dict.
|
||||
(define
|
||||
pl-cmp-make-var-map
|
||||
(fn
|
||||
(var-names)
|
||||
(let
|
||||
((m {}))
|
||||
(for-each
|
||||
(fn (name) (dict-set! m name (pl-mk-rt-var name)))
|
||||
var-names)
|
||||
m)))
|
||||
|
||||
;; Instantiate a parse-AST term using a pre-built var-map.
|
||||
;; ("var" "_") always gets a fresh anonymous var.
|
||||
(define
|
||||
pl-cmp-build-term
|
||||
(fn
|
||||
(ast var-map)
|
||||
(cond
|
||||
((pl-var? ast) ast)
|
||||
((not (list? ast)) ast)
|
||||
((empty? ast) ast)
|
||||
((= (first ast) "var")
|
||||
(let
|
||||
((name (nth ast 1)))
|
||||
(if (= name "_") (pl-mk-rt-var "_") (dict-get var-map name))))
|
||||
((or (= (first ast) "atom") (= (first ast) "num") (= (first ast) "str"))
|
||||
ast)
|
||||
((= (first ast) "compound")
|
||||
(list
|
||||
"compound"
|
||||
(nth ast 1)
|
||||
(map (fn (a) (pl-cmp-build-term a var-map)) (nth ast 2))))
|
||||
((= (first ast) "clause")
|
||||
(list
|
||||
"clause"
|
||||
(pl-cmp-build-term (nth ast 1) var-map)
|
||||
(pl-cmp-build-term (nth ast 2) var-map)))
|
||||
(true ast))))
|
||||
|
||||
;; Compile one parse-AST clause to a lambda.
|
||||
;; Pre-computes var names at compile time; creates fresh vars per call.
|
||||
(define
|
||||
pl-compile-clause
|
||||
(fn
|
||||
(clause)
|
||||
(let
|
||||
((var-names (pl-cmp-collect-vars clause))
|
||||
(head-ast (nth clause 1))
|
||||
(body-ast (nth clause 2)))
|
||||
(fn
|
||||
(goal trail db cut-box k)
|
||||
(let
|
||||
((var-map (pl-cmp-make-var-map var-names)))
|
||||
(let
|
||||
((fresh-head (pl-cmp-build-term head-ast var-map))
|
||||
(fresh-body (pl-cmp-build-term body-ast var-map)))
|
||||
(let
|
||||
((mark (pl-trail-mark trail)))
|
||||
(if
|
||||
(pl-unify! goal fresh-head trail)
|
||||
(let
|
||||
((r (pl-solve! db fresh-body trail cut-box k)))
|
||||
(if r true (begin (pl-trail-undo-to! trail mark) false)))
|
||||
(begin (pl-trail-undo-to! trail mark) false)))))))))
|
||||
|
||||
;; Try a list of compiled clause lambdas — same cut semantics as pl-try-clauses!.
|
||||
(define
|
||||
pl-try-compiled-clauses!
|
||||
(fn
|
||||
(db
|
||||
goal
|
||||
trail
|
||||
compiled-clauses
|
||||
outer-cut-box
|
||||
outer-was-cut
|
||||
inner-cut-box
|
||||
k)
|
||||
(cond
|
||||
((empty? compiled-clauses) false)
|
||||
(true
|
||||
(let
|
||||
((r ((first compiled-clauses) goal trail db inner-cut-box k)))
|
||||
(cond
|
||||
(r true)
|
||||
((dict-get inner-cut-box :cut) false)
|
||||
((and (not outer-was-cut) (dict-get outer-cut-box :cut)) false)
|
||||
(true
|
||||
(pl-try-compiled-clauses!
|
||||
db
|
||||
goal
|
||||
trail
|
||||
(rest compiled-clauses)
|
||||
outer-cut-box
|
||||
outer-was-cut
|
||||
inner-cut-box
|
||||
k))))))))
|
||||
|
||||
;; Compile all clauses in DB and store in :compiled table.
|
||||
;; After this call, pl-solve-user! will dispatch via compiled lambdas.
|
||||
;; Note: clauses assert!-ed after this call are not compiled.
|
||||
(define
|
||||
pl-compile-db!
|
||||
(fn
|
||||
(db)
|
||||
(let
|
||||
((src-table (dict-get db :clauses)) (compiled-table {}))
|
||||
(for-each
|
||||
(fn
|
||||
(key)
|
||||
(dict-set!
|
||||
compiled-table
|
||||
key
|
||||
(map pl-compile-clause (dict-get src-table key))))
|
||||
(keys src-table))
|
||||
(dict-set! db :compiled compiled-table)
|
||||
db)))
|
||||
|
||||
;; Cross-validate: load src into both a plain and a compiled DB,
|
||||
;; run goal-str through each, return true iff solution counts match.
|
||||
;; Use this to keep the interpreter as the reference implementation.
|
||||
(define
|
||||
pl-compiled-matches-interp?
|
||||
(fn
|
||||
(src goal-str)
|
||||
(let
|
||||
((db-interp (pl-mk-db)) (db-comp (pl-mk-db)))
|
||||
(pl-db-load! db-interp (pl-parse src))
|
||||
(pl-db-load! db-comp (pl-parse src))
|
||||
(pl-compile-db! db-comp)
|
||||
(let
|
||||
((gi (pl-instantiate (pl-parse-goal goal-str) {}))
|
||||
(gc (pl-instantiate (pl-parse-goal goal-str) {})))
|
||||
(=
|
||||
(pl-solve-count! db-interp gi (pl-mk-trail))
|
||||
(pl-solve-count! db-comp gc (pl-mk-trail)))))))
|
||||
@@ -1,129 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run every Prolog test suite via sx_server and refresh scoreboard.{json,md}.
|
||||
# Exit 0 if all green, 1 if any failures.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
SX="${SX_SERVER:-/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
|
||||
if [[ ! -x "$SX" ]]; then
|
||||
echo "sx_server not found at $SX (set SX_SERVER env to override)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
# name : test-file : runner-fn
|
||||
SUITES=(
|
||||
"parse:lib/prolog/tests/parse.sx:pl-parse-tests-run!"
|
||||
"unify:lib/prolog/tests/unify.sx:pl-unify-tests-run!"
|
||||
"clausedb:lib/prolog/tests/clausedb.sx:pl-clausedb-tests-run!"
|
||||
"solve:lib/prolog/tests/solve.sx:pl-solve-tests-run!"
|
||||
"operators:lib/prolog/tests/operators.sx:pl-operators-tests-run!"
|
||||
"dynamic:lib/prolog/tests/dynamic.sx:pl-dynamic-tests-run!"
|
||||
"findall:lib/prolog/tests/findall.sx:pl-findall-tests-run!"
|
||||
"term_inspect:lib/prolog/tests/term_inspect.sx:pl-term-inspect-tests-run!"
|
||||
"append:lib/prolog/tests/programs/append.sx:pl-append-tests-run!"
|
||||
"reverse:lib/prolog/tests/programs/reverse.sx:pl-reverse-tests-run!"
|
||||
"member:lib/prolog/tests/programs/member.sx:pl-member-tests-run!"
|
||||
"nqueens:lib/prolog/tests/programs/nqueens.sx:pl-nqueens-tests-run!"
|
||||
"family:lib/prolog/tests/programs/family.sx:pl-family-tests-run!"
|
||||
"atoms:lib/prolog/tests/atoms.sx:pl-atom-tests-run!"
|
||||
"query_api:lib/prolog/tests/query_api.sx:pl-query-api-tests-run!"
|
||||
"iso_predicates:lib/prolog/tests/iso_predicates.sx:pl-iso-predicates-tests-run!"
|
||||
"meta_predicates:lib/prolog/tests/meta_predicates.sx:pl-meta-predicates-tests-run!"
|
||||
"list_predicates:lib/prolog/tests/list_predicates.sx:pl-list-predicates-tests-run!"
|
||||
"meta_call:lib/prolog/tests/meta_call.sx:pl-meta-call-tests-run!"
|
||||
"set_predicates:lib/prolog/tests/set_predicates.sx:pl-set-predicates-tests-run!"
|
||||
"char_predicates:lib/prolog/tests/char_predicates.sx:pl-char-predicates-tests-run!"
|
||||
"io_predicates:lib/prolog/tests/io_predicates.sx:pl-io-predicates-tests-run!"
|
||||
"assert_rules:lib/prolog/tests/assert_rules.sx:pl-assert-rules-tests-run!"
|
||||
"string_agg:lib/prolog/tests/string_agg.sx:pl-string-agg-tests-run!"
|
||||
"advanced:lib/prolog/tests/advanced.sx:pl-advanced-tests-run!"
|
||||
"compiler:lib/prolog/tests/compiler.sx:pl-compiler-tests-run!"
|
||||
"cross_validate:lib/prolog/tests/cross_validate.sx:pl-cross-validate-tests-run!"
|
||||
"integration:lib/prolog/tests/integration.sx:pl-integration-tests-run!"
|
||||
"hs_bridge:lib/prolog/tests/hs_bridge.sx:pl-hs-bridge-tests-run!"
|
||||
)
|
||||
|
||||
SCRIPT='(epoch 1)
|
||||
(load "lib/prolog/tokenizer.sx")
|
||||
(load "lib/prolog/parser.sx")
|
||||
(load "lib/prolog/runtime.sx")
|
||||
(load "lib/prolog/query.sx")
|
||||
(load "lib/prolog/compiler.sx")
|
||||
(load "lib/prolog/hs-bridge.sx")'
|
||||
for entry in "${SUITES[@]}"; do
|
||||
IFS=: read -r _ file _ <<< "$entry"
|
||||
SCRIPT+=$'\n(load "'"$file"$'")'
|
||||
done
|
||||
for entry in "${SUITES[@]}"; do
|
||||
IFS=: read -r _ _ fn <<< "$entry"
|
||||
SCRIPT+=$'\n(eval "('"$fn"$')")'
|
||||
done
|
||||
|
||||
OUTPUT="$(printf '%s\n' "$SCRIPT" | "$SX" 2>&1)"
|
||||
|
||||
mapfile -t LINES < <(printf '%s\n' "$OUTPUT" | grep -E '^\{:failed')
|
||||
|
||||
if [[ ${#LINES[@]} -ne ${#SUITES[@]} ]]; then
|
||||
echo "Expected ${#SUITES[@]} suite results, got ${#LINES[@]}" >&2
|
||||
echo "---- raw output ----" >&2
|
||||
printf '%s\n' "$OUTPUT" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
TOTAL_PASS=0
|
||||
TOTAL_FAIL=0
|
||||
TOTAL=0
|
||||
JSON_SUITES=""
|
||||
MD_ROWS=""
|
||||
|
||||
for i in "${!SUITES[@]}"; do
|
||||
IFS=: read -r name _ _ <<< "${SUITES[$i]}"
|
||||
line="${LINES[$i]}"
|
||||
passed=$(grep -oE ':passed [0-9]+' <<< "$line" | grep -oE '[0-9]+')
|
||||
total=$(grep -oE ':total [0-9]+' <<< "$line" | grep -oE '[0-9]+')
|
||||
failed=$(grep -oE ':failed [0-9]+' <<< "$line" | grep -oE '[0-9]+')
|
||||
TOTAL_PASS=$((TOTAL_PASS + passed))
|
||||
TOTAL_FAIL=$((TOTAL_FAIL + failed))
|
||||
TOTAL=$((TOTAL + total))
|
||||
status="ok"
|
||||
[[ "$failed" -gt 0 ]] && status="FAIL"
|
||||
[[ -n "$JSON_SUITES" ]] && JSON_SUITES+=","
|
||||
JSON_SUITES+="\"$name\":{\"passed\":$passed,\"total\":$total,\"failed\":$failed}"
|
||||
MD_ROWS+="| $name | $passed | $total | $status |"$'\n'
|
||||
done
|
||||
|
||||
WHEN="$(date -Iseconds 2>/dev/null || date)"
|
||||
|
||||
cat > "$HERE/scoreboard.json" <<JSON
|
||||
{
|
||||
"total_passed": $TOTAL_PASS,
|
||||
"total_failed": $TOTAL_FAIL,
|
||||
"total": $TOTAL,
|
||||
"suites": {$JSON_SUITES},
|
||||
"generated": "$WHEN"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$HERE/scoreboard.md" <<MD
|
||||
# Prolog scoreboard
|
||||
|
||||
**$TOTAL_PASS / $TOTAL passing** ($TOTAL_FAIL failure(s)).
|
||||
Generated $WHEN.
|
||||
|
||||
| Suite | Passed | Total | Status |
|
||||
|-------|--------|-------|--------|
|
||||
$MD_ROWS
|
||||
Run \`bash lib/prolog/conformance.sh\` to refresh. Override the binary
|
||||
with \`SX_SERVER=path/to/sx_server.exe bash …\`.
|
||||
MD
|
||||
|
||||
if [[ "$TOTAL_FAIL" -gt 0 ]]; then
|
||||
echo "$TOTAL_FAIL failure(s) across $TOTAL tests" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All $TOTAL tests pass."
|
||||
@@ -1,72 +0,0 @@
|
||||
;; lib/prolog/hs-bridge.sx — Prolog↔Hyperscript bridge
|
||||
;;
|
||||
;; Creates SX functions backed by a Prolog DB, callable directly from
|
||||
;; Hyperscript DSL conditions. No parser/compiler changes needed:
|
||||
;; when allowed(user, action) then …
|
||||
;; compiles to (allowed user action) — a plain SX call.
|
||||
;;
|
||||
;; Setup:
|
||||
;; (define pl-db (pl-load "role(alice,admin). permission(admin,edit). allowed(U,A) :- role(U,R), permission(R,A)."))
|
||||
;; (define allowed (pl-hs-predicate/2 pl-db "allowed"))
|
||||
;;
|
||||
;; Requires tokenizer.sx, parser.sx, runtime.sx, query.sx loaded first.
|
||||
|
||||
;; Test whether a ground Prolog goal succeeds against db.
|
||||
;; Returns true/false (not a solution dict).
|
||||
(define
|
||||
pl-hs-query
|
||||
(fn (db goal-str) (not (nil? (pl-query-one db goal-str)))))
|
||||
|
||||
;; Build a Prolog goal string from a predicate name and arg list.
|
||||
;; SX values: strings/keywords (already strings in SX) pass through;
|
||||
;; numbers are stringified via str.
|
||||
(define
|
||||
pl-hs-build-goal
|
||||
(fn
|
||||
(pred-name args)
|
||||
(str pred-name "(" (join ", " (map (fn (a) (str a)) args)) ")")))
|
||||
|
||||
;; Return a 1-arg SX function that succeeds iff pred(a) holds in db.
|
||||
(define
|
||||
pl-hs-predicate/1
|
||||
(fn
|
||||
(db pred-name)
|
||||
(fn (a) (pl-hs-query db (pl-hs-build-goal pred-name (list a))))))
|
||||
|
||||
;; Return a 2-arg SX function that succeeds iff pred(a, b) holds in db.
|
||||
(define
|
||||
pl-hs-predicate/2
|
||||
(fn
|
||||
(db pred-name)
|
||||
(fn (a b) (pl-hs-query db (pl-hs-build-goal pred-name (list a b))))))
|
||||
|
||||
;; Return a 3-arg SX function that succeeds iff pred(a, b, c) holds in db.
|
||||
(define
|
||||
pl-hs-predicate/3
|
||||
(fn
|
||||
(db pred-name)
|
||||
(fn (a b c) (pl-hs-query db (pl-hs-build-goal pred-name (list a b c))))))
|
||||
|
||||
;; Install every predicate in install-list as a named def in the caller's
|
||||
;; environment. install-list: list of (name arity) pairs.
|
||||
;; Returns a dict {name → fn} for the caller to destructure.
|
||||
(define
|
||||
pl-hs-install
|
||||
(fn
|
||||
(db install-list)
|
||||
(reduce
|
||||
(fn
|
||||
(acc entry)
|
||||
(let
|
||||
((pred-name (first entry)) (arity (nth entry 1)))
|
||||
(dict-set!
|
||||
acc
|
||||
pred-name
|
||||
(cond
|
||||
((= arity 1) (pl-hs-predicate/1 db pred-name))
|
||||
((= arity 2) (pl-hs-predicate/2 db pred-name))
|
||||
((= arity 3) (pl-hs-predicate/3 db pred-name))
|
||||
(true (fn (a b) false))))
|
||||
acc))
|
||||
{}
|
||||
install-list)))
|
||||
@@ -1,20 +1,28 @@
|
||||
;; lib/prolog/parser.sx — tokens → Prolog AST
|
||||
;;
|
||||
;; Phase 4 grammar (with operator table):
|
||||
;; Phase 1 grammar (NO operator table yet):
|
||||
;; Program := Clause* EOF
|
||||
;; Clause := Term[999] "." | Term[999] ":-" Term[1200] "."
|
||||
;; Term[Pmax] uses precedence climbing on the operator table:
|
||||
;; primary = Atom | Var | Number | String | Compound | List | "(" Term[1200] ")"
|
||||
;; while next token is infix op `op` with prec(op) ≤ Pmax:
|
||||
;; consume op; parse rhs at right-prec(op); fold into compound(op-name,[lhs,rhs])
|
||||
;; Clause := Term "." | Term ":-" Term "."
|
||||
;; Term := Atom | Var | Number | String | Compound | List
|
||||
;; Compound := atom "(" ArgList ")"
|
||||
;; ArgList := Term ("," Term)*
|
||||
;; List := "[" "]" | "[" Term ("," Term)* ("|" Term)? "]"
|
||||
;;
|
||||
;; Op type → right-prec for op at precedence P:
|
||||
;; xfx → P-1 strict-both
|
||||
;; xfy → P right-associative
|
||||
;; yfx → P-1 left-associative
|
||||
;; Term AST shapes (all tagged lists for uniform dispatch):
|
||||
;; ("atom" name) — atom
|
||||
;; ("var" name) — variable template (parser-time only)
|
||||
;; ("num" value) — integer or float
|
||||
;; ("str" value) — string literal
|
||||
;; ("compound" functor args) — compound term, args is list of term-ASTs
|
||||
;; ("cut") — the cut atom !
|
||||
;;
|
||||
;; AST shapes are unchanged — operators just become compound terms.
|
||||
;; A clause is (list "clause" head body). A fact is head with body = ("atom" "true").
|
||||
;;
|
||||
;; The empty list is (atom "[]"). Cons is compound "." with two args:
|
||||
;; [1, 2, 3] → .(1, .(2, .(3, [])))
|
||||
;; [H|T] → .(H, T)
|
||||
|
||||
;; ── Parser state helpers ────────────────────────────────────────────
|
||||
(define
|
||||
pp-peek
|
||||
(fn
|
||||
@@ -58,6 +66,7 @@
|
||||
(if (= (get t :value) nil) "" (get t :value))
|
||||
"'"))))))
|
||||
|
||||
;; ── AST constructors ────────────────────────────────────────────────
|
||||
(define pl-mk-atom (fn (name) (list "atom" name)))
|
||||
(define pl-mk-var (fn (name) (list "var" name)))
|
||||
(define pl-mk-num (fn (n) (list "num" n)))
|
||||
@@ -65,14 +74,18 @@
|
||||
(define pl-mk-compound (fn (f args) (list "compound" f args)))
|
||||
(define pl-mk-cut (fn () (list "cut")))
|
||||
|
||||
;; Term tag extractors
|
||||
(define pl-term-tag (fn (t) (if (list? t) (first t) nil)))
|
||||
(define pl-term-val (fn (t) (nth t 1)))
|
||||
(define pl-compound-functor (fn (t) (nth t 1)))
|
||||
(define pl-compound-args (fn (t) (nth t 2)))
|
||||
|
||||
;; Empty-list atom and cons helpers
|
||||
(define pl-nil-term (fn () (pl-mk-atom "[]")))
|
||||
|
||||
(define pl-mk-cons (fn (h t) (pl-mk-compound "." (list h t))))
|
||||
|
||||
;; Build cons list from a list of terms + optional tail
|
||||
(define
|
||||
pl-mk-list-term
|
||||
(fn
|
||||
@@ -82,61 +95,9 @@
|
||||
tail
|
||||
(pl-mk-cons (first items) (pl-mk-list-term (rest items) tail)))))
|
||||
|
||||
;; ── Operator table (Phase 4) ──────────────────────────────────────
|
||||
;; Each entry: (name precedence type). Type ∈ "xfx" "xfy" "yfx".
|
||||
(define
|
||||
pl-op-table
|
||||
(list
|
||||
(list "," 1000 "xfy")
|
||||
(list ";" 1100 "xfy")
|
||||
(list "->" 1050 "xfy")
|
||||
(list "=" 700 "xfx")
|
||||
(list "\\=" 700 "xfx")
|
||||
(list "is" 700 "xfx")
|
||||
(list "<" 700 "xfx")
|
||||
(list ">" 700 "xfx")
|
||||
(list "=<" 700 "xfx")
|
||||
(list ">=" 700 "xfx")
|
||||
(list "+" 500 "yfx")
|
||||
(list "-" 500 "yfx")
|
||||
(list "*" 400 "yfx")
|
||||
(list "/" 400 "yfx")
|
||||
(list ":-" 1200 "xfx")
|
||||
(list "mod" 400 "yfx")))
|
||||
|
||||
(define
|
||||
pl-op-find
|
||||
(fn
|
||||
(name table)
|
||||
(cond
|
||||
((empty? table) nil)
|
||||
((= (first (first table)) name) (rest (first table)))
|
||||
(true (pl-op-find name (rest table))))))
|
||||
|
||||
(define pl-op-lookup (fn (name) (pl-op-find name pl-op-table)))
|
||||
|
||||
;; Token → (name prec type) for known infix ops, else nil.
|
||||
(define
|
||||
pl-token-op
|
||||
(fn
|
||||
(t)
|
||||
(let
|
||||
((ty (get t :type)) (vv (get t :value)))
|
||||
(cond
|
||||
((and (= ty "punct") (= vv ","))
|
||||
(let
|
||||
((info (pl-op-lookup ",")))
|
||||
(if (nil? info) nil (cons "," info))))
|
||||
((or (= ty "atom") (= ty "op"))
|
||||
(let
|
||||
((info (pl-op-lookup vv)))
|
||||
(if (nil? info) nil (cons vv info))))
|
||||
(true nil)))))
|
||||
|
||||
;; ── Term parser ─────────────────────────────────────────────────────
|
||||
;; Primary term: atom, var, num, str, compound (atom + paren), list, cut, parens.
|
||||
(define
|
||||
pp-parse-primary
|
||||
pp-parse-term
|
||||
(fn
|
||||
(st)
|
||||
(let
|
||||
@@ -150,12 +111,6 @@
|
||||
((and (= ty "op") (= vv "!"))
|
||||
(do (pp-advance! st) (pl-mk-cut)))
|
||||
((and (= ty "punct") (= vv "[")) (pp-parse-list st))
|
||||
((and (= ty "punct") (= vv "("))
|
||||
(do
|
||||
(pp-advance! st)
|
||||
(let
|
||||
((inner (pp-parse-term-prec st 1200)))
|
||||
(do (pp-expect! st "punct" ")") inner))))
|
||||
((= ty "atom")
|
||||
(do
|
||||
(pp-advance! st)
|
||||
@@ -178,51 +133,13 @@
|
||||
(if (= vv nil) "" vv)
|
||||
"'"))))))))
|
||||
|
||||
;; Operator-aware term parser: precedence climbing.
|
||||
(define
|
||||
pp-parse-term-prec
|
||||
(fn
|
||||
(st max-prec)
|
||||
(let ((left (pp-parse-primary st))) (pp-parse-op-rhs st left max-prec))))
|
||||
|
||||
(define
|
||||
pp-parse-op-rhs
|
||||
(fn
|
||||
(st left max-prec)
|
||||
(let
|
||||
((op-info (pl-token-op (pp-peek st))))
|
||||
(cond
|
||||
((nil? op-info) left)
|
||||
(true
|
||||
(let
|
||||
((name (first op-info))
|
||||
(prec (nth op-info 1))
|
||||
(ty (nth op-info 2)))
|
||||
(cond
|
||||
((> prec max-prec) left)
|
||||
(true
|
||||
(let
|
||||
((right-prec (if (= ty "xfy") prec (- prec 1))))
|
||||
(do
|
||||
(pp-advance! st)
|
||||
(let
|
||||
((right (pp-parse-term-prec st right-prec)))
|
||||
(pp-parse-op-rhs
|
||||
st
|
||||
(pl-mk-compound name (list left right))
|
||||
max-prec))))))))))))
|
||||
|
||||
;; Backwards-compat alias.
|
||||
(define pp-parse-term (fn (st) (pp-parse-term-prec st 999)))
|
||||
|
||||
;; Args inside parens: parse at prec 999 so comma-as-operator (1000)
|
||||
;; is not consumed; the explicit comma loop handles separation.
|
||||
;; Parse one or more comma-separated terms (arguments).
|
||||
(define
|
||||
pp-parse-arg-list
|
||||
(fn
|
||||
(st)
|
||||
(let
|
||||
((first-arg (pp-parse-term-prec st 999)) (args (list)))
|
||||
((first-arg (pp-parse-term st)) (args (list)))
|
||||
(do
|
||||
(append! args first-arg)
|
||||
(define
|
||||
@@ -233,12 +150,12 @@
|
||||
(pp-at? st "punct" ",")
|
||||
(do
|
||||
(pp-advance! st)
|
||||
(append! args (pp-parse-term-prec st 999))
|
||||
(append! args (pp-parse-term st))
|
||||
(loop)))))
|
||||
(loop)
|
||||
args))))
|
||||
|
||||
;; List literal.
|
||||
;; Parse a [ ... ] list literal. Consumes the "[".
|
||||
(define
|
||||
pp-parse-list
|
||||
(fn
|
||||
@@ -251,7 +168,7 @@
|
||||
(let
|
||||
((items (list)))
|
||||
(do
|
||||
(append! items (pp-parse-term-prec st 999))
|
||||
(append! items (pp-parse-term st))
|
||||
(define
|
||||
comma-loop
|
||||
(fn
|
||||
@@ -260,17 +177,52 @@
|
||||
(pp-at? st "punct" ",")
|
||||
(do
|
||||
(pp-advance! st)
|
||||
(append! items (pp-parse-term-prec st 999))
|
||||
(append! items (pp-parse-term st))
|
||||
(comma-loop)))))
|
||||
(comma-loop)
|
||||
(let
|
||||
((tail (if (pp-at? st "punct" "|") (do (pp-advance! st) (pp-parse-term-prec st 999)) (pl-nil-term))))
|
||||
((tail (if (pp-at? st "punct" "|") (do (pp-advance! st) (pp-parse-term st)) (pl-nil-term))))
|
||||
(do (pp-expect! st "punct" "]") (pl-mk-list-term items tail)))))))))
|
||||
|
||||
;; ── Body parsing ────────────────────────────────────────────────────
|
||||
;; A body is a single term parsed at prec 1200 — operator parser folds
|
||||
;; `,`, `;`, `->` automatically into right-associative compounds.
|
||||
(define pp-parse-body (fn (st) (pp-parse-term-prec st 1200)))
|
||||
;; A clause body is a comma-separated list of goals. We flatten into a
|
||||
;; right-associative `,` compound: (A, B, C) → ','(A, ','(B, C))
|
||||
;; If only one goal, it's that goal directly.
|
||||
(define
|
||||
pp-parse-body
|
||||
(fn
|
||||
(st)
|
||||
(let
|
||||
((first-goal (pp-parse-term st)) (rest-goals (list)))
|
||||
(do
|
||||
(define
|
||||
gloop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(pp-at? st "punct" ",")
|
||||
(do
|
||||
(pp-advance! st)
|
||||
(append! rest-goals (pp-parse-term st))
|
||||
(gloop)))))
|
||||
(gloop)
|
||||
(if
|
||||
(= (len rest-goals) 0)
|
||||
first-goal
|
||||
(pp-build-conj first-goal rest-goals))))))
|
||||
|
||||
(define
|
||||
pp-build-conj
|
||||
(fn
|
||||
(first-goal rest-goals)
|
||||
(if
|
||||
(= (len rest-goals) 0)
|
||||
first-goal
|
||||
(pl-mk-compound
|
||||
","
|
||||
(list
|
||||
first-goal
|
||||
(pp-build-conj (first rest-goals) (rest rest-goals)))))))
|
||||
|
||||
;; ── Clause parsing ──────────────────────────────────────────────────
|
||||
(define
|
||||
@@ -278,11 +230,12 @@
|
||||
(fn
|
||||
(st)
|
||||
(let
|
||||
((head (pp-parse-term-prec st 999)))
|
||||
((head (pp-parse-term st)))
|
||||
(let
|
||||
((body (if (pp-at? st "op" ":-") (do (pp-advance! st) (pp-parse-body st)) (pl-mk-atom "true"))))
|
||||
(do (pp-expect! st "punct" ".") (list "clause" head body))))))
|
||||
|
||||
;; Parse an entire program — returns list of clauses.
|
||||
(define
|
||||
pl-parse-program
|
||||
(fn
|
||||
@@ -300,9 +253,13 @@
|
||||
(ploop)
|
||||
clauses))))
|
||||
|
||||
;; Parse a single query term (no trailing "."). Returns the term.
|
||||
(define
|
||||
pl-parse-query
|
||||
(fn (tokens) (let ((st {:idx 0 :tokens tokens})) (pp-parse-body st))))
|
||||
|
||||
;; Convenience: source → clauses
|
||||
(define pl-parse (fn (src) (pl-parse-program (pl-tokenize src))))
|
||||
|
||||
;; Convenience: source → query term
|
||||
(define pl-parse-goal (fn (src) (pl-parse-query (pl-tokenize src))))
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
;; lib/prolog/query.sx — high-level Prolog query API for SX/Hyperscript callers.
|
||||
;;
|
||||
;; Requires tokenizer.sx, parser.sx, runtime.sx to be loaded first.
|
||||
;;
|
||||
;; Public API:
|
||||
;; (pl-load source-str) → db
|
||||
;; (pl-query-all db query-str) → list of solution dicts {var-name → term-string}
|
||||
;; (pl-query-one db query-str) → first solution dict or nil
|
||||
;; (pl-query source-str query-str) → list of solution dicts (convenience)
|
||||
|
||||
;; Collect variable name strings from a parse-time AST (pre-instantiation).
|
||||
;; Returns list of unique strings, excluding anonymous "_".
|
||||
(define
|
||||
pl-query-extract-vars
|
||||
(fn
|
||||
(ast)
|
||||
(let
|
||||
((seen {}))
|
||||
(let
|
||||
((collect!
|
||||
(fn
|
||||
(t)
|
||||
(cond
|
||||
((not (list? t)) nil)
|
||||
((empty? t) nil)
|
||||
((= (first t) "var")
|
||||
(if
|
||||
(not (= (nth t 1) "_"))
|
||||
(dict-set! seen (nth t 1) true)
|
||||
nil))
|
||||
((= (first t) "compound")
|
||||
(for-each collect! (nth t 2)))
|
||||
(true nil)))))
|
||||
(collect! ast)
|
||||
(keys seen)))))
|
||||
|
||||
;; Build a solution dict from a var-env after a successful solve.
|
||||
;; Maps each variable name string to its formatted term value.
|
||||
(define
|
||||
pl-query-solution-dict
|
||||
(fn
|
||||
(var-names var-env)
|
||||
(let
|
||||
((d {}))
|
||||
(for-each
|
||||
(fn (name) (dict-set! d name (pl-format-term (dict-get var-env name))))
|
||||
var-names)
|
||||
d)))
|
||||
|
||||
;; Parse source-str and load clauses into a fresh DB.
|
||||
;; Returns the DB for reuse across multiple queries.
|
||||
(define
|
||||
pl-load
|
||||
(fn
|
||||
(source-str)
|
||||
(let
|
||||
((db (pl-mk-db)))
|
||||
(if
|
||||
(and (string? source-str) (not (= source-str "")))
|
||||
(pl-db-load! db (pl-parse source-str))
|
||||
nil)
|
||||
db)))
|
||||
|
||||
;; Run query-str against db, returning a list of solution dicts.
|
||||
;; Each dict maps variable name strings to their formatted term values.
|
||||
;; Returns an empty list if no solutions.
|
||||
(define
|
||||
pl-query-all
|
||||
(fn
|
||||
(db query-str)
|
||||
(let
|
||||
((parsed (pl-parse (str "q_ :- " query-str "."))))
|
||||
(let
|
||||
((body-ast (nth (first parsed) 2)))
|
||||
(let
|
||||
((var-names (pl-query-extract-vars body-ast))
|
||||
(var-env {}))
|
||||
(let
|
||||
((goal (pl-instantiate body-ast var-env))
|
||||
(trail (pl-mk-trail))
|
||||
(solutions (list)))
|
||||
(let
|
||||
((mark (pl-trail-mark trail)))
|
||||
(pl-solve!
|
||||
db
|
||||
goal
|
||||
trail
|
||||
{:cut false}
|
||||
(fn
|
||||
()
|
||||
(begin
|
||||
(append!
|
||||
solutions
|
||||
(pl-query-solution-dict var-names var-env))
|
||||
false)))
|
||||
(pl-trail-undo-to! trail mark)
|
||||
solutions)))))))
|
||||
|
||||
;; Return the first solution dict, or nil if no solutions.
|
||||
(define
|
||||
pl-query-one
|
||||
(fn
|
||||
(db query-str)
|
||||
(let
|
||||
((all (pl-query-all db query-str)))
|
||||
(if (empty? all) nil (first all)))))
|
||||
|
||||
;; Convenience: parse source-str, then run query-str against it.
|
||||
;; Returns a list of solution dicts. Creates a fresh DB each call.
|
||||
(define
|
||||
pl-query
|
||||
(fn
|
||||
(source-str query-str)
|
||||
(pl-query-all (pl-load source-str) query-str)))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"total_passed": 590,
|
||||
"total_failed": 0,
|
||||
"total": 590,
|
||||
"suites": {"parse":{"passed":25,"total":25,"failed":0},"unify":{"passed":47,"total":47,"failed":0},"clausedb":{"passed":14,"total":14,"failed":0},"solve":{"passed":62,"total":62,"failed":0},"operators":{"passed":19,"total":19,"failed":0},"dynamic":{"passed":11,"total":11,"failed":0},"findall":{"passed":11,"total":11,"failed":0},"term_inspect":{"passed":14,"total":14,"failed":0},"append":{"passed":6,"total":6,"failed":0},"reverse":{"passed":6,"total":6,"failed":0},"member":{"passed":7,"total":7,"failed":0},"nqueens":{"passed":6,"total":6,"failed":0},"family":{"passed":10,"total":10,"failed":0},"atoms":{"passed":34,"total":34,"failed":0},"query_api":{"passed":16,"total":16,"failed":0},"iso_predicates":{"passed":29,"total":29,"failed":0},"meta_predicates":{"passed":25,"total":25,"failed":0},"list_predicates":{"passed":33,"total":33,"failed":0},"meta_call":{"passed":15,"total":15,"failed":0},"set_predicates":{"passed":15,"total":15,"failed":0},"char_predicates":{"passed":27,"total":27,"failed":0},"io_predicates":{"passed":24,"total":24,"failed":0},"assert_rules":{"passed":15,"total":15,"failed":0},"string_agg":{"passed":25,"total":25,"failed":0},"advanced":{"passed":21,"total":21,"failed":0},"compiler":{"passed":17,"total":17,"failed":0},"cross_validate":{"passed":17,"total":17,"failed":0},"integration":{"passed":20,"total":20,"failed":0},"hs_bridge":{"passed":19,"total":19,"failed":0}},
|
||||
"generated": "2026-05-06T08:29:09+00:00"
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
# Prolog scoreboard
|
||||
|
||||
**590 / 590 passing** (0 failure(s)).
|
||||
Generated 2026-05-06T08:29:09+00:00.
|
||||
|
||||
| Suite | Passed | Total | Status |
|
||||
|-------|--------|-------|--------|
|
||||
| parse | 25 | 25 | ok |
|
||||
| unify | 47 | 47 | ok |
|
||||
| clausedb | 14 | 14 | ok |
|
||||
| solve | 62 | 62 | ok |
|
||||
| operators | 19 | 19 | ok |
|
||||
| dynamic | 11 | 11 | ok |
|
||||
| findall | 11 | 11 | ok |
|
||||
| term_inspect | 14 | 14 | ok |
|
||||
| append | 6 | 6 | ok |
|
||||
| reverse | 6 | 6 | ok |
|
||||
| member | 7 | 7 | ok |
|
||||
| nqueens | 6 | 6 | ok |
|
||||
| family | 10 | 10 | ok |
|
||||
| atoms | 34 | 34 | ok |
|
||||
| query_api | 16 | 16 | ok |
|
||||
| iso_predicates | 29 | 29 | ok |
|
||||
| meta_predicates | 25 | 25 | ok |
|
||||
| list_predicates | 33 | 33 | ok |
|
||||
| meta_call | 15 | 15 | ok |
|
||||
| set_predicates | 15 | 15 | ok |
|
||||
| char_predicates | 27 | 27 | ok |
|
||||
| io_predicates | 24 | 24 | ok |
|
||||
| assert_rules | 15 | 15 | ok |
|
||||
| string_agg | 25 | 25 | ok |
|
||||
| advanced | 21 | 21 | ok |
|
||||
| compiler | 17 | 17 | ok |
|
||||
| cross_validate | 17 | 17 | ok |
|
||||
| integration | 20 | 20 | ok |
|
||||
| hs_bridge | 19 | 19 | ok |
|
||||
|
||||
Run `bash lib/prolog/conformance.sh` to refresh. Override the binary
|
||||
with `SX_SERVER=path/to/sx_server.exe bash …`.
|
||||
@@ -1,254 +0,0 @@
|
||||
;; lib/prolog/tests/advanced.sx — predsort/3, term_variables/2, arith extensions
|
||||
|
||||
(define pl-adv-test-count 0)
|
||||
(define pl-adv-test-pass 0)
|
||||
(define pl-adv-test-fail 0)
|
||||
(define pl-adv-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-adv-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-adv-test-count (+ pl-adv-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-adv-test-pass (+ pl-adv-test-pass 1))
|
||||
(begin
|
||||
(set! pl-adv-test-fail (+ pl-adv-test-fail 1))
|
||||
(append!
|
||||
pl-adv-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-adv-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-adv-db (pl-mk-db))
|
||||
;; Load a numeric comparator for predsort tests
|
||||
(pl-db-load!
|
||||
pl-adv-db
|
||||
(pl-parse
|
||||
"cmp_num(Order, X, Y) :- (X < Y -> Order = '<' ; (X > Y -> Order = '>' ; Order = '='))."))
|
||||
|
||||
;; ── Arithmetic extensions ──────────────────────────────────────────
|
||||
|
||||
(define pl-adv-arith-env-1 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is floor(3.7)" pl-adv-arith-env-1)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"floor(3.7) = 3"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-1 "X")))
|
||||
3)
|
||||
|
||||
(define pl-adv-arith-env-2 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is ceiling(3.2)" pl-adv-arith-env-2)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"ceiling(3.2) = 4"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-2 "X")))
|
||||
4)
|
||||
|
||||
(define pl-adv-arith-env-3 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is truncate(3.9)" pl-adv-arith-env-3)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"truncate(3.9) = 3"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-3 "X")))
|
||||
3)
|
||||
|
||||
(define pl-adv-arith-env-4 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is truncate(0 - 3.9)" pl-adv-arith-env-4)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"truncate(0-3.9) = -3"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-4 "X")))
|
||||
-3)
|
||||
|
||||
(define pl-adv-arith-env-5 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is round(3.5)" pl-adv-arith-env-5)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"round(3.5) = 4"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-5 "X")))
|
||||
4)
|
||||
|
||||
(define pl-adv-arith-env-6 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is sqrt(4.0)" pl-adv-arith-env-6)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"sqrt(4.0) = 2"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-6 "X")))
|
||||
2)
|
||||
|
||||
(define pl-adv-arith-env-7 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is sign(0 - 5)" pl-adv-arith-env-7)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"sign(0-5) = -1"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-7 "X")))
|
||||
-1)
|
||||
|
||||
(define pl-adv-arith-env-8 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is sign(0)" pl-adv-arith-env-8)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"sign(0) = 0"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-8 "X")))
|
||||
0)
|
||||
|
||||
(define pl-adv-arith-env-9 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is sign(3)" pl-adv-arith-env-9)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"sign(3) = 1"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-9 "X")))
|
||||
1)
|
||||
|
||||
(define pl-adv-arith-env-10 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is pow(2, 3)" pl-adv-arith-env-10)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"pow(2,3) = 8"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-10 "X")))
|
||||
8)
|
||||
|
||||
(define pl-adv-arith-env-11 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is floor(0 - 3.7)" pl-adv-arith-env-11)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"floor(0-3.7) = -4"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-11 "X")))
|
||||
-4)
|
||||
|
||||
(define pl-adv-arith-env-12 {:X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "X is ceiling(0 - 3.2)" pl-adv-arith-env-12)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"ceiling(0-3.2) = -3"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-adv-arith-env-12 "X")))
|
||||
-3)
|
||||
|
||||
;; ── term_variables/2 ──────────────────────────────────────────────
|
||||
|
||||
(define pl-adv-tv-env-1 {:Vs (pl-mk-rt-var "Vs")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "term_variables(hello, Vs)" pl-adv-tv-env-1)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"term_variables(hello,Vs) -> []"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-adv-tv-env-1 "Vs")))
|
||||
"[]")
|
||||
|
||||
(define pl-adv-tv-env-2 {:Vs (pl-mk-rt-var "Vs")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "term_variables(f(a, g(b)), Vs)" pl-adv-tv-env-2)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"term_variables(f(a,g(b)),Vs) -> []"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-adv-tv-env-2 "Vs")))
|
||||
"[]")
|
||||
|
||||
(define pl-adv-tv-env-3 {:Y (pl-mk-rt-var "Y") :Vs (pl-mk-rt-var "Vs") :X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "term_variables(f(X, Y), Vs)" pl-adv-tv-env-3)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"term_variables(f(X,Y),Vs) has 2 vars"
|
||||
(pl-list-length (pl-walk-deep (dict-get pl-adv-tv-env-3 "Vs")))
|
||||
2)
|
||||
|
||||
(define pl-adv-tv-env-4 {:Vs (pl-mk-rt-var "Vs") :X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "term_variables(X, Vs)" pl-adv-tv-env-4)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"term_variables(X,Vs) has 1 var"
|
||||
(pl-list-length (pl-walk-deep (dict-get pl-adv-tv-env-4 "Vs")))
|
||||
1)
|
||||
|
||||
(define pl-adv-tv-env-5 {:Y (pl-mk-rt-var "Y") :Vs (pl-mk-rt-var "Vs") :X (pl-mk-rt-var "X")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "term_variables(foo(X, Y, X), Vs)" pl-adv-tv-env-5)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"term_variables(foo(X,Y,X),Vs) deduplicates X -> 2 vars"
|
||||
(pl-list-length (pl-walk-deep (dict-get pl-adv-tv-env-5 "Vs")))
|
||||
2)
|
||||
|
||||
;; ── predsort/3 ────────────────────────────────────────────────────
|
||||
|
||||
(define pl-adv-ps-env-1 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "predsort(cmp_num, [], R)" pl-adv-ps-env-1)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"predsort([]) -> []"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-adv-ps-env-1 "R")))
|
||||
"[]")
|
||||
|
||||
(define pl-adv-ps-env-2 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "predsort(cmp_num, [1], R)" pl-adv-ps-env-2)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"predsort([1]) -> [1]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-adv-ps-env-2 "R")))
|
||||
".(1, [])")
|
||||
|
||||
(define pl-adv-ps-env-3 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "predsort(cmp_num, [3,1,2], R)" pl-adv-ps-env-3)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"predsort([3,1,2]) -> [1,2,3]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-adv-ps-env-3 "R")))
|
||||
".(1, .(2, .(3, [])))")
|
||||
|
||||
(define pl-adv-ps-env-4 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-adv-db
|
||||
(pl-adv-goal "predsort(cmp_num, [3,1,2,1,3], R)" pl-adv-ps-env-4)
|
||||
(pl-mk-trail))
|
||||
(pl-adv-test!
|
||||
"predsort([3,1,2,1,3]) dedup -> [1,2,3]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-adv-ps-env-4 "R")))
|
||||
".(1, .(2, .(3, [])))")
|
||||
|
||||
;; ── Runner ─────────────────────────────────────────────────────────
|
||||
|
||||
(define pl-advanced-tests-run! (fn () {:failed pl-adv-test-fail :passed pl-adv-test-pass :total pl-adv-test-count :failures pl-adv-test-failures}))
|
||||
@@ -1,215 +0,0 @@
|
||||
;; lib/prolog/tests/assert_rules.sx — assert/assertz/asserta with rule terms (head :- body)
|
||||
;; Tests that :- is in the op table (prec 1200 xfx) and pl-build-clause handles rule form.
|
||||
|
||||
(define pl-ar-test-count 0)
|
||||
(define pl-ar-test-pass 0)
|
||||
(define pl-ar-test-fail 0)
|
||||
(define pl-ar-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-ar-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-ar-test-count (+ pl-ar-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-ar-test-pass (+ pl-ar-test-pass 1))
|
||||
(begin
|
||||
(set! pl-ar-test-fail (+ pl-ar-test-fail 1))
|
||||
(append!
|
||||
pl-ar-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-ar-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
;; ── DB1: assertz a simple rule then query ──────────────────────────
|
||||
(define pl-ar-db1 (pl-mk-db))
|
||||
|
||||
(pl-solve-once!
|
||||
pl-ar-db1
|
||||
(pl-ar-goal "assertz((double(X, Y) :- Y is X * 2))" {})
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"assertz rule: double(3, Y) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-ar-db1
|
||||
(pl-ar-goal "double(3, Y)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-ar-env1 {})
|
||||
(pl-solve-once!
|
||||
pl-ar-db1
|
||||
(pl-ar-goal "double(3, Y)" pl-ar-env1)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"assertz rule: double(3, Y) binds Y to 6"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ar-env1 "Y")))
|
||||
6)
|
||||
|
||||
(define pl-ar-env1b {})
|
||||
(pl-solve-once!
|
||||
pl-ar-db1
|
||||
(pl-ar-goal "double(10, Y)" pl-ar-env1b)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"assertz rule: double(10, Y) yields 20"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ar-env1b "Y")))
|
||||
20)
|
||||
|
||||
;; ── DB2: assert a rule with multiple facts, count solutions ─────────
|
||||
(define pl-ar-db2 (pl-mk-db))
|
||||
|
||||
(pl-solve-once!
|
||||
pl-ar-db2
|
||||
(pl-ar-goal "assert(fact(a))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-ar-db2
|
||||
(pl-ar-goal "assert(fact(b))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-ar-db2
|
||||
(pl-ar-goal "assertz((copy(X) :- fact(X)))" {})
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"rule copy/1 using fact/1: 2 solutions"
|
||||
(pl-solve-count! pl-ar-db2 (pl-ar-goal "copy(X)" {}) (pl-mk-trail))
|
||||
2)
|
||||
|
||||
(define pl-ar-env2a {})
|
||||
(pl-solve-once! pl-ar-db2 (pl-ar-goal "copy(X)" pl-ar-env2a) (pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"rule copy/1: first solution is a"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-ar-env2a "X")))
|
||||
"a")
|
||||
|
||||
;; ── DB3: asserta rule is tried before existing clauses ─────────────
|
||||
(define pl-ar-db3 (pl-mk-db))
|
||||
|
||||
(pl-solve-once!
|
||||
pl-ar-db3
|
||||
(pl-ar-goal "assert(ord(a))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-ar-db3
|
||||
(pl-ar-goal "asserta((ord(b) :- true))" {})
|
||||
(pl-mk-trail))
|
||||
|
||||
(define pl-ar-env3 {})
|
||||
(pl-solve-once! pl-ar-db3 (pl-ar-goal "ord(X)" pl-ar-env3) (pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"asserta rule ord(b) is tried before ord(a)"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-ar-env3 "X")))
|
||||
"b")
|
||||
|
||||
(pl-ar-test!
|
||||
"asserta: total solutions for ord/1 is 2"
|
||||
(pl-solve-count! pl-ar-db3 (pl-ar-goal "ord(X)" {}) (pl-mk-trail))
|
||||
2)
|
||||
|
||||
;; ── DB4: rule with conjunction in body ─────────────────────────────
|
||||
(define pl-ar-db4 (pl-mk-db))
|
||||
|
||||
(pl-solve-once!
|
||||
pl-ar-db4
|
||||
(pl-ar-goal "assert(num(1))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-ar-db4
|
||||
(pl-ar-goal "assert(num(2))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-ar-db4
|
||||
(pl-ar-goal "assertz((big(X) :- num(X), X > 1))" {})
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"conjunction in rule body: big(1) fails"
|
||||
(pl-solve-once! pl-ar-db4 (pl-ar-goal "big(1)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-ar-test!
|
||||
"conjunction in rule body: big(2) succeeds"
|
||||
(pl-solve-once! pl-ar-db4 (pl-ar-goal "big(2)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── DB5: recursive rule ─────────────────────────────────────────────
|
||||
(define pl-ar-db5 (pl-mk-db))
|
||||
|
||||
(pl-solve-once!
|
||||
pl-ar-db5
|
||||
(pl-ar-goal "assert((nat(0) :- true))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-ar-db5
|
||||
(pl-ar-goal "assertz((nat(s(X)) :- nat(X)))" {})
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"recursive rule: nat(0) succeeds"
|
||||
(pl-solve-once! pl-ar-db5 (pl-ar-goal "nat(0)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-ar-test!
|
||||
"recursive rule: nat(s(0)) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-ar-db5
|
||||
(pl-ar-goal "nat(s(0))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-ar-test!
|
||||
"recursive rule: nat(s(s(0))) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-ar-db5
|
||||
(pl-ar-goal "nat(s(s(0)))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-ar-test!
|
||||
"recursive rule: nat(bad) fails"
|
||||
(pl-solve-once! pl-ar-db5 (pl-ar-goal "nat(bad)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── DB6: rule with true body (explicit) ────────────────────────────
|
||||
(define pl-ar-db6 (pl-mk-db))
|
||||
|
||||
(pl-solve-once!
|
||||
pl-ar-db6
|
||||
(pl-ar-goal "assertz((always(X) :- true))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-ar-db6
|
||||
(pl-ar-goal "assert(always(extra))" {})
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-ar-test!
|
||||
"rule body=true: always(foo) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-ar-db6
|
||||
(pl-ar-goal "always(foo)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-ar-test!
|
||||
"rule body=true: always/1 has 2 clauses (1 rule + 1 fact)"
|
||||
(pl-solve-count!
|
||||
pl-ar-db6
|
||||
(pl-ar-goal "always(X)" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
;; ── Runner ──────────────────────────────────────────────────────────
|
||||
(define pl-assert-rules-tests-run! (fn () {:failed pl-ar-test-fail :passed pl-ar-test-pass :total pl-ar-test-count :failures pl-ar-test-failures}))
|
||||
@@ -1,305 +0,0 @@
|
||||
;; lib/prolog/tests/atoms.sx — type predicates + string/atom built-ins
|
||||
|
||||
(define pl-at-test-count 0)
|
||||
(define pl-at-test-pass 0)
|
||||
(define pl-at-test-fail 0)
|
||||
(define pl-at-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-at-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-at-test-count (+ pl-at-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-at-test-pass (+ pl-at-test-pass 1))
|
||||
(begin
|
||||
(set! pl-at-test-fail (+ pl-at-test-fail 1))
|
||||
(append!
|
||||
pl-at-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-at-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-at-db (pl-mk-db))
|
||||
|
||||
;; ── var/1 + nonvar/1 ──
|
||||
|
||||
(pl-at-test!
|
||||
"var(X) for unbound var"
|
||||
(pl-solve-once! pl-at-db (pl-at-goal "var(X)" {}) (pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"var(foo) fails"
|
||||
(pl-solve-once! pl-at-db (pl-at-goal "var(foo)" {}) (pl-mk-trail))
|
||||
false)
|
||||
(pl-at-test!
|
||||
"nonvar(foo) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "nonvar(foo)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"nonvar(X) for unbound var fails"
|
||||
(pl-solve-once! pl-at-db (pl-at-goal "nonvar(X)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── atom/1 ──
|
||||
|
||||
(pl-at-test!
|
||||
"atom(foo) succeeds"
|
||||
(pl-solve-once! pl-at-db (pl-at-goal "atom(foo)" {}) (pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"atom([]) succeeds"
|
||||
(pl-solve-once! pl-at-db (pl-at-goal "atom([])" {}) (pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"atom(42) fails"
|
||||
(pl-solve-once! pl-at-db (pl-at-goal "atom(42)" {}) (pl-mk-trail))
|
||||
false)
|
||||
(pl-at-test!
|
||||
"atom(f(x)) fails"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom(f(x))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── number/1 + integer/1 ──
|
||||
|
||||
(pl-at-test!
|
||||
"number(42) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "number(42)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"number(foo) fails"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "number(foo)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
(pl-at-test!
|
||||
"integer(7) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "integer(7)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── compound/1 + callable/1 + atomic/1 ──
|
||||
|
||||
(pl-at-test!
|
||||
"compound(f(x)) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "compound(f(x))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"compound(foo) fails"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "compound(foo)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
(pl-at-test!
|
||||
"callable(foo) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "callable(foo)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"callable(f(x)) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "callable(f(x))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"callable(42) fails"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "callable(42)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
(pl-at-test!
|
||||
"atomic(foo) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atomic(foo)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"atomic(42) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atomic(42)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"atomic(f(x)) fails"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atomic(f(x))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── is_list/1 ──
|
||||
|
||||
(pl-at-test!
|
||||
"is_list([]) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "is_list([])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"is_list([1,2,3]) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "is_list([1,2,3])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"is_list(foo) fails"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "is_list(foo)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── atom_length/2 ──
|
||||
|
||||
(define pl-at-env-al {})
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_length(hello, N)" pl-at-env-al)
|
||||
(pl-mk-trail))
|
||||
(pl-at-test!
|
||||
"atom_length(hello, N) -> N=5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-at-env-al "N")))
|
||||
5)
|
||||
(pl-at-test!
|
||||
"atom_length empty atom"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_length('', 0)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── atom_concat/3 ──
|
||||
|
||||
(define pl-at-env-ac {})
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_concat(foo, bar, X)" pl-at-env-ac)
|
||||
(pl-mk-trail))
|
||||
(pl-at-test!
|
||||
"atom_concat(foo, bar, X) -> X=foobar"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-at-env-ac "X")))
|
||||
"foobar")
|
||||
|
||||
(pl-at-test!
|
||||
"atom_concat(foo, bar, foobar) check"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_concat(foo, bar, foobar)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
(pl-at-test!
|
||||
"atom_concat(foo, bar, foobaz) fails"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_concat(foo, bar, foobaz)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-at-env-ac2 {})
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_concat(foo, Y, foobar)" pl-at-env-ac2)
|
||||
(pl-mk-trail))
|
||||
(pl-at-test!
|
||||
"atom_concat(foo, Y, foobar) -> Y=bar"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-at-env-ac2 "Y")))
|
||||
"bar")
|
||||
|
||||
;; ── atom_chars/2 ──
|
||||
|
||||
(define pl-at-env-ach {})
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_chars(cat, Cs)" pl-at-env-ach)
|
||||
(pl-mk-trail))
|
||||
(pl-at-test!
|
||||
"atom_chars(cat, Cs) -> Cs=[c,a,t]"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_chars(cat, [c,a,t])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-at-env-ach2 {})
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "atom_chars(A, [h,i])" pl-at-env-ach2)
|
||||
(pl-mk-trail))
|
||||
(pl-at-test!
|
||||
"atom_chars(A, [h,i]) -> A=hi"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-at-env-ach2 "A")))
|
||||
"hi")
|
||||
|
||||
;; ── char_code/2 ──
|
||||
|
||||
(define pl-at-env-cc {})
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "char_code(a, N)" pl-at-env-cc)
|
||||
(pl-mk-trail))
|
||||
(pl-at-test!
|
||||
"char_code(a, N) -> N=97"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-at-env-cc "N")))
|
||||
97)
|
||||
|
||||
(define pl-at-env-cc2 {})
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "char_code(C, 65)" pl-at-env-cc2)
|
||||
(pl-mk-trail))
|
||||
(pl-at-test!
|
||||
"char_code(C, 65) -> C='A'"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-at-env-cc2 "C")))
|
||||
"A")
|
||||
|
||||
;; ── number_codes/2 ──
|
||||
|
||||
(pl-at-test!
|
||||
"number_codes(42, [52,50])"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "number_codes(42, [52,50])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── number_chars/2 ──
|
||||
|
||||
(pl-at-test!
|
||||
"number_chars(42, ['4','2'])"
|
||||
(pl-solve-once!
|
||||
pl-at-db
|
||||
(pl-at-goal "number_chars(42, ['4','2'])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-atom-tests-run! (fn () {:failed pl-at-test-fail :passed pl-at-test-pass :total pl-at-test-count :failures pl-at-test-failures}))
|
||||
@@ -1,290 +0,0 @@
|
||||
;; lib/prolog/tests/char_predicates.sx — char_type/2, upcase_atom/2, downcase_atom/2,
|
||||
;; string_upper/2, string_lower/2
|
||||
|
||||
(define pl-cp-test-count 0)
|
||||
(define pl-cp-test-pass 0)
|
||||
(define pl-cp-test-fail 0)
|
||||
(define pl-cp-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-cp-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-cp-test-count (+ pl-cp-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-cp-test-pass (+ pl-cp-test-pass 1))
|
||||
(begin
|
||||
(set! pl-cp-test-fail (+ pl-cp-test-fail 1))
|
||||
(append!
|
||||
pl-cp-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-cp-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-cp-db (pl-mk-db))
|
||||
|
||||
;; ─── char_type/2 — alpha ──────────────────────────────────────────
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type(a, alpha) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(a, alpha)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type('1', alpha) fails"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type('1', alpha)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type('A', alpha) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type('A', alpha)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ─── char_type/2 — alnum ─────────────────────────────────────────
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type('5', alnum) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type('5', alnum)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type(a, alnum) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(a, alnum)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type(' ', alnum) fails"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(' ', alnum)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ─── char_type/2 — digit ─────────────────────────────────────────
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type('5', digit) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type('5', digit)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type(a, digit) fails"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(a, digit)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ─── char_type/2 — digit(Weight) ─────────────────────────────────
|
||||
|
||||
(define pl-cp-env-dw {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type('5', digit(N))" pl-cp-env-dw)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"char_type('5', digit(N)) -> N=5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-cp-env-dw "N")))
|
||||
5)
|
||||
|
||||
(define pl-cp-env-dw0 {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type('0', digit(N))" pl-cp-env-dw0)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"char_type('0', digit(N)) -> N=0"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-cp-env-dw0 "N")))
|
||||
0)
|
||||
|
||||
;; ─── char_type/2 — space/white ───────────────────────────────────
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type(' ', space) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(' ', space)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type(a, space) fails"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(a, space)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ─── char_type/2 — upper(Lower) ──────────────────────────────────
|
||||
|
||||
(define pl-cp-env-ul {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type('A', upper(L))" pl-cp-env-ul)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"char_type('A', upper(L)) -> L=a"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-ul "L")))
|
||||
"a")
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type(a, upper(L)) fails — not uppercase"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(a, upper(_))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ─── char_type/2 — lower(Upper) ──────────────────────────────────
|
||||
|
||||
(define pl-cp-env-lu {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(a, lower(U))" pl-cp-env-lu)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"char_type(a, lower(U)) -> U='A'"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-lu "U")))
|
||||
"A")
|
||||
|
||||
;; ─── char_type/2 — ascii(Code) ───────────────────────────────────
|
||||
|
||||
(define pl-cp-env-as {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(a, ascii(C))" pl-cp-env-as)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"char_type(a, ascii(C)) -> C=97"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-cp-env-as "C")))
|
||||
97)
|
||||
|
||||
;; ─── char_type/2 — punct ─────────────────────────────────────────
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type('.', punct) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type('.', punct)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"char_type(a, punct) fails"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "char_type(a, punct)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ─── upcase_atom/2 ───────────────────────────────────────────────
|
||||
|
||||
(define pl-cp-env-ua {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "upcase_atom(hello, X)" pl-cp-env-ua)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"upcase_atom(hello, X) -> X='HELLO'"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-ua "X")))
|
||||
"HELLO")
|
||||
|
||||
(pl-cp-test!
|
||||
"upcase_atom(hello, 'HELLO') succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "upcase_atom(hello, 'HELLO')" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"upcase_atom('Hello World', 'HELLO WORLD') succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "upcase_atom('Hello World', 'HELLO WORLD')" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"upcase_atom('', '') succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "upcase_atom('', '')" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ─── downcase_atom/2 ─────────────────────────────────────────────
|
||||
|
||||
(define pl-cp-env-da {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "downcase_atom('HELLO', X)" pl-cp-env-da)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"downcase_atom('HELLO', X) -> X=hello"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-da "X")))
|
||||
"hello")
|
||||
|
||||
(pl-cp-test!
|
||||
"downcase_atom('HELLO', hello) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "downcase_atom('HELLO', hello)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-cp-test!
|
||||
"downcase_atom(hello, hello) succeeds — already lowercase"
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "downcase_atom(hello, hello)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ─── string_upper/2 + string_lower/2 (aliases) ───────────────────
|
||||
|
||||
(define pl-cp-env-su {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "string_upper(hello, X)" pl-cp-env-su)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"string_upper(hello, X) -> X='HELLO'"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-su "X")))
|
||||
"HELLO")
|
||||
|
||||
(define pl-cp-env-sl {})
|
||||
(pl-solve-once!
|
||||
pl-cp-db
|
||||
(pl-cp-goal "string_lower('WORLD', X)" pl-cp-env-sl)
|
||||
(pl-mk-trail))
|
||||
(pl-cp-test!
|
||||
"string_lower('WORLD', X) -> X=world"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-cp-env-sl "X")))
|
||||
"world")
|
||||
|
||||
(define pl-char-predicates-tests-run! (fn () {:failed pl-cp-test-fail :passed pl-cp-test-pass :total pl-cp-test-count :failures pl-cp-test-failures}))
|
||||
@@ -1,99 +0,0 @@
|
||||
;; lib/prolog/tests/clausedb.sx — Clause DB unit tests
|
||||
|
||||
(define pl-db-test-count 0)
|
||||
(define pl-db-test-pass 0)
|
||||
(define pl-db-test-fail 0)
|
||||
(define pl-db-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-db-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-db-test-count (+ pl-db-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-db-test-pass (+ pl-db-test-pass 1))
|
||||
(begin
|
||||
(set! pl-db-test-fail (+ pl-db-test-fail 1))
|
||||
(append!
|
||||
pl-db-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(pl-db-test!
|
||||
"head-key atom arity 0"
|
||||
(pl-head-key (nth (first (pl-parse "foo.")) 1))
|
||||
"foo/0")
|
||||
|
||||
(pl-db-test!
|
||||
"head-key compound arity 2"
|
||||
(pl-head-key (nth (first (pl-parse "bar(a, b).")) 1))
|
||||
"bar/2")
|
||||
|
||||
(pl-db-test!
|
||||
"clause-key of :- clause"
|
||||
(pl-clause-key (first (pl-parse "likes(mary, X) :- friendly(X).")))
|
||||
"likes/2")
|
||||
|
||||
(pl-db-test!
|
||||
"empty db lookup returns empty list"
|
||||
(len (pl-db-lookup (pl-mk-db) "parent/2"))
|
||||
0)
|
||||
|
||||
(define pl-db-t1 (pl-mk-db))
|
||||
(pl-db-load! pl-db-t1 (pl-parse "foo(a). foo(b). foo(c)."))
|
||||
|
||||
(pl-db-test!
|
||||
"three facts same functor"
|
||||
(len (pl-db-lookup pl-db-t1 "foo/1"))
|
||||
3)
|
||||
(pl-db-test!
|
||||
"mismatching key returns empty"
|
||||
(len (pl-db-lookup pl-db-t1 "foo/2"))
|
||||
0)
|
||||
|
||||
(pl-db-test!
|
||||
"first clause has arg a"
|
||||
(pl-atom-name
|
||||
(first (pl-args (nth (first (pl-db-lookup pl-db-t1 "foo/1")) 1))))
|
||||
"a")
|
||||
|
||||
(pl-db-test!
|
||||
"third clause has arg c"
|
||||
(pl-atom-name
|
||||
(first (pl-args (nth (nth (pl-db-lookup pl-db-t1 "foo/1") 2) 1))))
|
||||
"c")
|
||||
|
||||
(define pl-db-t2 (pl-mk-db))
|
||||
(pl-db-load! pl-db-t2 (pl-parse "foo. bar. foo. parent(a, b). parent(c, d)."))
|
||||
|
||||
(pl-db-test!
|
||||
"atom heads keyed as foo/0"
|
||||
(len (pl-db-lookup pl-db-t2 "foo/0"))
|
||||
2)
|
||||
(pl-db-test!
|
||||
"atom heads keyed as bar/0"
|
||||
(len (pl-db-lookup pl-db-t2 "bar/0"))
|
||||
1)
|
||||
(pl-db-test!
|
||||
"compound heads keyed as parent/2"
|
||||
(len (pl-db-lookup pl-db-t2 "parent/2"))
|
||||
2)
|
||||
|
||||
(pl-db-test!
|
||||
"lookup-goal extracts functor/arity"
|
||||
(len
|
||||
(pl-db-lookup-goal pl-db-t2 (nth (first (pl-parse "parent(X, Y).")) 1)))
|
||||
2)
|
||||
|
||||
(pl-db-test!
|
||||
"lookup-goal on atom goal"
|
||||
(len (pl-db-lookup-goal pl-db-t2 (nth (first (pl-parse "foo.")) 1)))
|
||||
2)
|
||||
|
||||
(pl-db-test!
|
||||
"stored clause is clause form"
|
||||
(first (first (pl-db-lookup pl-db-t2 "parent/2")))
|
||||
"clause")
|
||||
|
||||
(define pl-clausedb-tests-run! (fn () {:failed pl-db-test-fail :passed pl-db-test-pass :total pl-db-test-count :failures pl-db-test-failures}))
|
||||
@@ -1,185 +0,0 @@
|
||||
;; lib/prolog/tests/compiler.sx — compiled clause dispatch tests
|
||||
|
||||
(define pl-cmp-test-count 0)
|
||||
(define pl-cmp-test-pass 0)
|
||||
(define pl-cmp-test-fail 0)
|
||||
(define pl-cmp-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-cmp-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(set! pl-cmp-test-count (+ pl-cmp-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-cmp-test-pass (+ pl-cmp-test-pass 1))
|
||||
(begin
|
||||
(set! pl-cmp-test-fail (+ pl-cmp-test-fail 1))
|
||||
(append! pl-cmp-test-failures name)))))
|
||||
|
||||
;; Load src, compile, return DB.
|
||||
(define
|
||||
pl-cmp-mk
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((db (pl-mk-db)))
|
||||
(pl-db-load! db (pl-parse src))
|
||||
(pl-compile-db! db)
|
||||
db)))
|
||||
|
||||
;; Run goal string against compiled DB; return bool (instantiates vars).
|
||||
(define
|
||||
pl-cmp-once
|
||||
(fn
|
||||
(db src)
|
||||
(pl-solve-once!
|
||||
db
|
||||
(pl-instantiate (pl-parse-goal src) {})
|
||||
(pl-mk-trail))))
|
||||
|
||||
;; Count solutions for goal string against compiled DB.
|
||||
(define
|
||||
pl-cmp-count
|
||||
(fn
|
||||
(db src)
|
||||
(pl-solve-count!
|
||||
db
|
||||
(pl-instantiate (pl-parse-goal src) {})
|
||||
(pl-mk-trail))))
|
||||
|
||||
;; ── 1. Simple facts ──────────────────────────────────────────────
|
||||
|
||||
(define pl-cmp-db1 (pl-cmp-mk "color(red). color(green). color(blue)."))
|
||||
|
||||
(pl-cmp-test! "compiled fact hit" (pl-cmp-once pl-cmp-db1 "color(red)") true)
|
||||
(pl-cmp-test!
|
||||
"compiled fact miss"
|
||||
(pl-cmp-once pl-cmp-db1 "color(yellow)")
|
||||
false)
|
||||
(pl-cmp-test! "compiled fact count" (pl-cmp-count pl-cmp-db1 "color(X)") 3)
|
||||
|
||||
;; ── 2. Recursive rule: append ────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-cmp-db2
|
||||
(pl-cmp-mk "append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R)."))
|
||||
|
||||
(pl-cmp-test!
|
||||
"compiled append build"
|
||||
(pl-cmp-once pl-cmp-db2 "append([1,2],[3],[1,2,3])")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"compiled append fail"
|
||||
(pl-cmp-once pl-cmp-db2 "append([1,2],[3],[1,2])")
|
||||
false)
|
||||
(pl-cmp-test!
|
||||
"compiled append split count"
|
||||
(pl-cmp-count pl-cmp-db2 "append(X, Y, [a,b])")
|
||||
3)
|
||||
|
||||
;; ── 3. Cut ───────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-cmp-db3
|
||||
(pl-cmp-mk "first(X, [X|_]) :- !. first(X, [_|T]) :- first(X, T)."))
|
||||
|
||||
(pl-cmp-test!
|
||||
"compiled cut: only one solution"
|
||||
(pl-cmp-count pl-cmp-db3 "first(X, [a,b,c])")
|
||||
1)
|
||||
|
||||
(let
|
||||
((db pl-cmp-db3) (trail (pl-mk-trail)) (env {}))
|
||||
(let
|
||||
((x (pl-mk-rt-var "X")))
|
||||
(dict-set! env "X" x)
|
||||
(pl-solve-once!
|
||||
db
|
||||
(pl-instantiate (pl-parse-goal "first(X, [a,b,c])") env)
|
||||
trail)
|
||||
(pl-cmp-test!
|
||||
"compiled cut: correct binding"
|
||||
(pl-atom-name (pl-walk x))
|
||||
"a")))
|
||||
|
||||
;; ── 4. member ────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-cmp-db4
|
||||
(pl-cmp-mk "member(X, [X|_]). member(X, [_|T]) :- member(X, T)."))
|
||||
|
||||
(pl-cmp-test!
|
||||
"compiled member hit"
|
||||
(pl-cmp-once pl-cmp-db4 "member(b, [a,b,c])")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"compiled member miss"
|
||||
(pl-cmp-once pl-cmp-db4 "member(d, [a,b,c])")
|
||||
false)
|
||||
(pl-cmp-test!
|
||||
"compiled member count"
|
||||
(pl-cmp-count pl-cmp-db4 "member(X, [a,b,c])")
|
||||
3)
|
||||
|
||||
;; ── 5. Arithmetic in body ────────────────────────────────────────
|
||||
|
||||
(define pl-cmp-db5 (pl-cmp-mk "double(X, Y) :- Y is X * 2."))
|
||||
|
||||
(let
|
||||
((db pl-cmp-db5) (trail (pl-mk-trail)) (env {}))
|
||||
(let
|
||||
((y (pl-mk-rt-var "Y")))
|
||||
(dict-set! env "Y" y)
|
||||
(pl-solve-once!
|
||||
db
|
||||
(pl-instantiate (pl-parse-goal "double(5, Y)") env)
|
||||
trail)
|
||||
(pl-cmp-test! "compiled arithmetic in body" (pl-num-val (pl-walk y)) 10)))
|
||||
|
||||
;; ── 6. Transitive ancestor ───────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-cmp-db6
|
||||
(pl-cmp-mk
|
||||
(str
|
||||
"parent(a,b). parent(b,c). parent(c,d)."
|
||||
"ancestor(X,Y) :- parent(X,Y)."
|
||||
"ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).")))
|
||||
|
||||
(pl-cmp-test!
|
||||
"compiled ancestor direct"
|
||||
(pl-cmp-once pl-cmp-db6 "ancestor(a,b)")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"compiled ancestor 3-step"
|
||||
(pl-cmp-once pl-cmp-db6 "ancestor(a,d)")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"compiled ancestor fail"
|
||||
(pl-cmp-once pl-cmp-db6 "ancestor(d,a)")
|
||||
false)
|
||||
|
||||
;; ── 7. Fallback: uncompiled predicate calls compiled sub-predicate
|
||||
|
||||
(define
|
||||
pl-cmp-db7
|
||||
(let
|
||||
((db (pl-mk-db)))
|
||||
(pl-db-load! db (pl-parse "q(1). q(2)."))
|
||||
(pl-compile-db! db)
|
||||
(pl-db-load! db (pl-parse "r(X) :- q(X)."))
|
||||
db))
|
||||
|
||||
(pl-cmp-test!
|
||||
"uncompiled predicate resolves"
|
||||
(pl-cmp-once pl-cmp-db7 "r(1)")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"uncompiled calls compiled sub-pred count"
|
||||
(pl-cmp-count pl-cmp-db7 "r(X)")
|
||||
2)
|
||||
|
||||
;; ── Runner ───────────────────────────────────────────────────────
|
||||
|
||||
(define pl-compiler-tests-run! (fn () {:failed pl-cmp-test-fail :passed pl-cmp-test-pass :total pl-cmp-test-count :failures pl-cmp-test-failures}))
|
||||
@@ -1,86 +0,0 @@
|
||||
;; lib/prolog/tests/cross_validate.sx
|
||||
;; Verifies that the compiled solver produces the same solution counts as the
|
||||
;; interpreter for each classic program + built-in exercise.
|
||||
;; Interpreter is the reference: if they disagree, the compiler is wrong.
|
||||
|
||||
(define pl-xv-test-count 0)
|
||||
(define pl-xv-test-pass 0)
|
||||
(define pl-xv-test-fail 0)
|
||||
(define pl-xv-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-xv-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(set! pl-xv-test-count (+ pl-xv-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-xv-test-pass (+ pl-xv-test-pass 1))
|
||||
(begin
|
||||
(set! pl-xv-test-fail (+ pl-xv-test-fail 1))
|
||||
(append! pl-xv-test-failures name)))))
|
||||
|
||||
;; Shorthand: assert compiled result matches interpreter.
|
||||
(define
|
||||
pl-xv-match!
|
||||
(fn
|
||||
(name src goal)
|
||||
(pl-xv-test! name (pl-compiled-matches-interp? src goal) true)))
|
||||
|
||||
;; ── 1. append/3 ─────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-xv-append
|
||||
"append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R).")
|
||||
|
||||
(pl-xv-match! "append build 2+2" pl-xv-append "append([1,2],[3,4],X)")
|
||||
(pl-xv-match! "append split [a,b,c]" pl-xv-append "append(X, Y, [a,b,c])")
|
||||
(pl-xv-match! "append member-mode" pl-xv-append "append(_, [3], [1,2,3])")
|
||||
|
||||
;; ── 2. member/2 ─────────────────────────────────────────────────
|
||||
|
||||
(define pl-xv-member "member(X, [X|_]). member(X, [_|T]) :- member(X, T).")
|
||||
|
||||
(pl-xv-match! "member check hit" pl-xv-member "member(b, [a,b,c])")
|
||||
(pl-xv-match! "member count" pl-xv-member "member(X, [a,b,c])")
|
||||
(pl-xv-match! "member empty" pl-xv-member "member(X, [])")
|
||||
|
||||
;; ── 3. facts + transitive rules ─────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-xv-ancestor
|
||||
(str
|
||||
"parent(a,b). parent(b,c). parent(c,d). parent(a,c)."
|
||||
"ancestor(X,Y) :- parent(X,Y)."
|
||||
"ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y)."))
|
||||
|
||||
(pl-xv-match! "ancestor direct" pl-xv-ancestor "ancestor(a,b)")
|
||||
(pl-xv-match! "ancestor transitive" pl-xv-ancestor "ancestor(a,d)")
|
||||
(pl-xv-match! "ancestor all from a" pl-xv-ancestor "ancestor(a,Y)")
|
||||
|
||||
;; ── 4. cut semantics ────────────────────────────────────────────
|
||||
|
||||
(define pl-xv-cut "first(X,[X|_]) :- !. first(X,[_|T]) :- first(X,T).")
|
||||
|
||||
(pl-xv-match! "cut one solution" pl-xv-cut "first(X,[a,b,c])")
|
||||
(pl-xv-match! "cut empty list" pl-xv-cut "first(X,[])")
|
||||
|
||||
;; ── 5. arithmetic ───────────────────────────────────────────────
|
||||
|
||||
(define pl-xv-arith "sq(X,Y) :- Y is X * X. even(X) :- 0 is X mod 2.")
|
||||
|
||||
(pl-xv-match! "sq(3,Y) count" pl-xv-arith "sq(3,Y)")
|
||||
(pl-xv-match! "sq(3,9) check" pl-xv-arith "sq(3,9)")
|
||||
(pl-xv-match! "even(4) check" pl-xv-arith "even(4)")
|
||||
(pl-xv-match! "even(3) check" pl-xv-arith "even(3)")
|
||||
|
||||
;; ── 6. if-then-else ─────────────────────────────────────────────
|
||||
|
||||
(define pl-xv-ite "classify(X, pos) :- X > 0, !. classify(_, nonpos).")
|
||||
|
||||
(pl-xv-match! "classify positive" pl-xv-ite "classify(5, C)")
|
||||
(pl-xv-match! "classify zero" pl-xv-ite "classify(0, C)")
|
||||
|
||||
;; ── Runner ───────────────────────────────────────────────────────
|
||||
|
||||
(define pl-cross-validate-tests-run! (fn () {:failed pl-xv-test-fail :passed pl-xv-test-pass :total pl-xv-test-count :failures pl-xv-test-failures}))
|
||||
@@ -1,158 +0,0 @@
|
||||
;; lib/prolog/tests/dynamic.sx — assert/asserta/assertz/retract.
|
||||
|
||||
(define pl-dy-test-count 0)
|
||||
(define pl-dy-test-pass 0)
|
||||
(define pl-dy-test-fail 0)
|
||||
(define pl-dy-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-dy-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-dy-test-count (+ pl-dy-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-dy-test-pass (+ pl-dy-test-pass 1))
|
||||
(begin
|
||||
(set! pl-dy-test-fail (+ pl-dy-test-fail 1))
|
||||
(append!
|
||||
pl-dy-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-dy-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
;; assertz then query
|
||||
(define pl-dy-db1 (pl-mk-db))
|
||||
(pl-solve-once!
|
||||
pl-dy-db1
|
||||
(pl-dy-goal "assertz(foo(1))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"assertz(foo(1)) + foo(1)"
|
||||
(pl-solve-once! pl-dy-db1 (pl-dy-goal "foo(1)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-dy-test!
|
||||
"after one assertz, foo/1 has 1 clause"
|
||||
(pl-solve-count! pl-dy-db1 (pl-dy-goal "foo(X)" {}) (pl-mk-trail))
|
||||
1)
|
||||
|
||||
;; assertz appends — order preserved
|
||||
(define pl-dy-db2 (pl-mk-db))
|
||||
(pl-solve-once!
|
||||
pl-dy-db2
|
||||
(pl-dy-goal "assertz(p(1))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-dy-db2
|
||||
(pl-dy-goal "assertz(p(2))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"assertz twice — count 2"
|
||||
(pl-solve-count! pl-dy-db2 (pl-dy-goal "p(X)" {}) (pl-mk-trail))
|
||||
2)
|
||||
|
||||
(define pl-dy-env-a {})
|
||||
(pl-solve-once! pl-dy-db2 (pl-dy-goal "p(X)" pl-dy-env-a) (pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"assertz: first solution is the first asserted (1)"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-dy-env-a "X")))
|
||||
1)
|
||||
|
||||
;; asserta prepends
|
||||
(define pl-dy-db3 (pl-mk-db))
|
||||
(pl-solve-once!
|
||||
pl-dy-db3
|
||||
(pl-dy-goal "assertz(p(1))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-dy-db3
|
||||
(pl-dy-goal "asserta(p(99))" {})
|
||||
(pl-mk-trail))
|
||||
(define pl-dy-env-b {})
|
||||
(pl-solve-once! pl-dy-db3 (pl-dy-goal "p(X)" pl-dy-env-b) (pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"asserta: prepended clause is first solution"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-dy-env-b "X")))
|
||||
99)
|
||||
|
||||
;; assert/1 = assertz/1
|
||||
(define pl-dy-db4 (pl-mk-db))
|
||||
(pl-solve-once!
|
||||
pl-dy-db4
|
||||
(pl-dy-goal "assert(g(7))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"assert/1 alias"
|
||||
(pl-solve-once! pl-dy-db4 (pl-dy-goal "g(7)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; retract removes a fact
|
||||
(define pl-dy-db5 (pl-mk-db))
|
||||
(pl-solve-once!
|
||||
pl-dy-db5
|
||||
(pl-dy-goal "assertz(q(1))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-dy-db5
|
||||
(pl-dy-goal "assertz(q(2))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-dy-db5
|
||||
(pl-dy-goal "assertz(q(3))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"before retract: 3 clauses"
|
||||
(pl-solve-count! pl-dy-db5 (pl-dy-goal "q(X)" {}) (pl-mk-trail))
|
||||
3)
|
||||
(pl-solve-once!
|
||||
pl-dy-db5
|
||||
(pl-dy-goal "retract(q(2))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"after retract(q(2)): 2 clauses left"
|
||||
(pl-solve-count! pl-dy-db5 (pl-dy-goal "q(X)" {}) (pl-mk-trail))
|
||||
2)
|
||||
|
||||
(define pl-dy-env-c {})
|
||||
(pl-solve-once! pl-dy-db5 (pl-dy-goal "q(X)" pl-dy-env-c) (pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"after retract(q(2)): first remaining is 1"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-dy-env-c "X")))
|
||||
1)
|
||||
|
||||
;; retract of non-existent
|
||||
(pl-dy-test!
|
||||
"retract(missing(0)) on empty db fails"
|
||||
(pl-solve-once!
|
||||
(pl-mk-db)
|
||||
(pl-dy-goal "retract(missing(0))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; retract with unbound var matches first
|
||||
(define pl-dy-db6 (pl-mk-db))
|
||||
(pl-solve-once!
|
||||
pl-dy-db6
|
||||
(pl-dy-goal "assertz(r(11))" {})
|
||||
(pl-mk-trail))
|
||||
(pl-solve-once!
|
||||
pl-dy-db6
|
||||
(pl-dy-goal "assertz(r(22))" {})
|
||||
(pl-mk-trail))
|
||||
(define pl-dy-env-d {})
|
||||
(pl-solve-once!
|
||||
pl-dy-db6
|
||||
(pl-dy-goal "retract(r(X))" pl-dy-env-d)
|
||||
(pl-mk-trail))
|
||||
(pl-dy-test!
|
||||
"retract(r(X)) binds X to first match"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-dy-env-d "X")))
|
||||
11)
|
||||
|
||||
(define pl-dynamic-tests-run! (fn () {:failed pl-dy-test-fail :passed pl-dy-test-pass :total pl-dy-test-count :failures pl-dy-test-failures}))
|
||||
@@ -1,167 +0,0 @@
|
||||
;; lib/prolog/tests/findall.sx — findall/3, bagof/3, setof/3.
|
||||
|
||||
(define pl-fb-test-count 0)
|
||||
(define pl-fb-test-pass 0)
|
||||
(define pl-fb-test-fail 0)
|
||||
(define pl-fb-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-fb-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-fb-test-count (+ pl-fb-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-fb-test-pass (+ pl-fb-test-pass 1))
|
||||
(begin
|
||||
(set! pl-fb-test-fail (+ pl-fb-test-fail 1))
|
||||
(append!
|
||||
pl-fb-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-fb-term-to-sx
|
||||
(fn
|
||||
(t)
|
||||
(cond
|
||||
((pl-num? t) (pl-num-val t))
|
||||
((pl-atom? t) (pl-atom-name t))
|
||||
(true (list :complex)))))
|
||||
|
||||
(define
|
||||
pl-fb-list-walked
|
||||
(fn
|
||||
(w)
|
||||
(cond
|
||||
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
|
||||
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
|
||||
(cons
|
||||
(pl-fb-term-to-sx (first (pl-args w)))
|
||||
(pl-fb-list-walked (nth (pl-args w) 1))))
|
||||
(true (list :not-list)))))
|
||||
|
||||
(define pl-fb-list-to-sx (fn (t) (pl-fb-list-walked (pl-walk-deep t))))
|
||||
|
||||
(define
|
||||
pl-fb-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-fb-prog-src "member(X, [X|_]). member(X, [_|T]) :- member(X, T).")
|
||||
|
||||
(define pl-fb-db (pl-mk-db))
|
||||
(pl-db-load! pl-fb-db (pl-parse pl-fb-prog-src))
|
||||
|
||||
;; ── findall ──
|
||||
|
||||
(define pl-fb-env-1 {})
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "findall(X, member(X, [a, b, c]), L)" pl-fb-env-1)
|
||||
(pl-mk-trail))
|
||||
(pl-fb-test!
|
||||
"findall member [a, b, c]"
|
||||
(pl-fb-list-to-sx (dict-get pl-fb-env-1 "L"))
|
||||
(list "a" "b" "c"))
|
||||
|
||||
(define pl-fb-env-2 {})
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "findall(X, (member(X, [1, 2, 3]), X >= 2), L)" pl-fb-env-2)
|
||||
(pl-mk-trail))
|
||||
(pl-fb-test!
|
||||
"findall with comparison filter"
|
||||
(pl-fb-list-to-sx (dict-get pl-fb-env-2 "L"))
|
||||
(list 2 3))
|
||||
|
||||
(define pl-fb-env-3 {})
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "findall(X, fail, L)" pl-fb-env-3)
|
||||
(pl-mk-trail))
|
||||
(pl-fb-test!
|
||||
"findall on fail succeeds with empty list"
|
||||
(pl-fb-list-to-sx (dict-get pl-fb-env-3 "L"))
|
||||
(list))
|
||||
|
||||
(pl-fb-test!
|
||||
"findall(X, fail, L) the goal succeeds"
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "findall(X, fail, L)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-fb-env-4 {})
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal
|
||||
"findall(p(X, Y), (member(X, [1, 2]), member(Y, [a, b])), L)"
|
||||
pl-fb-env-4)
|
||||
(pl-mk-trail))
|
||||
(pl-fb-test!
|
||||
"findall over compound template — count = 4"
|
||||
(len (pl-fb-list-to-sx (dict-get pl-fb-env-4 "L")))
|
||||
4)
|
||||
|
||||
;; ── bagof ──
|
||||
|
||||
(pl-fb-test!
|
||||
"bagof succeeds when results exist"
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "bagof(X, member(X, [1, 2, 3]), L)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-fb-test!
|
||||
"bagof fails on empty"
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "bagof(X, fail, L)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-fb-env-5 {})
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "bagof(X, member(X, [c, a, b]), L)" pl-fb-env-5)
|
||||
(pl-mk-trail))
|
||||
(pl-fb-test!
|
||||
"bagof preserves order"
|
||||
(pl-fb-list-to-sx (dict-get pl-fb-env-5 "L"))
|
||||
(list "c" "a" "b"))
|
||||
|
||||
;; ── setof ──
|
||||
|
||||
(define pl-fb-env-6 {})
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "setof(X, member(X, [c, a, b, a, c]), L)" pl-fb-env-6)
|
||||
(pl-mk-trail))
|
||||
(pl-fb-test!
|
||||
"setof sorts + dedupes atoms"
|
||||
(pl-fb-list-to-sx (dict-get pl-fb-env-6 "L"))
|
||||
(list "a" "b" "c"))
|
||||
|
||||
(pl-fb-test!
|
||||
"setof fails on empty"
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "setof(X, fail, L)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-fb-env-7 {})
|
||||
(pl-solve-once!
|
||||
pl-fb-db
|
||||
(pl-fb-goal "setof(X, member(X, [3, 1, 2, 1, 3]), L)" pl-fb-env-7)
|
||||
(pl-mk-trail))
|
||||
(pl-fb-test!
|
||||
"setof sorts + dedupes nums"
|
||||
(pl-fb-list-to-sx (dict-get pl-fb-env-7 "L"))
|
||||
(list 1 2 3))
|
||||
|
||||
(define pl-findall-tests-run! (fn () {:failed pl-fb-test-fail :passed pl-fb-test-pass :total pl-fb-test-count :failures pl-fb-test-failures}))
|
||||
@@ -1,165 +0,0 @@
|
||||
;; lib/prolog/tests/hs_bridge.sx — tests for Prolog↔Hyperscript bridge
|
||||
;;
|
||||
;; Verifies pl-hs-query, pl-hs-predicate/N, and pl-hs-install.
|
||||
;; Also demonstrates the end-to-end DSL pattern:
|
||||
;; (define allowed (pl-hs-predicate/2 db "allowed"))
|
||||
;; → (allowed "alice" "edit") is what Hyperscript compiles
|
||||
;; `when allowed(alice, edit)` to.
|
||||
|
||||
(define pl-hsb-test-count 0)
|
||||
(define pl-hsb-test-pass 0)
|
||||
(define pl-hsb-test-fail 0)
|
||||
(define pl-hsb-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-hsb-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-hsb-test-count (+ pl-hsb-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-hsb-test-pass (+ pl-hsb-test-pass 1))
|
||||
(begin
|
||||
(set! pl-hsb-test-fail (+ pl-hsb-test-fail 1))
|
||||
(append!
|
||||
pl-hsb-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
;; ── shared KB ──
|
||||
|
||||
(define
|
||||
pl-hsb-perm-src
|
||||
"role(alice, admin). role(bob, editor). role(charlie, viewer). permission(admin, read). permission(admin, write). permission(admin, delete). permission(editor, read). permission(editor, write). permission(viewer, read). allowed(U, A) :- role(U, R), permission(R, A).")
|
||||
|
||||
(define pl-hsb-db (pl-load pl-hsb-perm-src))
|
||||
|
||||
;; ── pl-hs-query ──
|
||||
|
||||
(pl-hsb-test!
|
||||
"pl-hs-query: ground fact succeeds"
|
||||
(pl-hs-query pl-hsb-db "role(alice, admin)")
|
||||
true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"pl-hs-query: absent fact fails"
|
||||
(pl-hs-query pl-hsb-db "role(alice, viewer)")
|
||||
false)
|
||||
|
||||
(pl-hsb-test!
|
||||
"pl-hs-query: rule derivation succeeds"
|
||||
(pl-hs-query pl-hsb-db "allowed(alice, delete)")
|
||||
true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"pl-hs-query: rule derivation fails"
|
||||
(pl-hs-query pl-hsb-db "allowed(charlie, delete)")
|
||||
false)
|
||||
|
||||
(pl-hsb-test!
|
||||
"pl-hs-query: arithmetic goal"
|
||||
(pl-hs-query pl-hsb-db "X is 3 + 4, X = 7")
|
||||
true)
|
||||
|
||||
;; ── pl-hs-predicate/2 ──
|
||||
|
||||
(define pl-hsb-allowed (pl-hs-predicate/2 pl-hsb-db "allowed"))
|
||||
|
||||
(pl-hsb-test!
|
||||
"predicate/2: alice can read"
|
||||
(pl-hsb-allowed "alice" "read")
|
||||
true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"predicate/2: alice can delete"
|
||||
(pl-hsb-allowed "alice" "delete")
|
||||
true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"predicate/2: charlie cannot write"
|
||||
(pl-hsb-allowed "charlie" "write")
|
||||
false)
|
||||
|
||||
(pl-hsb-test!
|
||||
"predicate/2: bob can write"
|
||||
(pl-hsb-allowed "bob" "write")
|
||||
true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"predicate/2: unknown user fails"
|
||||
(pl-hsb-allowed "eve" "read")
|
||||
false)
|
||||
|
||||
;; ── DSL simulation ──
|
||||
;; Hyperscript compiles `when allowed(user, action) then …`
|
||||
;; to `(allowed user action)` — a direct SX function call.
|
||||
;; Here we verify that pattern works end-to-end.
|
||||
|
||||
(define pl-hsb-user "alice")
|
||||
(define pl-hsb-action "write")
|
||||
|
||||
(pl-hsb-test!
|
||||
"DSL simulation: (allowed user action) true path"
|
||||
(pl-hsb-allowed pl-hsb-user pl-hsb-action)
|
||||
true)
|
||||
|
||||
(define pl-hsb-user2 "charlie")
|
||||
|
||||
(pl-hsb-test!
|
||||
"DSL simulation: (allowed user action) false path"
|
||||
(pl-hsb-allowed pl-hsb-user2 pl-hsb-action)
|
||||
false)
|
||||
|
||||
;; ── pl-hs-predicate/1 ──
|
||||
|
||||
(define pl-hsb-viewer-src "color(red). color(green). color(blue).")
|
||||
(define pl-hsb-color-db (pl-load pl-hsb-viewer-src))
|
||||
(define pl-hsb-color? (pl-hs-predicate/1 pl-hsb-color-db "color"))
|
||||
|
||||
(pl-hsb-test! "predicate/1: color(red) succeeds" (pl-hsb-color? "red") true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"predicate/1: color(purple) fails"
|
||||
(pl-hsb-color? "purple")
|
||||
false)
|
||||
|
||||
;; ── pl-hs-predicate/3 ──
|
||||
|
||||
(define pl-hsb-3ary-src "between_vals(X, Lo, Hi) :- X >= Lo, X =< Hi.")
|
||||
(define pl-hsb-3ary-db (pl-load pl-hsb-3ary-src))
|
||||
(define pl-hsb-in-range? (pl-hs-predicate/3 pl-hsb-3ary-db "between_vals"))
|
||||
|
||||
(pl-hsb-test!
|
||||
"predicate/3: 5 in range [1,10]"
|
||||
(pl-hsb-in-range? "5" "1" "10")
|
||||
true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"predicate/3: 15 not in range [1,10]"
|
||||
(pl-hsb-in-range? "15" "1" "10")
|
||||
false)
|
||||
|
||||
;; ── pl-hs-install ──
|
||||
|
||||
(define
|
||||
pl-hsb-installed
|
||||
(pl-hs-install
|
||||
pl-hsb-db
|
||||
(list (list "allowed" 2) (list "role" 2) (list "permission" 2))))
|
||||
|
||||
(pl-hsb-test!
|
||||
"pl-hs-install: returns dict with allowed key"
|
||||
(not (nil? (dict-get pl-hsb-installed "allowed")))
|
||||
true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"pl-hs-install: installed allowed fn works"
|
||||
((dict-get pl-hsb-installed "allowed") "alice" "delete")
|
||||
true)
|
||||
|
||||
(pl-hsb-test!
|
||||
"pl-hs-install: installed role fn works"
|
||||
((dict-get pl-hsb-installed "role") "bob" "editor")
|
||||
true)
|
||||
|
||||
(define pl-hs-bridge-tests-run! (fn () {:failed pl-hsb-test-fail :passed pl-hsb-test-pass :total pl-hsb-test-count :failures pl-hsb-test-failures}))
|
||||
@@ -1,172 +0,0 @@
|
||||
;; lib/prolog/tests/integration.sx — end-to-end integration tests via pl-query-* API
|
||||
;;
|
||||
;; Tests the full source→parse→load→solve pipeline with real programs.
|
||||
;; Covers: permission system, graph reachability, quicksort, fibonacci, dynamic KB.
|
||||
|
||||
(define pl-int-test-count 0)
|
||||
(define pl-int-test-pass 0)
|
||||
(define pl-int-test-fail 0)
|
||||
(define pl-int-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-int-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-int-test-count (+ pl-int-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-int-test-pass (+ pl-int-test-pass 1))
|
||||
(begin
|
||||
(set! pl-int-test-fail (+ pl-int-test-fail 1))
|
||||
(append!
|
||||
pl-int-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
;; ── Permission system ──
|
||||
;; role/2 + permission/2 facts, allowed/2 rule
|
||||
|
||||
(define
|
||||
pl-int-perm-src
|
||||
"role(alice, admin). role(bob, editor). role(charlie, viewer). permission(admin, read). permission(admin, write). permission(admin, delete). permission(editor, read). permission(editor, write). permission(viewer, read). allowed(U, A) :- role(U, R), permission(R, A).")
|
||||
|
||||
(define pl-int-perm-db (pl-load pl-int-perm-src))
|
||||
|
||||
(pl-int-test!
|
||||
"alice can read"
|
||||
(len (pl-query-all pl-int-perm-db "allowed(alice, read)"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"alice can delete"
|
||||
(len (pl-query-all pl-int-perm-db "allowed(alice, delete)"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"charlie cannot write"
|
||||
(len (pl-query-all pl-int-perm-db "allowed(charlie, write)"))
|
||||
0)
|
||||
|
||||
(pl-int-test!
|
||||
"alice has 3 permissions"
|
||||
(len (pl-query-all pl-int-perm-db "allowed(alice, A)"))
|
||||
3)
|
||||
|
||||
(pl-int-test!
|
||||
"only one user can delete"
|
||||
(len (pl-query-all pl-int-perm-db "allowed(U, delete)"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"the deleter is alice"
|
||||
(dict-get (first (pl-query-all pl-int-perm-db "allowed(U, delete)")) "U")
|
||||
"alice")
|
||||
|
||||
;; ── Graph reachability ──
|
||||
;; Directed edges; path/2 transitive closure via two clauses
|
||||
|
||||
(define
|
||||
pl-int-graph-src
|
||||
"edge(a, b). edge(b, c). edge(c, d). edge(b, d). path(X, Y) :- edge(X, Y). path(X, Y) :- edge(X, Z), path(Z, Y).")
|
||||
|
||||
(define pl-int-graph-db (pl-load pl-int-graph-src))
|
||||
|
||||
(pl-int-test!
|
||||
"direct edge a→b is a path"
|
||||
(len (pl-query-all pl-int-graph-db "path(a, b)"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"transitive path a→c"
|
||||
(len (pl-query-all pl-int-graph-db "path(a, c)"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"no path d→a (no back-edges)"
|
||||
(len (pl-query-all pl-int-graph-db "path(d, a)"))
|
||||
0)
|
||||
|
||||
(pl-int-test!
|
||||
"4 derivations from a (b,c,d via two routes to d)"
|
||||
(len (pl-query-all pl-int-graph-db "path(a, Y)"))
|
||||
4)
|
||||
|
||||
;; ── Quicksort ──
|
||||
;; Partition-and-recurse; uses its own append/3 to avoid DB pollution
|
||||
|
||||
(define
|
||||
pl-int-qs-src
|
||||
"partition(_, [], [], []). partition(Piv, [H|T], [H|Less], Greater) :- H =< Piv, !, partition(Piv, T, Less, Greater). partition(Piv, [H|T], Less, [H|Greater]) :- partition(Piv, T, Less, Greater). append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R). quicksort([], []). quicksort([H|T], Sorted) :- partition(H, T, Less, Greater), quicksort(Less, SL), quicksort(Greater, SG), append(SL, [H|SG], Sorted).")
|
||||
|
||||
(define pl-int-qs-db (pl-load pl-int-qs-src))
|
||||
|
||||
(pl-int-test!
|
||||
"quicksort([]) = [] (ground check)"
|
||||
(len (pl-query-all pl-int-qs-db "quicksort([], [])"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"quicksort([3,1,2]) = [1,2,3] (ground check)"
|
||||
(len (pl-query-all pl-int-qs-db "quicksort([3,1,2], [1,2,3])"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"quicksort([5,3,1,4,2]) = [1,2,3,4,5] (ground check)"
|
||||
(len (pl-query-all pl-int-qs-db "quicksort([5,3,1,4,2], [1,2,3,4,5])"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"quicksort([3,1,2], [3,1,2]) fails — unsorted order rejected"
|
||||
(len (pl-query-all pl-int-qs-db "quicksort([3,1,2], [3,1,2])"))
|
||||
0)
|
||||
|
||||
;; ── Fibonacci ──
|
||||
;; Naive recursive; ground checks avoid list-format uncertainty
|
||||
|
||||
(define
|
||||
pl-int-fib-src
|
||||
"fib(0, 0). fib(1, 1). fib(N, F) :- N > 1, N1 is N - 1, N2 is N - 2, fib(N1, F1), fib(N2, F2), F is F1 + F2.")
|
||||
|
||||
(define pl-int-fib-db (pl-load pl-int-fib-src))
|
||||
|
||||
(pl-int-test!
|
||||
"fib(0, 0) succeeds"
|
||||
(len (pl-query-all pl-int-fib-db "fib(0, 0)"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"fib(5, 5) succeeds"
|
||||
(len (pl-query-all pl-int-fib-db "fib(5, 5)"))
|
||||
1)
|
||||
|
||||
(pl-int-test!
|
||||
"fib(7, 13) succeeds"
|
||||
(len (pl-query-all pl-int-fib-db "fib(7, 13)"))
|
||||
1)
|
||||
|
||||
;; ── Dynamic knowledge base ──
|
||||
;; Assert and retract facts; the DB dict is mutable so mutations persist
|
||||
|
||||
(define pl-int-dyn-src "color(red). color(green). color(blue).")
|
||||
(define pl-int-dyn-db (pl-load pl-int-dyn-src))
|
||||
|
||||
(pl-int-test!
|
||||
"initial KB: 3 colors"
|
||||
(len (pl-query-all pl-int-dyn-db "color(X)"))
|
||||
3)
|
||||
|
||||
(pl-int-test!
|
||||
"after assert(color(yellow)): 4 colors"
|
||||
(begin
|
||||
(pl-query-all pl-int-dyn-db "assert(color(yellow))")
|
||||
(len (pl-query-all pl-int-dyn-db "color(X)")))
|
||||
4)
|
||||
|
||||
(pl-int-test!
|
||||
"after retract(color(red)): back to 3 colors"
|
||||
(begin
|
||||
(pl-query-all pl-int-dyn-db "retract(color(red))")
|
||||
(len (pl-query-all pl-int-dyn-db "color(X)")))
|
||||
3)
|
||||
|
||||
(define pl-integration-tests-run! (fn () {:failed pl-int-test-fail :passed pl-int-test-pass :total pl-int-test-count :failures pl-int-test-failures}))
|
||||
@@ -1,326 +0,0 @@
|
||||
;; lib/prolog/tests/io_predicates.sx — term_to_atom/2, term_string/2,
|
||||
;; with_output_to/2, writeln/1, format/1, format/2
|
||||
|
||||
(define pl-io-test-count 0)
|
||||
(define pl-io-test-pass 0)
|
||||
(define pl-io-test-fail 0)
|
||||
(define pl-io-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-io-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-io-test-count (+ pl-io-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-io-test-pass (+ pl-io-test-pass 1))
|
||||
(begin
|
||||
(set! pl-io-test-fail (+ pl-io-test-fail 1))
|
||||
(append!
|
||||
pl-io-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-io-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-io-db (pl-mk-db))
|
||||
|
||||
;; helper: get output buffer after running a goal
|
||||
(define
|
||||
pl-io-capture!
|
||||
(fn
|
||||
(goal)
|
||||
(do
|
||||
(pl-output-clear!)
|
||||
(pl-solve-once! pl-io-db goal (pl-mk-trail))
|
||||
pl-output-buffer)))
|
||||
|
||||
;; ─── term_to_atom/2 — bound Term direction ─────────────────────────────────
|
||||
|
||||
(pl-io-test!
|
||||
"term_to_atom(foo(a,b), A) — compound"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_to_atom(foo(a,b), A)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
|
||||
"foo(a, b)")
|
||||
|
||||
(pl-io-test!
|
||||
"term_to_atom(hello, A) — atom"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_to_atom(hello, A)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
|
||||
"hello")
|
||||
|
||||
(pl-io-test!
|
||||
"term_to_atom(42, A) — number"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_to_atom(42, A)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
|
||||
"42")
|
||||
|
||||
(pl-io-test!
|
||||
"term_to_atom(foo(a,b), 'foo(a, b)') — succeeds when Atom matches"
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_to_atom(foo(a,b), 'foo(a, b)')" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-io-test!
|
||||
"term_to_atom(hello, world) — fails on mismatch"
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_to_atom(hello, world)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ─── term_to_atom/2 — parse direction (Atom bound, Term unbound) ───────────
|
||||
|
||||
(pl-io-test!
|
||||
"term_to_atom(T, 'foo(a)') — parse direction gives compound"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_to_atom(T, 'foo(a)')" env)
|
||||
(pl-mk-trail))
|
||||
(let
|
||||
((t (pl-walk-deep (dict-get env "T"))))
|
||||
(and (pl-compound? t) (= (pl-fun t) "foo"))))
|
||||
true)
|
||||
|
||||
(pl-io-test!
|
||||
"term_to_atom(T, hello) — parse direction gives atom"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_to_atom(T, hello)" env)
|
||||
(pl-mk-trail))
|
||||
(let
|
||||
((t (pl-walk-deep (dict-get env "T"))))
|
||||
(and (pl-atom? t) (= (pl-atom-name t) "hello"))))
|
||||
true)
|
||||
|
||||
;; ─── term_string/2 — alias ──────────────────────────────────────────────────
|
||||
|
||||
(pl-io-test!
|
||||
"term_string(bar(x), A) — same as term_to_atom"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_string(bar(x), A)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
|
||||
"bar(x)")
|
||||
|
||||
(pl-io-test!
|
||||
"term_string(42, A) — number to string"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "term_string(42, A)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "A"))))
|
||||
"42")
|
||||
|
||||
;; ─── writeln/1 ─────────────────────────────────────────────────────────────
|
||||
|
||||
(pl-io-test!
|
||||
"writeln(hello) writes 'hello\n'"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), writeln(hello))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"hello
|
||||
")
|
||||
|
||||
(pl-io-test!
|
||||
"writeln(42) writes '42\n'"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), writeln(42))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"42
|
||||
")
|
||||
|
||||
;; ─── with_output_to/2 ──────────────────────────────────────────────────────
|
||||
|
||||
(pl-io-test!
|
||||
"with_output_to(atom(X), write(foo)) — captures write output"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), write(foo))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"foo")
|
||||
|
||||
(pl-io-test!
|
||||
"with_output_to(atom(X), (write(a), write(b))) — concat output"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), (write(a), write(b)))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"ab")
|
||||
|
||||
(pl-io-test!
|
||||
"with_output_to(atom(X), nl) — captures newline"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), nl)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"
|
||||
")
|
||||
|
||||
(pl-io-test!
|
||||
"with_output_to(atom(X), true) — captures empty string"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), true)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"")
|
||||
|
||||
(pl-io-test!
|
||||
"with_output_to(string(X), write(hello)) — string sink works"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(string(X), write(hello))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"hello")
|
||||
|
||||
(pl-io-test!
|
||||
"with_output_to(atom(X), fail) — fails when goal fails"
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), fail)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ─── format/1 ──────────────────────────────────────────────────────────────
|
||||
|
||||
(pl-io-test!
|
||||
"format('hello~n') — tilde-n becomes newline"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), format('hello~n'))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"hello
|
||||
")
|
||||
|
||||
(pl-io-test!
|
||||
"format('~~') — double tilde becomes single tilde"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), format('~~'))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"~")
|
||||
|
||||
(pl-io-test!
|
||||
"format('abc') — plain text passes through"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), format(abc))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"abc")
|
||||
|
||||
;; ─── format/2 ──────────────────────────────────────────────────────────────
|
||||
|
||||
(pl-io-test!
|
||||
"format('~w+~w', [1,2]) — two ~w args"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), format('~w+~w', [1,2]))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"1+2")
|
||||
|
||||
(pl-io-test!
|
||||
"format('hello ~a!', [world]) — ~a with atom arg"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), format('hello ~a!', [world]))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"hello world!")
|
||||
|
||||
(pl-io-test!
|
||||
"format('n=~d', [42]) — ~d with integer arg"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), format('n=~d', [42]))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"n=42")
|
||||
|
||||
(pl-io-test!
|
||||
"format('~w', [foo(a)]) — ~w with compound"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-io-db
|
||||
(pl-io-goal "with_output_to(atom(X), format('~w', [foo(a)]))" env)
|
||||
(pl-mk-trail))
|
||||
(pl-atom-name (pl-walk-deep (dict-get env "X"))))
|
||||
"foo(a)")
|
||||
|
||||
(define
|
||||
pl-io-predicates-tests-run!
|
||||
(fn
|
||||
()
|
||||
{:failed pl-io-test-fail
|
||||
:passed pl-io-test-pass
|
||||
:total pl-io-test-count
|
||||
:failures pl-io-test-failures}))
|
||||
@@ -1,320 +0,0 @@
|
||||
;; lib/prolog/tests/iso_predicates.sx — succ/2, plus/3, between/3, length/2, last/2, nth0/3, nth1/3, max/min arith
|
||||
|
||||
(define pl-ip-test-count 0)
|
||||
(define pl-ip-test-pass 0)
|
||||
(define pl-ip-test-fail 0)
|
||||
(define pl-ip-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-ip-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-ip-test-count (+ pl-ip-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-ip-test-pass (+ pl-ip-test-pass 1))
|
||||
(begin
|
||||
(set! pl-ip-test-fail (+ pl-ip-test-fail 1))
|
||||
(append!
|
||||
pl-ip-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-ip-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-ip-db (pl-mk-db))
|
||||
|
||||
;; ── succ/2 ──
|
||||
|
||||
(define pl-ip-env-s1 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "succ(3, X)" pl-ip-env-s1)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"succ(3, X) → X=4"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-s1 "X")))
|
||||
4)
|
||||
|
||||
(define pl-ip-env-s2 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "succ(0, X)" pl-ip-env-s2)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"succ(0, X) → X=1"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-s2 "X")))
|
||||
1)
|
||||
|
||||
(define pl-ip-env-s3 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "succ(X, 5)" pl-ip-env-s3)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"succ(X, 5) → X=4"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-s3 "X")))
|
||||
4)
|
||||
|
||||
(pl-ip-test!
|
||||
"succ(X, 0) fails"
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "succ(X, 0)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── plus/3 ──
|
||||
|
||||
(define pl-ip-env-p1 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "plus(2, 3, X)" pl-ip-env-p1)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"plus(2, 3, X) → X=5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-p1 "X")))
|
||||
5)
|
||||
|
||||
(define pl-ip-env-p2 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "plus(2, X, 7)" pl-ip-env-p2)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"plus(2, X, 7) → X=5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-p2 "X")))
|
||||
5)
|
||||
|
||||
(define pl-ip-env-p3 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "plus(X, 3, 7)" pl-ip-env-p3)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"plus(X, 3, 7) → X=4"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-p3 "X")))
|
||||
4)
|
||||
|
||||
(pl-ip-test!
|
||||
"plus(0, 0, 0) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "plus(0, 0, 0)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── between/3 ──
|
||||
|
||||
(pl-ip-test!
|
||||
"between(1, 3, X): 3 solutions"
|
||||
(pl-solve-count!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "between(1, 3, X)" {})
|
||||
(pl-mk-trail))
|
||||
3)
|
||||
|
||||
(pl-ip-test!
|
||||
"between(1, 3, 2) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "between(1, 3, 2)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-ip-test!
|
||||
"between(1, 3, 5) fails"
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "between(1, 3, 5)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-ip-test!
|
||||
"between(5, 3, X): 0 solutions (empty range)"
|
||||
(pl-solve-count!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "between(5, 3, X)" {})
|
||||
(pl-mk-trail))
|
||||
0)
|
||||
|
||||
(define pl-ip-env-b1 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "between(1, 5, X)" pl-ip-env-b1)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"between(1, 5, X): first solution X=1"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-b1 "X")))
|
||||
1)
|
||||
|
||||
(pl-ip-test!
|
||||
"between + condition: between(1,5,X), X > 3 → 2 solutions"
|
||||
(pl-solve-count!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "between(1, 5, X), X > 3" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
;; ── length/2 ──
|
||||
|
||||
(define pl-ip-env-l1 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "length([1,2,3], N)" pl-ip-env-l1)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"length([1,2,3], N) → N=3"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-l1 "N")))
|
||||
3)
|
||||
|
||||
(define pl-ip-env-l2 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "length([], N)" pl-ip-env-l2)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"length([], N) → N=0"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-l2 "N")))
|
||||
0)
|
||||
|
||||
(pl-ip-test!
|
||||
"length([a,b], 2) check succeeds"
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "length([a,b], 2)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-ip-env-l3 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "length(L, 3)" pl-ip-env-l3)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"length(L, 3): L is a list of length 3"
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "length(L, 3), is_list(L)" pl-ip-env-l3)
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── last/2 ──
|
||||
|
||||
(define pl-ip-env-la1 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "last([1,2,3], X)" pl-ip-env-la1)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"last([1,2,3], X) → X=3"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-la1 "X")))
|
||||
3)
|
||||
|
||||
(define pl-ip-env-la2 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "last([a], X)" pl-ip-env-la2)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"last([a], X) → X=a"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-la2 "X")))
|
||||
"a")
|
||||
|
||||
(pl-ip-test!
|
||||
"last([], X) fails"
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "last([], X)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── nth0/3 ──
|
||||
|
||||
(define pl-ip-env-n0 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "nth0(0, [a,b,c], X)" pl-ip-env-n0)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"nth0(0, [a,b,c], X) → X=a"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-n0 "X")))
|
||||
"a")
|
||||
|
||||
(define pl-ip-env-n1 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "nth0(2, [a,b,c], X)" pl-ip-env-n1)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"nth0(2, [a,b,c], X) → X=c"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-n1 "X")))
|
||||
"c")
|
||||
|
||||
(pl-ip-test!
|
||||
"nth0(5, [a,b,c], X) fails"
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "nth0(5, [a,b,c], X)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── nth1/3 ──
|
||||
|
||||
(define pl-ip-env-n1a {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "nth1(1, [a,b,c], X)" pl-ip-env-n1a)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"nth1(1, [a,b,c], X) → X=a"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-n1a "X")))
|
||||
"a")
|
||||
|
||||
(define pl-ip-env-n1b {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "nth1(3, [a,b,c], X)" pl-ip-env-n1b)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"nth1(3, [a,b,c], X) → X=c"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-ip-env-n1b "X")))
|
||||
"c")
|
||||
|
||||
;; ── max/min in arithmetic ──
|
||||
|
||||
(define pl-ip-env-m1 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "X is max(3, 5)" pl-ip-env-m1)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"X is max(3, 5) → X=5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-m1 "X")))
|
||||
5)
|
||||
|
||||
(define pl-ip-env-m2 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "X is min(3, 5)" pl-ip-env-m2)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"X is min(3, 5) → X=3"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-m2 "X")))
|
||||
3)
|
||||
|
||||
(define pl-ip-env-m3 {})
|
||||
(pl-solve-once!
|
||||
pl-ip-db
|
||||
(pl-ip-goal "X is max(7, 2) + min(1, 4)" pl-ip-env-m3)
|
||||
(pl-mk-trail))
|
||||
(pl-ip-test!
|
||||
"X is max(7,2) + min(1,4) → X=8"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-ip-env-m3 "X")))
|
||||
8)
|
||||
|
||||
(define pl-iso-predicates-tests-run! (fn () {:failed pl-ip-test-fail :passed pl-ip-test-pass :total pl-ip-test-count :failures pl-ip-test-failures}))
|
||||
@@ -1,335 +0,0 @@
|
||||
;; lib/prolog/tests/list_predicates.sx — ==/2, \==/2, flatten/2, numlist/3,
|
||||
;; atomic_list_concat/2,3, sum_list/2, max_list/2, min_list/2, delete/3
|
||||
|
||||
(define pl-lp-test-count 0)
|
||||
(define pl-lp-test-pass 0)
|
||||
(define pl-lp-test-fail 0)
|
||||
(define pl-lp-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-lp-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-lp-test-count (+ pl-lp-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-lp-test-pass (+ pl-lp-test-pass 1))
|
||||
(begin
|
||||
(set! pl-lp-test-fail (+ pl-lp-test-fail 1))
|
||||
(append!
|
||||
pl-lp-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-lp-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-lp-db (pl-mk-db))
|
||||
|
||||
;; ── ==/2 ───────────────────────────────────────────────────────────
|
||||
|
||||
(pl-lp-test!
|
||||
"==(a, a) succeeds"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "==(a, a)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-lp-test!
|
||||
"==(a, b) fails"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "==(a, b)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-lp-test!
|
||||
"==(1, 1) succeeds"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "==(1, 1)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-lp-test!
|
||||
"==(1, 2) fails"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "==(1, 2)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-lp-test!
|
||||
"==(f(a,b), f(a,b)) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "==(f(a,b), f(a,b))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-lp-test!
|
||||
"==(f(a,b), f(a,c)) fails"
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "==(f(a,b), f(a,c))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; unbound var vs atom: fails (different tags)
|
||||
(pl-lp-test!
|
||||
"==(X, a) fails (unbound var vs atom)"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "==(X, a)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; two unbound vars with SAME name in same env share the same runtime var
|
||||
(define pl-lp-env-same-var {})
|
||||
(pl-lp-goal "==(X, X)" pl-lp-env-same-var)
|
||||
(pl-lp-test!
|
||||
"==(X, X) succeeds (same runtime var)"
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-instantiate
|
||||
(nth (first (pl-parse "g :- ==(X, X).")) 2)
|
||||
pl-lp-env-same-var)
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── \==/2 ──────────────────────────────────────────────────────────
|
||||
|
||||
(pl-lp-test!
|
||||
"\\==(a, b) succeeds"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "\\==(a, b)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-lp-test!
|
||||
"\\==(a, a) fails"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "\\==(a, a)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-lp-test!
|
||||
"\\==(X, a) succeeds (unbound var differs from atom)"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "\\==(X, a)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-lp-test!
|
||||
"\\==(1, 2) succeeds"
|
||||
(pl-solve-once! pl-lp-db (pl-lp-goal "\\==(1, 2)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── flatten/2 ──────────────────────────────────────────────────────
|
||||
|
||||
(define pl-lp-env-fl1 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "flatten([], F)" pl-lp-env-fl1)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"flatten([], []) -> empty"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-fl1 "F")))
|
||||
"[]")
|
||||
|
||||
(define pl-lp-env-fl2 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "flatten([1,2,3], F)" pl-lp-env-fl2)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"flatten([1,2,3], F) -> [1,2,3]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-fl2 "F")))
|
||||
".(1, .(2, .(3, [])))")
|
||||
|
||||
(define pl-lp-env-fl3 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "flatten([1,[2,[3]],4], F)" pl-lp-env-fl3)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"flatten([1,[2,[3]],4], F) -> [1,2,3,4]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-fl3 "F")))
|
||||
".(1, .(2, .(3, .(4, []))))")
|
||||
|
||||
(define pl-lp-env-fl4 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "flatten([[a,b],[c]], F)" pl-lp-env-fl4)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"flatten([[a,b],[c]], F) -> [a,b,c]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-fl4 "F")))
|
||||
".(a, .(b, .(c, [])))")
|
||||
|
||||
;; ── numlist/3 ──────────────────────────────────────────────────────
|
||||
|
||||
(define pl-lp-env-nl1 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "numlist(1, 5, L)" pl-lp-env-nl1)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"numlist(1,5,L) -> [1,2,3,4,5]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-nl1 "L")))
|
||||
".(1, .(2, .(3, .(4, .(5, [])))))")
|
||||
|
||||
(define pl-lp-env-nl2 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "numlist(3, 3, L)" pl-lp-env-nl2)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"numlist(3,3,L) -> [3]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-nl2 "L")))
|
||||
".(3, [])")
|
||||
|
||||
(pl-lp-test!
|
||||
"numlist(5, 3, L) fails (Low > High)"
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "numlist(5, 3, L)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; ── atomic_list_concat/2 ───────────────────────────────────────────
|
||||
|
||||
(define pl-lp-env-alc1 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "atomic_list_concat([a, b, c], R)" pl-lp-env-alc1)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"atomic_list_concat([a,b,c], R) -> abc"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-lp-env-alc1 "R")))
|
||||
"abc")
|
||||
|
||||
(define pl-lp-env-alc2 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "atomic_list_concat([hello, world], R)" pl-lp-env-alc2)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"atomic_list_concat([hello,world], R) -> helloworld"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-lp-env-alc2 "R")))
|
||||
"helloworld")
|
||||
|
||||
;; ── atomic_list_concat/3 ───────────────────────────────────────────
|
||||
|
||||
(define pl-lp-env-alcs1 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "atomic_list_concat([a, b, c], '-', R)" pl-lp-env-alcs1)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"atomic_list_concat([a,b,c], '-', R) -> a-b-c"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-lp-env-alcs1 "R")))
|
||||
"a-b-c")
|
||||
|
||||
(define pl-lp-env-alcs2 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "atomic_list_concat([x], '-', R)" pl-lp-env-alcs2)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"atomic_list_concat([x], '-', R) -> x (single element, no sep)"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-lp-env-alcs2 "R")))
|
||||
"x")
|
||||
|
||||
;; ── sum_list/2 ─────────────────────────────────────────────────────
|
||||
|
||||
(define pl-lp-env-sl1 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "sum_list([1,2,3], S)" pl-lp-env-sl1)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"sum_list([1,2,3], S) -> 6"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-sl1 "S")))
|
||||
6)
|
||||
|
||||
(define pl-lp-env-sl2 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "sum_list([10], S)" pl-lp-env-sl2)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"sum_list([10], S) -> 10"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-sl2 "S")))
|
||||
10)
|
||||
|
||||
(define pl-lp-env-sl3 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "sum_list([], S)" pl-lp-env-sl3)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"sum_list([], S) -> 0"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-sl3 "S")))
|
||||
0)
|
||||
|
||||
;; ── max_list/2 ─────────────────────────────────────────────────────
|
||||
|
||||
(define pl-lp-env-mx1 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "max_list([3,1,4,1,5,9,2,6], M)" pl-lp-env-mx1)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"max_list([3,1,4,1,5,9,2,6], M) -> 9"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-mx1 "M")))
|
||||
9)
|
||||
|
||||
(define pl-lp-env-mx2 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "max_list([7], M)" pl-lp-env-mx2)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"max_list([7], M) -> 7"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-mx2 "M")))
|
||||
7)
|
||||
|
||||
;; ── min_list/2 ─────────────────────────────────────────────────────
|
||||
|
||||
(define pl-lp-env-mn1 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "min_list([3,1,4,1,5,9,2,6], M)" pl-lp-env-mn1)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"min_list([3,1,4,1,5,9,2,6], M) -> 1"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-mn1 "M")))
|
||||
1)
|
||||
|
||||
(define pl-lp-env-mn2 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "min_list([5,2,8], M)" pl-lp-env-mn2)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"min_list([5,2,8], M) -> 2"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-lp-env-mn2 "M")))
|
||||
2)
|
||||
|
||||
;; ── delete/3 ───────────────────────────────────────────────────────
|
||||
|
||||
(define pl-lp-env-del1 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "delete([1,2,3,2,1], 2, R)" pl-lp-env-del1)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"delete([1,2,3,2,1], 2, R) -> [1,3,1]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-del1 "R")))
|
||||
".(1, .(3, .(1, [])))")
|
||||
|
||||
(define pl-lp-env-del2 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "delete([a,b,c], d, R)" pl-lp-env-del2)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"delete([a,b,c], d, R) -> [a,b,c] (nothing deleted)"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-del2 "R")))
|
||||
".(a, .(b, .(c, [])))")
|
||||
|
||||
(define pl-lp-env-del3 {})
|
||||
(pl-solve-once!
|
||||
pl-lp-db
|
||||
(pl-lp-goal "delete([], x, R)" pl-lp-env-del3)
|
||||
(pl-mk-trail))
|
||||
(pl-lp-test!
|
||||
"delete([], x, R) -> []"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-lp-env-del3 "R")))
|
||||
"[]")
|
||||
|
||||
(define pl-list-predicates-tests-run! (fn () {:failed pl-lp-test-fail :passed pl-lp-test-pass :total pl-lp-test-count :failures pl-lp-test-failures}))
|
||||
@@ -1,197 +0,0 @@
|
||||
;; lib/prolog/tests/meta_call.sx — forall/2, maplist/2, maplist/3, include/3, exclude/3
|
||||
(define pl-mc-test-count 0)
|
||||
(define pl-mc-test-pass 0)
|
||||
(define pl-mc-test-fail 0)
|
||||
(define pl-mc-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-mc-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-mc-test-count (+ pl-mc-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-mc-test-pass (+ pl-mc-test-pass 1))
|
||||
(begin
|
||||
(set! pl-mc-test-fail (+ pl-mc-test-fail 1))
|
||||
(append!
|
||||
pl-mc-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-mc-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define
|
||||
pl-mc-term-to-sx
|
||||
(fn
|
||||
(t)
|
||||
(cond
|
||||
((pl-num? t) (pl-num-val t))
|
||||
((pl-atom? t) (pl-atom-name t))
|
||||
(else t))))
|
||||
|
||||
(define
|
||||
pl-mc-list-sx
|
||||
(fn
|
||||
(t)
|
||||
(let
|
||||
((w (pl-walk-deep t)))
|
||||
(cond
|
||||
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
|
||||
((and (pl-compound? w) (= (pl-fun w) "."))
|
||||
(cons
|
||||
(pl-mc-term-to-sx (first (pl-args w)))
|
||||
(pl-mc-list-sx (nth (pl-args w) 1))))
|
||||
(else (list :not-list))))))
|
||||
|
||||
(define pl-mc-db (pl-mk-db))
|
||||
|
||||
(pl-db-load!
|
||||
pl-mc-db
|
||||
(pl-parse "member(X, [X|_]). member(X, [_|T]) :- member(X, T)."))
|
||||
|
||||
(pl-db-load! pl-mc-db (pl-parse "double(X, Y) :- Y is X * 2."))
|
||||
|
||||
(pl-db-load! pl-mc-db (pl-parse "even(X) :- 0 is X mod 2."))
|
||||
|
||||
;; -- forall/2 --
|
||||
|
||||
(pl-mc-test!
|
||||
"forall(member(X,[2,4,6]), 0 is X mod 2) — all even"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "forall(member(X,[2,4,6]), 0 is X mod 2)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mc-test!
|
||||
"forall(member(X,[2,3,6]), 0 is X mod 2) — 3 is odd, fails"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "forall(member(X,[2,3,6]), 0 is X mod 2)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-mc-test!
|
||||
"forall(member(_,[]), true) — vacuously true"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "forall(member(_,[]), true)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; -- maplist/2 --
|
||||
|
||||
(pl-mc-test!
|
||||
"maplist(atom, [a,b,c]) — all atoms"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "maplist(atom, [a,b,c])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mc-test!
|
||||
"maplist(atom, [a,1,c]) — 1 is not atom, fails"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "maplist(atom, [a,1,c])" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-mc-test!
|
||||
"maplist(atom, []) — vacuously true"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "maplist(atom, [])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; -- maplist/3 --
|
||||
|
||||
(pl-mc-test!
|
||||
"maplist(double, [1,2,3], [2,4,6]) — deterministic check"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "maplist(double, [1,2,3], [2,4,6])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mc-test!
|
||||
"maplist(double, [1,2,3], [2,4,7]) — wrong result fails"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "maplist(double, [1,2,3], [2,4,7])" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-mc-env-ml3 {:L (pl-mk-rt-var "L")})
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "maplist(double, [1,2,3], L)" pl-mc-env-ml3)
|
||||
(pl-mk-trail))
|
||||
(pl-mc-test!
|
||||
"maplist(double, [1,2,3], L) — L bound to [2,4,6]"
|
||||
(pl-mc-list-sx (dict-get pl-mc-env-ml3 "L"))
|
||||
(list 2 4 6))
|
||||
|
||||
;; -- include/3 --
|
||||
|
||||
(pl-mc-test!
|
||||
"include(even, [1,2,3,4,5,6], [2,4,6])"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "include(even, [1,2,3,4,5,6], [2,4,6])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mc-test!
|
||||
"include(even, [], [])"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "include(even, [], [])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-mc-env-inc {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "include(even, [1,2,3,4,5,6], R)" pl-mc-env-inc)
|
||||
(pl-mk-trail))
|
||||
(pl-mc-test!
|
||||
"include(even, [1,2,3,4,5,6], R) — R bound to [2,4,6]"
|
||||
(pl-mc-list-sx (dict-get pl-mc-env-inc "R"))
|
||||
(list 2 4 6))
|
||||
|
||||
;; -- exclude/3 --
|
||||
|
||||
(pl-mc-test!
|
||||
"exclude(even, [1,2,3,4,5,6], [1,3,5])"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "exclude(even, [1,2,3,4,5,6], [1,3,5])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mc-test!
|
||||
"exclude(even, [], [])"
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "exclude(even, [], [])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-mc-env-exc {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-mc-db
|
||||
(pl-mc-goal "exclude(even, [1,2,3,4,5,6], R)" pl-mc-env-exc)
|
||||
(pl-mk-trail))
|
||||
(pl-mc-test!
|
||||
"exclude(even, [1,2,3,4,5,6], R) — R bound to [1,3,5]"
|
||||
(pl-mc-list-sx (dict-get pl-mc-env-exc "R"))
|
||||
(list 1 3 5))
|
||||
|
||||
(define pl-meta-call-tests-run! (fn () {:failed pl-mc-test-fail :passed pl-mc-test-pass :total pl-mc-test-count :failures pl-mc-test-failures}))
|
||||
@@ -1,252 +0,0 @@
|
||||
;; lib/prolog/tests/meta_predicates.sx — \+/1, not/1, once/1, ignore/1, ground/1, sort/2, msort/2, atom_number/2, number_string/2
|
||||
|
||||
(define pl-mp-test-count 0)
|
||||
(define pl-mp-test-pass 0)
|
||||
(define pl-mp-test-fail 0)
|
||||
(define pl-mp-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-mp-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-mp-test-count (+ pl-mp-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-mp-test-pass (+ pl-mp-test-pass 1))
|
||||
(begin
|
||||
(set! pl-mp-test-fail (+ pl-mp-test-fail 1))
|
||||
(append!
|
||||
pl-mp-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-mp-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-mp-db (pl-mk-db))
|
||||
(pl-db-load!
|
||||
pl-mp-db
|
||||
(pl-parse "member(X, [X|_]). member(X, [_|T]) :- member(X, T)."))
|
||||
|
||||
;; -- \+/1 --
|
||||
|
||||
(pl-mp-test!
|
||||
"\\+(fail) succeeds"
|
||||
(pl-solve-once! pl-mp-db (pl-mp-goal "\\+(fail)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mp-test!
|
||||
"\\+(true) fails"
|
||||
(pl-solve-once! pl-mp-db (pl-mp-goal "\\+(true)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-mp-test!
|
||||
"\\+(member(d, [a,b,c])) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "\\+(member(d, [a,b,c]))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mp-test!
|
||||
"\\+(member(a, [a,b,c])) fails"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "\\+(member(a, [a,b,c]))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-mp-env-neg {})
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "\\+(X = 5)" pl-mp-env-neg)
|
||||
(pl-mk-trail))
|
||||
(pl-mp-test!
|
||||
"\\+(X=5) fails, X stays unbound (bindings undone)"
|
||||
(nil? (pl-var-binding (dict-get pl-mp-env-neg "X")))
|
||||
true)
|
||||
|
||||
;; -- not/1 --
|
||||
|
||||
(pl-mp-test!
|
||||
"not(fail) succeeds"
|
||||
(pl-solve-once! pl-mp-db (pl-mp-goal "not(fail)" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mp-test!
|
||||
"not(true) fails"
|
||||
(pl-solve-once! pl-mp-db (pl-mp-goal "not(true)" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; -- once/1 --
|
||||
|
||||
(pl-mp-test!
|
||||
"once(member(X,[1,2,3])) succeeds once"
|
||||
(pl-solve-count!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "once(member(X,[1,2,3]))" {})
|
||||
(pl-mk-trail))
|
||||
1)
|
||||
|
||||
(define pl-mp-env-once {})
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "once(member(X,[1,2,3]))" pl-mp-env-once)
|
||||
(pl-mk-trail))
|
||||
(pl-mp-test!
|
||||
"once(member(X,[1,2,3])): X=1 (first solution)"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-mp-env-once "X")))
|
||||
1)
|
||||
|
||||
(pl-mp-test!
|
||||
"once(fail) fails"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "once(fail)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; -- ignore/1 --
|
||||
|
||||
(pl-mp-test!
|
||||
"ignore(true) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "ignore(true)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mp-test!
|
||||
"ignore(fail) still succeeds"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "ignore(fail)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; -- ground/1 --
|
||||
|
||||
(pl-mp-test!
|
||||
"ground(foo(1, a)) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "ground(foo(1, a))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mp-test!
|
||||
"ground(foo(X, a)) fails (X unbound)"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "ground(foo(X, a))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-mp-test!
|
||||
"ground(42) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "ground(42)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; -- sort/2 --
|
||||
|
||||
(pl-mp-test!
|
||||
"sort([b,a,c], [a,b,c])"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "sort([b,a,c], [a,b,c])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mp-test!
|
||||
"sort([b,a,a,c], [a,b,c]) (removes duplicates)"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "sort([b,a,a,c], [a,b,c])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mp-test!
|
||||
"sort([], [])"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "sort([], [])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; -- msort/2 --
|
||||
|
||||
(pl-mp-test!
|
||||
"msort([b,a,a,c], [a,a,b,c]) (keeps duplicates)"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "msort([b,a,a,c], [a,a,b,c])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mp-test!
|
||||
"msort([3,1,2,1], [1,1,2,3])"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "msort([3,1,2,1], [1,1,2,3])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; -- atom_number/2 --
|
||||
|
||||
(define pl-mp-env-an1 {})
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "atom_number('42', N)" pl-mp-env-an1)
|
||||
(pl-mk-trail))
|
||||
(pl-mp-test!
|
||||
"atom_number('42', N) -> N=42"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-mp-env-an1 "N")))
|
||||
42)
|
||||
|
||||
(define pl-mp-env-an2 {})
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "atom_number(A, 7)" pl-mp-env-an2)
|
||||
(pl-mk-trail))
|
||||
(pl-mp-test!
|
||||
"atom_number(A, 7) -> A='7'"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-mp-env-an2 "A")))
|
||||
"7")
|
||||
|
||||
(pl-mp-test!
|
||||
"atom_number(foo, N) fails (not a number)"
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "atom_number(foo, N)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
;; -- number_string/2 --
|
||||
|
||||
(define pl-mp-env-ns1 {})
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "number_string(42, S)" pl-mp-env-ns1)
|
||||
(pl-mk-trail))
|
||||
(pl-mp-test!
|
||||
"number_string(42, S) -> S='42'"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-mp-env-ns1 "S")))
|
||||
"42")
|
||||
|
||||
(define pl-mp-env-ns2 {})
|
||||
(pl-solve-once!
|
||||
pl-mp-db
|
||||
(pl-mp-goal "number_string(N, '3.14')" pl-mp-env-ns2)
|
||||
(pl-mk-trail))
|
||||
(pl-mp-test!
|
||||
"number_string(N, '3.14') -> N=3.14"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-mp-env-ns2 "N")))
|
||||
3.14)
|
||||
|
||||
(define pl-meta-predicates-tests-run! (fn () {:failed pl-mp-test-fail :passed pl-mp-test-pass :total pl-mp-test-count :failures pl-mp-test-failures}))
|
||||
@@ -1,193 +0,0 @@
|
||||
;; lib/prolog/tests/operators.sx — operator-table parsing + comparison built-ins.
|
||||
|
||||
(define pl-op-test-count 0)
|
||||
(define pl-op-test-pass 0)
|
||||
(define pl-op-test-fail 0)
|
||||
(define pl-op-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-op-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-op-test-count (+ pl-op-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-op-test-pass (+ pl-op-test-pass 1))
|
||||
(begin
|
||||
(set! pl-op-test-fail (+ pl-op-test-fail 1))
|
||||
(append!
|
||||
pl-op-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define pl-op-empty-db (pl-mk-db))
|
||||
|
||||
(define
|
||||
pl-op-body
|
||||
(fn (src) (nth (first (pl-parse (str "g :- " src "."))) 2)))
|
||||
|
||||
(define pl-op-goal (fn (src env) (pl-instantiate (pl-op-body src) env)))
|
||||
|
||||
;; ── parsing tests ──
|
||||
|
||||
(pl-op-test!
|
||||
"infix +"
|
||||
(pl-op-body "a + b")
|
||||
(list "compound" "+" (list (list "atom" "a") (list "atom" "b"))))
|
||||
|
||||
(pl-op-test!
|
||||
"infix * tighter than +"
|
||||
(pl-op-body "a + b * c")
|
||||
(list
|
||||
"compound"
|
||||
"+"
|
||||
(list
|
||||
(list "atom" "a")
|
||||
(list "compound" "*" (list (list "atom" "b") (list "atom" "c"))))))
|
||||
|
||||
(pl-op-test!
|
||||
"parens override precedence"
|
||||
(pl-op-body "(a + b) * c")
|
||||
(list
|
||||
"compound"
|
||||
"*"
|
||||
(list
|
||||
(list "compound" "+" (list (list "atom" "a") (list "atom" "b")))
|
||||
(list "atom" "c"))))
|
||||
|
||||
(pl-op-test!
|
||||
"+ is yfx (left-assoc)"
|
||||
(pl-op-body "a + b + c")
|
||||
(list
|
||||
"compound"
|
||||
"+"
|
||||
(list
|
||||
(list "compound" "+" (list (list "atom" "a") (list "atom" "b")))
|
||||
(list "atom" "c"))))
|
||||
|
||||
(pl-op-test!
|
||||
"; is xfy (right-assoc)"
|
||||
(pl-op-body "a ; b ; c")
|
||||
(list
|
||||
"compound"
|
||||
";"
|
||||
(list
|
||||
(list "atom" "a")
|
||||
(list "compound" ";" (list (list "atom" "b") (list "atom" "c"))))))
|
||||
|
||||
(pl-op-test!
|
||||
"= folds at 700"
|
||||
(pl-op-body "X = 5")
|
||||
(list "compound" "=" (list (list "var" "X") (list "num" 5))))
|
||||
|
||||
(pl-op-test!
|
||||
"is + nests via 700>500>400"
|
||||
(pl-op-body "X is 2 + 3 * 4")
|
||||
(list
|
||||
"compound"
|
||||
"is"
|
||||
(list
|
||||
(list "var" "X")
|
||||
(list
|
||||
"compound"
|
||||
"+"
|
||||
(list
|
||||
(list "num" 2)
|
||||
(list "compound" "*" (list (list "num" 3) (list "num" 4))))))))
|
||||
|
||||
(pl-op-test!
|
||||
"< parses at 700"
|
||||
(pl-op-body "2 < 3")
|
||||
(list "compound" "<" (list (list "num" 2) (list "num" 3))))
|
||||
|
||||
(pl-op-test!
|
||||
"mod parses as yfx 400"
|
||||
(pl-op-body "10 mod 3")
|
||||
(list "compound" "mod" (list (list "num" 10) (list "num" 3))))
|
||||
|
||||
(pl-op-test!
|
||||
"comma in body folds right-assoc"
|
||||
(pl-op-body "a, b, c")
|
||||
(list
|
||||
"compound"
|
||||
","
|
||||
(list
|
||||
(list "atom" "a")
|
||||
(list "compound" "," (list (list "atom" "b") (list "atom" "c"))))))
|
||||
|
||||
;; ── solver tests via infix ──
|
||||
|
||||
(pl-op-test!
|
||||
"X is 2 + 3 binds X = 5"
|
||||
(let
|
||||
((env {}) (trail (pl-mk-trail)))
|
||||
(begin
|
||||
(pl-solve-once! pl-op-empty-db (pl-op-goal "X is 2 + 3" env) trail)
|
||||
(pl-num-val (pl-walk-deep (dict-get env "X")))))
|
||||
5)
|
||||
|
||||
(pl-op-test!
|
||||
"infix conjunction parses + solves"
|
||||
(pl-solve-once!
|
||||
pl-op-empty-db
|
||||
(pl-op-goal "X = 5, X = 5" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-op-test!
|
||||
"infix mismatch fails"
|
||||
(pl-solve-once!
|
||||
pl-op-empty-db
|
||||
(pl-op-goal "X = 5, X = 6" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-op-test!
|
||||
"infix disjunction picks left"
|
||||
(pl-solve-once!
|
||||
pl-op-empty-db
|
||||
(pl-op-goal "true ; fail" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-op-test!
|
||||
"2 < 5 succeeds"
|
||||
(pl-solve-once!
|
||||
pl-op-empty-db
|
||||
(pl-op-goal "2 < 5" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-op-test!
|
||||
"5 < 2 fails"
|
||||
(pl-solve-once!
|
||||
pl-op-empty-db
|
||||
(pl-op-goal "5 < 2" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-op-test!
|
||||
"5 >= 5 succeeds"
|
||||
(pl-solve-once!
|
||||
pl-op-empty-db
|
||||
(pl-op-goal "5 >= 5" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-op-test!
|
||||
"3 =< 5 succeeds"
|
||||
(pl-solve-once!
|
||||
pl-op-empty-db
|
||||
(pl-op-goal "3 =< 5" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-op-test!
|
||||
"infix < with arithmetic both sides"
|
||||
(pl-solve-once!
|
||||
pl-op-empty-db
|
||||
(pl-op-goal "1 + 2 < 2 * 3" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-operators-tests-run! (fn () {:failed pl-op-test-fail :passed pl-op-test-pass :total pl-op-test-count :failures pl-op-test-failures}))
|
||||
@@ -1,5 +0,0 @@
|
||||
%% append/3 — list concatenation, classic Prolog
|
||||
%% Two clauses: empty-prefix base case + recursive cons-prefix.
|
||||
%% Bidirectional — works in all modes: build, check, split.
|
||||
append([], L, L).
|
||||
append([H|T], L, [H|R]) :- append(T, L, R).
|
||||
@@ -1,114 +0,0 @@
|
||||
;; lib/prolog/tests/programs/append.sx — append/3 test runner
|
||||
;;
|
||||
;; Mirrors the Prolog source in append.pl (embedded as a string here because
|
||||
;; the SX runtime has no file-read primitive yet).
|
||||
|
||||
(define pl-ap-test-count 0)
|
||||
(define pl-ap-test-pass 0)
|
||||
(define pl-ap-test-fail 0)
|
||||
(define pl-ap-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-ap-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-ap-test-count (+ pl-ap-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-ap-test-pass (+ pl-ap-test-pass 1))
|
||||
(begin
|
||||
(set! pl-ap-test-fail (+ pl-ap-test-fail 1))
|
||||
(append!
|
||||
pl-ap-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-ap-term-to-sx
|
||||
(fn
|
||||
(t)
|
||||
(cond
|
||||
((pl-num? t) (pl-num-val t))
|
||||
((pl-atom? t) (pl-atom-name t))
|
||||
(true (list :complex)))))
|
||||
|
||||
(define
|
||||
pl-ap-list-walked
|
||||
(fn
|
||||
(w)
|
||||
(cond
|
||||
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
|
||||
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
|
||||
(cons
|
||||
(pl-ap-term-to-sx (first (pl-args w)))
|
||||
(pl-ap-list-walked (nth (pl-args w) 1))))
|
||||
(true (list :not-list)))))
|
||||
|
||||
(define pl-ap-list-to-sx (fn (t) (pl-ap-list-walked (pl-walk-deep t))))
|
||||
|
||||
(define
|
||||
pl-ap-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define
|
||||
pl-ap-prog-src
|
||||
"append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R).")
|
||||
|
||||
(define pl-ap-db (pl-mk-db))
|
||||
|
||||
(pl-db-load! pl-ap-db (pl-parse pl-ap-prog-src))
|
||||
|
||||
(define pl-ap-env-1 {})
|
||||
(define pl-ap-goal-1 (pl-ap-goal "append([], [a, b], X)" pl-ap-env-1))
|
||||
(pl-solve-once! pl-ap-db pl-ap-goal-1 (pl-mk-trail))
|
||||
|
||||
(pl-ap-test!
|
||||
"append([], [a, b], X) → X = [a, b]"
|
||||
(pl-ap-list-to-sx (dict-get pl-ap-env-1 "X"))
|
||||
(list "a" "b"))
|
||||
|
||||
(define pl-ap-env-2 {})
|
||||
(define pl-ap-goal-2 (pl-ap-goal "append([1, 2], [3, 4], X)" pl-ap-env-2))
|
||||
(pl-solve-once! pl-ap-db pl-ap-goal-2 (pl-mk-trail))
|
||||
|
||||
(pl-ap-test!
|
||||
"append([1, 2], [3, 4], X) → X = [1, 2, 3, 4]"
|
||||
(pl-ap-list-to-sx (dict-get pl-ap-env-2 "X"))
|
||||
(list 1 2 3 4))
|
||||
|
||||
(pl-ap-test!
|
||||
"append([1], [2, 3], [1, 2, 3]) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-ap-db
|
||||
(pl-ap-goal "append([1], [2, 3], [1, 2, 3])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-ap-test!
|
||||
"append([1, 2], [3], [1, 2, 4]) fails"
|
||||
(pl-solve-once!
|
||||
pl-ap-db
|
||||
(pl-ap-goal "append([1, 2], [3], [1, 2, 4])" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-ap-test!
|
||||
"append(X, Y, [1, 2, 3]) backtracks 4 times"
|
||||
(pl-solve-count!
|
||||
pl-ap-db
|
||||
(pl-ap-goal "append(X, Y, [1, 2, 3])" {})
|
||||
(pl-mk-trail))
|
||||
4)
|
||||
|
||||
(define pl-ap-env-6 {})
|
||||
(define pl-ap-goal-6 (pl-ap-goal "append(X, [3], [1, 2, 3])" pl-ap-env-6))
|
||||
(pl-solve-once! pl-ap-db pl-ap-goal-6 (pl-mk-trail))
|
||||
|
||||
(pl-ap-test!
|
||||
"append(X, [3], [1, 2, 3]) deduces X = [1, 2]"
|
||||
(pl-ap-list-to-sx (dict-get pl-ap-env-6 "X"))
|
||||
(list 1 2))
|
||||
|
||||
(define pl-append-tests-run! (fn () {:failed pl-ap-test-fail :passed pl-ap-test-pass :total pl-ap-test-count :failures pl-ap-test-failures}))
|
||||
@@ -1,24 +0,0 @@
|
||||
%% family — facts + transitive ancestor + derived relations.
|
||||
%% Five-generation tree: tom -> bob -> {ann, pat} -> jim, plus tom's
|
||||
%% other child liz.
|
||||
|
||||
parent(tom, bob).
|
||||
parent(tom, liz).
|
||||
parent(bob, ann).
|
||||
parent(bob, pat).
|
||||
parent(pat, jim).
|
||||
|
||||
male(tom).
|
||||
male(bob).
|
||||
male(jim).
|
||||
male(pat).
|
||||
female(liz).
|
||||
female(ann).
|
||||
|
||||
father(F, C) :- parent(F, C), male(F).
|
||||
mother(M, C) :- parent(M, C), female(M).
|
||||
|
||||
ancestor(X, Y) :- parent(X, Y).
|
||||
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
|
||||
|
||||
sibling(X, Y) :- parent(P, X), parent(P, Y), \=(X, Y).
|
||||
@@ -1,116 +0,0 @@
|
||||
;; lib/prolog/tests/programs/family.sx — facts + ancestor + sibling relations.
|
||||
|
||||
(define pl-fa-test-count 0)
|
||||
(define pl-fa-test-pass 0)
|
||||
(define pl-fa-test-fail 0)
|
||||
(define pl-fa-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-fa-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-fa-test-count (+ pl-fa-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-fa-test-pass (+ pl-fa-test-pass 1))
|
||||
(begin
|
||||
(set! pl-fa-test-fail (+ pl-fa-test-fail 1))
|
||||
(append!
|
||||
pl-fa-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-fa-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define
|
||||
pl-fa-prog-src
|
||||
"parent(tom, bob). parent(tom, liz). parent(bob, ann). parent(bob, pat). parent(pat, jim). male(tom). male(bob). male(jim). male(pat). female(liz). female(ann). father(F, C) :- parent(F, C), male(F). mother(M, C) :- parent(M, C), female(M). ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y). sibling(X, Y) :- parent(P, X), parent(P, Y), \\=(X, Y).")
|
||||
|
||||
(define pl-fa-db (pl-mk-db))
|
||||
(pl-db-load! pl-fa-db (pl-parse pl-fa-prog-src))
|
||||
|
||||
(pl-fa-test!
|
||||
"parent(tom, bob) is a fact"
|
||||
(pl-solve-once!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "parent(tom, bob)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-fa-test!
|
||||
"parent(tom, ann) — not a direct parent"
|
||||
(pl-solve-once!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "parent(tom, ann)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-fa-test!
|
||||
"5 parent/2 facts in total"
|
||||
(pl-solve-count!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "parent(X, Y)" {})
|
||||
(pl-mk-trail))
|
||||
5)
|
||||
|
||||
(pl-fa-test!
|
||||
"ancestor(tom, jim) — three-step transitive"
|
||||
(pl-solve-once!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "ancestor(tom, jim)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-fa-test!
|
||||
"tom has 5 ancestors-of: bob, liz, ann, pat, jim"
|
||||
(pl-solve-count!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "ancestor(tom, X)" {})
|
||||
(pl-mk-trail))
|
||||
5)
|
||||
|
||||
(pl-fa-test!
|
||||
"father(bob, ann) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "father(bob, ann)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-fa-test!
|
||||
"father(liz, ann) fails (liz is female)"
|
||||
(pl-solve-once!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "father(liz, ann)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-fa-test!
|
||||
"mother(liz, X) fails (liz has no children)"
|
||||
(pl-solve-once!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "mother(liz, X)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-fa-test!
|
||||
"sibling(ann, pat) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "sibling(ann, pat)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-fa-test!
|
||||
"sibling(ann, ann) fails by \\="
|
||||
(pl-solve-once!
|
||||
pl-fa-db
|
||||
(pl-fa-goal "sibling(ann, ann)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-family-tests-run! (fn () {:failed pl-fa-test-fail :passed pl-fa-test-pass :total pl-fa-test-count :failures pl-fa-test-failures}))
|
||||
@@ -1,4 +0,0 @@
|
||||
%% member/2 — list membership.
|
||||
%% Generates all solutions on backtracking when the element is unbound.
|
||||
member(X, [X|_]).
|
||||
member(X, [_|T]) :- member(X, T).
|
||||
@@ -1,91 +0,0 @@
|
||||
;; lib/prolog/tests/programs/member.sx — member/2 generator.
|
||||
|
||||
(define pl-mb-test-count 0)
|
||||
(define pl-mb-test-pass 0)
|
||||
(define pl-mb-test-fail 0)
|
||||
(define pl-mb-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-mb-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-mb-test-count (+ pl-mb-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-mb-test-pass (+ pl-mb-test-pass 1))
|
||||
(begin
|
||||
(set! pl-mb-test-fail (+ pl-mb-test-fail 1))
|
||||
(append!
|
||||
pl-mb-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-mb-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-mb-prog-src "member(X, [X|_]). member(X, [_|T]) :- member(X, T).")
|
||||
|
||||
(define pl-mb-db (pl-mk-db))
|
||||
(pl-db-load! pl-mb-db (pl-parse pl-mb-prog-src))
|
||||
|
||||
(pl-mb-test!
|
||||
"member(2, [1, 2, 3]) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-mb-db
|
||||
(pl-mb-goal "member(2, [1, 2, 3])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-mb-test!
|
||||
"member(4, [1, 2, 3]) fails"
|
||||
(pl-solve-once!
|
||||
pl-mb-db
|
||||
(pl-mb-goal "member(4, [1, 2, 3])" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-mb-test!
|
||||
"member(X, []) fails"
|
||||
(pl-solve-once!
|
||||
pl-mb-db
|
||||
(pl-mb-goal "member(X, [])" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-mb-test!
|
||||
"member(X, [a, b, c]) generates 3 solutions"
|
||||
(pl-solve-count!
|
||||
pl-mb-db
|
||||
(pl-mb-goal "member(X, [a, b, c])" {})
|
||||
(pl-mk-trail))
|
||||
3)
|
||||
|
||||
(define pl-mb-env-1 {})
|
||||
(define pl-mb-goal-1 (pl-mb-goal "member(X, [11, 22, 33])" pl-mb-env-1))
|
||||
(pl-solve-once! pl-mb-db pl-mb-goal-1 (pl-mk-trail))
|
||||
|
||||
(pl-mb-test!
|
||||
"member(X, [11, 22, 33]) first solution X = 11"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-mb-env-1 "X")))
|
||||
11)
|
||||
|
||||
(pl-mb-test!
|
||||
"member(2, [1, 2, 3, 2, 1]) matches twice on backtrack"
|
||||
(pl-solve-count!
|
||||
pl-mb-db
|
||||
(pl-mb-goal "member(2, [1, 2, 3, 2, 1])" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
(pl-mb-test!
|
||||
"member with unbound list cell unifies"
|
||||
(pl-solve-once!
|
||||
pl-mb-db
|
||||
(pl-mb-goal "member(a, [X, b, c])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-member-tests-run! (fn () {:failed pl-mb-test-fail :passed pl-mb-test-pass :total pl-mb-test-count :failures pl-mb-test-failures}))
|
||||
@@ -1,27 +0,0 @@
|
||||
%% nqueens — permutation-and-test formulation.
|
||||
%% Caller passes the row list [1..N]; queens/2 finds N column placements
|
||||
%% s.t. no two queens attack on a diagonal. Same-column attacks are
|
||||
%% structurally impossible — Qs is a permutation, all distinct.
|
||||
%%
|
||||
%% No `>/2` `</2` `=</2` built-ins yet, so range/3 is omitted; tests pass
|
||||
%; the literal range list. Once the operator table lands and arithmetic
|
||||
%% comparison built-ins are in, range/3 can be added.
|
||||
queens(L, Qs) :- permute(L, Qs), safe(Qs).
|
||||
|
||||
permute([], []).
|
||||
permute(L, [H|T]) :- select(H, L, R), permute(R, T).
|
||||
|
||||
select(X, [X|T], T).
|
||||
select(X, [H|T], [H|R]) :- select(X, T, R).
|
||||
|
||||
safe([]).
|
||||
safe([Q|Qs]) :- safe(Qs), no_attack(Q, Qs, 1).
|
||||
|
||||
no_attack(_, [], _).
|
||||
no_attack(Q, [Q1|Qs], D) :-
|
||||
is(D2, +(Q, D)),
|
||||
\=(D2, Q1),
|
||||
is(D3, -(Q, D)),
|
||||
\=(D3, Q1),
|
||||
is(D1, +(D, 1)),
|
||||
no_attack(Q, Qs, D1).
|
||||
@@ -1,108 +0,0 @@
|
||||
;; lib/prolog/tests/programs/nqueens.sx — N-queens via permute + safe.
|
||||
|
||||
(define pl-nq-test-count 0)
|
||||
(define pl-nq-test-pass 0)
|
||||
(define pl-nq-test-fail 0)
|
||||
(define pl-nq-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-nq-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-nq-test-count (+ pl-nq-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-nq-test-pass (+ pl-nq-test-pass 1))
|
||||
(begin
|
||||
(set! pl-nq-test-fail (+ pl-nq-test-fail 1))
|
||||
(append!
|
||||
pl-nq-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-nq-term-to-sx
|
||||
(fn
|
||||
(t)
|
||||
(cond
|
||||
((pl-num? t) (pl-num-val t))
|
||||
((pl-atom? t) (pl-atom-name t))
|
||||
(true (list :complex)))))
|
||||
|
||||
(define
|
||||
pl-nq-list-walked
|
||||
(fn
|
||||
(w)
|
||||
(cond
|
||||
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
|
||||
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
|
||||
(cons
|
||||
(pl-nq-term-to-sx (first (pl-args w)))
|
||||
(pl-nq-list-walked (nth (pl-args w) 1))))
|
||||
(true (list :not-list)))))
|
||||
|
||||
(define pl-nq-list-to-sx (fn (t) (pl-nq-list-walked (pl-walk-deep t))))
|
||||
|
||||
(define
|
||||
pl-nq-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define
|
||||
pl-nq-prog-src
|
||||
"queens(L, Qs) :- permute(L, Qs), safe(Qs). permute([], []). permute(L, [H|T]) :- select(H, L, R), permute(R, T). select(X, [X|T], T). select(X, [H|T], [H|R]) :- select(X, T, R). safe([]). safe([Q|Qs]) :- safe(Qs), no_attack(Q, Qs, 1). no_attack(_, [], _). no_attack(Q, [Q1|Qs], D) :- is(D2, +(Q, D)), \\=(D2, Q1), is(D3, -(Q, D)), \\=(D3, Q1), is(D1, +(D, 1)), no_attack(Q, Qs, D1).")
|
||||
|
||||
(define pl-nq-db (pl-mk-db))
|
||||
(pl-db-load! pl-nq-db (pl-parse pl-nq-prog-src))
|
||||
|
||||
(pl-nq-test!
|
||||
"queens([1], Qs) → 1 solution"
|
||||
(pl-solve-count!
|
||||
pl-nq-db
|
||||
(pl-nq-goal "queens([1], Qs)" {})
|
||||
(pl-mk-trail))
|
||||
1)
|
||||
|
||||
(pl-nq-test!
|
||||
"queens([1, 2], Qs) → 0 solutions"
|
||||
(pl-solve-count!
|
||||
pl-nq-db
|
||||
(pl-nq-goal "queens([1, 2], Qs)" {})
|
||||
(pl-mk-trail))
|
||||
0)
|
||||
|
||||
(pl-nq-test!
|
||||
"queens([1, 2, 3], Qs) → 0 solutions"
|
||||
(pl-solve-count!
|
||||
pl-nq-db
|
||||
(pl-nq-goal "queens([1, 2, 3], Qs)" {})
|
||||
(pl-mk-trail))
|
||||
0)
|
||||
|
||||
(pl-nq-test!
|
||||
"queens([1, 2, 3, 4], Qs) → 2 solutions"
|
||||
(pl-solve-count!
|
||||
pl-nq-db
|
||||
(pl-nq-goal "queens([1, 2, 3, 4], Qs)" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
(pl-nq-test!
|
||||
"queens([1, 2, 3, 4, 5], Qs) → 10 solutions"
|
||||
(pl-solve-count!
|
||||
pl-nq-db
|
||||
(pl-nq-goal "queens([1, 2, 3, 4, 5], Qs)" {})
|
||||
(pl-mk-trail))
|
||||
10)
|
||||
|
||||
(define pl-nq-env-1 {})
|
||||
(define pl-nq-goal-1 (pl-nq-goal "queens([1, 2, 3, 4], Qs)" pl-nq-env-1))
|
||||
(pl-solve-once! pl-nq-db pl-nq-goal-1 (pl-mk-trail))
|
||||
|
||||
(pl-nq-test!
|
||||
"queens([1..4], Qs) first solution = [2, 4, 1, 3]"
|
||||
(pl-nq-list-to-sx (dict-get pl-nq-env-1 "Qs"))
|
||||
(list 2 4 1 3))
|
||||
|
||||
(define pl-nqueens-tests-run! (fn () {:failed pl-nq-test-fail :passed pl-nq-test-pass :total pl-nq-test-count :failures pl-nq-test-failures}))
|
||||
@@ -1,7 +0,0 @@
|
||||
%% reverse/2 — naive reverse via append/3.
|
||||
%% Quadratic — accumulates the reversed prefix one append per cons.
|
||||
reverse([], []).
|
||||
reverse([H|T], R) :- reverse(T, RT), append(RT, [H], R).
|
||||
|
||||
append([], L, L).
|
||||
append([H|T], L, [H|R]) :- append(T, L, R).
|
||||
@@ -1,113 +0,0 @@
|
||||
;; lib/prolog/tests/programs/reverse.sx — naive reverse/2 via append/3.
|
||||
;;
|
||||
;; Mirrors reverse.pl (embedded as a string here).
|
||||
|
||||
(define pl-rv-test-count 0)
|
||||
(define pl-rv-test-pass 0)
|
||||
(define pl-rv-test-fail 0)
|
||||
(define pl-rv-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-rv-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-rv-test-count (+ pl-rv-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-rv-test-pass (+ pl-rv-test-pass 1))
|
||||
(begin
|
||||
(set! pl-rv-test-fail (+ pl-rv-test-fail 1))
|
||||
(append!
|
||||
pl-rv-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-rv-term-to-sx
|
||||
(fn
|
||||
(t)
|
||||
(cond
|
||||
((pl-num? t) (pl-num-val t))
|
||||
((pl-atom? t) (pl-atom-name t))
|
||||
(true (list :complex)))))
|
||||
|
||||
(define
|
||||
pl-rv-list-walked
|
||||
(fn
|
||||
(w)
|
||||
(cond
|
||||
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
|
||||
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
|
||||
(cons
|
||||
(pl-rv-term-to-sx (first (pl-args w)))
|
||||
(pl-rv-list-walked (nth (pl-args w) 1))))
|
||||
(true (list :not-list)))))
|
||||
|
||||
(define pl-rv-list-to-sx (fn (t) (pl-rv-list-walked (pl-walk-deep t))))
|
||||
|
||||
(define
|
||||
pl-rv-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define
|
||||
pl-rv-prog-src
|
||||
"reverse([], []). reverse([H|T], R) :- reverse(T, RT), append(RT, [H], R). append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R).")
|
||||
|
||||
(define pl-rv-db (pl-mk-db))
|
||||
(pl-db-load! pl-rv-db (pl-parse pl-rv-prog-src))
|
||||
|
||||
(define pl-rv-env-1 {})
|
||||
(define pl-rv-goal-1 (pl-rv-goal "reverse([], X)" pl-rv-env-1))
|
||||
(pl-solve-once! pl-rv-db pl-rv-goal-1 (pl-mk-trail))
|
||||
|
||||
(pl-rv-test!
|
||||
"reverse([], X) → X = []"
|
||||
(pl-rv-list-to-sx (dict-get pl-rv-env-1 "X"))
|
||||
(list))
|
||||
|
||||
(define pl-rv-env-2 {})
|
||||
(define pl-rv-goal-2 (pl-rv-goal "reverse([1], X)" pl-rv-env-2))
|
||||
(pl-solve-once! pl-rv-db pl-rv-goal-2 (pl-mk-trail))
|
||||
|
||||
(pl-rv-test!
|
||||
"reverse([1], X) → X = [1]"
|
||||
(pl-rv-list-to-sx (dict-get pl-rv-env-2 "X"))
|
||||
(list 1))
|
||||
|
||||
(define pl-rv-env-3 {})
|
||||
(define pl-rv-goal-3 (pl-rv-goal "reverse([1, 2, 3], X)" pl-rv-env-3))
|
||||
(pl-solve-once! pl-rv-db pl-rv-goal-3 (pl-mk-trail))
|
||||
|
||||
(pl-rv-test!
|
||||
"reverse([1, 2, 3], X) → X = [3, 2, 1]"
|
||||
(pl-rv-list-to-sx (dict-get pl-rv-env-3 "X"))
|
||||
(list 3 2 1))
|
||||
|
||||
(define pl-rv-env-4 {})
|
||||
(define pl-rv-goal-4 (pl-rv-goal "reverse([a, b, c, d], X)" pl-rv-env-4))
|
||||
(pl-solve-once! pl-rv-db pl-rv-goal-4 (pl-mk-trail))
|
||||
|
||||
(pl-rv-test!
|
||||
"reverse([a, b, c, d], X) → X = [d, c, b, a]"
|
||||
(pl-rv-list-to-sx (dict-get pl-rv-env-4 "X"))
|
||||
(list "d" "c" "b" "a"))
|
||||
|
||||
(pl-rv-test!
|
||||
"reverse([1, 2, 3], [3, 2, 1]) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-rv-db
|
||||
(pl-rv-goal "reverse([1, 2, 3], [3, 2, 1])" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-rv-test!
|
||||
"reverse([1, 2], [1, 2]) fails"
|
||||
(pl-solve-once!
|
||||
pl-rv-db
|
||||
(pl-rv-goal "reverse([1, 2], [1, 2])" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-reverse-tests-run! (fn () {:failed pl-rv-test-fail :passed pl-rv-test-pass :total pl-rv-test-count :failures pl-rv-test-failures}))
|
||||
@@ -1,127 +0,0 @@
|
||||
;; lib/prolog/tests/query_api.sx — tests for pl-load/pl-query-all/pl-query-one/pl-query
|
||||
|
||||
(define pl-qa-test-count 0)
|
||||
(define pl-qa-test-pass 0)
|
||||
(define pl-qa-test-fail 0)
|
||||
(define pl-qa-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-qa-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-qa-test-count (+ pl-qa-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-qa-test-pass (+ pl-qa-test-pass 1))
|
||||
(begin
|
||||
(set! pl-qa-test-fail (+ pl-qa-test-fail 1))
|
||||
(append!
|
||||
pl-qa-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-qa-src
|
||||
"parent(tom, bob). parent(tom, liz). parent(bob, ann). ancestor(X, Y) :- parent(X, Y). ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).")
|
||||
|
||||
(define pl-qa-db (pl-load pl-qa-src))
|
||||
|
||||
;; ── pl-load ──
|
||||
|
||||
(pl-qa-test!
|
||||
"pl-load returns a usable DB (pl-query-all non-nil)"
|
||||
(not (nil? pl-qa-db))
|
||||
true)
|
||||
|
||||
;; ── pl-query-all: basic fact lookup ──
|
||||
|
||||
(pl-qa-test!
|
||||
"query-all parent(tom, X): 2 solutions"
|
||||
(len (pl-query-all pl-qa-db "parent(tom, X)"))
|
||||
2)
|
||||
|
||||
(pl-qa-test!
|
||||
"query-all parent(tom, X): first solution X=bob"
|
||||
(dict-get (first (pl-query-all pl-qa-db "parent(tom, X)")) "X")
|
||||
"bob")
|
||||
|
||||
(pl-qa-test!
|
||||
"query-all parent(tom, X): second solution X=liz"
|
||||
(dict-get (nth (pl-query-all pl-qa-db "parent(tom, X)") 1) "X")
|
||||
"liz")
|
||||
|
||||
;; ── pl-query-all: no solutions ──
|
||||
|
||||
(pl-qa-test!
|
||||
"query-all no solutions returns empty list"
|
||||
(pl-query-all pl-qa-db "parent(liz, X)")
|
||||
(list))
|
||||
|
||||
;; ── pl-query-all: boolean query (no vars) ──
|
||||
|
||||
(pl-qa-test!
|
||||
"boolean success: 1 solution (empty dict)"
|
||||
(len (pl-query-all pl-qa-db "parent(tom, bob)"))
|
||||
1)
|
||||
|
||||
(pl-qa-test!
|
||||
"boolean success: solution has no bindings"
|
||||
(empty? (keys (first (pl-query-all pl-qa-db "parent(tom, bob)"))))
|
||||
true)
|
||||
|
||||
(pl-qa-test!
|
||||
"boolean fail: 0 solutions"
|
||||
(len (pl-query-all pl-qa-db "parent(bob, tom)"))
|
||||
0)
|
||||
|
||||
;; ── pl-query-all: multi-var ──
|
||||
|
||||
(pl-qa-test!
|
||||
"query-all parent(X, Y): 3 solutions total"
|
||||
(len (pl-query-all pl-qa-db "parent(X, Y)"))
|
||||
3)
|
||||
|
||||
;; ── pl-query-all: rule-based (ancestor/2) ──
|
||||
|
||||
(pl-qa-test!
|
||||
"query-all ancestor(tom, X): 3 descendants (bob, liz, ann)"
|
||||
(len (pl-query-all pl-qa-db "ancestor(tom, X)"))
|
||||
3)
|
||||
|
||||
;; ── pl-query-all: built-in in query ──
|
||||
|
||||
(pl-qa-test!
|
||||
"query with is/2 built-in"
|
||||
(dict-get (first (pl-query-all pl-qa-db "X is 2 + 3")) "X")
|
||||
"5")
|
||||
|
||||
;; ── pl-query-one ──
|
||||
|
||||
(pl-qa-test!
|
||||
"query-one returns first solution"
|
||||
(dict-get (pl-query-one pl-qa-db "parent(tom, X)") "X")
|
||||
"bob")
|
||||
|
||||
(pl-qa-test!
|
||||
"query-one returns nil for no solutions"
|
||||
(pl-query-one pl-qa-db "parent(liz, X)")
|
||||
nil)
|
||||
|
||||
;; ── pl-query convenience ──
|
||||
|
||||
(pl-qa-test!
|
||||
"pl-query convenience: count solutions"
|
||||
(len (pl-query "likes(alice, bob). likes(alice, carol)." "likes(alice, X)"))
|
||||
2)
|
||||
|
||||
(pl-qa-test!
|
||||
"pl-query convenience: first solution"
|
||||
(dict-get (first (pl-query "likes(alice, bob). likes(alice, carol)." "likes(alice, X)")) "X")
|
||||
"bob")
|
||||
|
||||
(pl-qa-test!
|
||||
"pl-query with empty source (built-ins only)"
|
||||
(dict-get (first (pl-query "" "X is 6 * 7")) "X")
|
||||
"42")
|
||||
|
||||
(define pl-query-api-tests-run! (fn () {:failed pl-qa-test-fail :passed pl-qa-test-pass :total pl-qa-test-count :failures pl-qa-test-failures}))
|
||||
@@ -1,195 +0,0 @@
|
||||
;; lib/prolog/tests/set_predicates.sx — foldl/4, list_to_set/2, intersection/3, subtract/3, union/3
|
||||
|
||||
(define pl-sp-test-count 0)
|
||||
(define pl-sp-test-pass 0)
|
||||
(define pl-sp-test-fail 0)
|
||||
(define pl-sp-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-sp-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-sp-test-count (+ pl-sp-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-sp-test-pass (+ pl-sp-test-pass 1))
|
||||
(begin
|
||||
(set! pl-sp-test-fail (+ pl-sp-test-fail 1))
|
||||
(append!
|
||||
pl-sp-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-sp-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
;; DB with add/3 for foldl tests
|
||||
(define pl-sp-db (pl-mk-db))
|
||||
(pl-db-load! pl-sp-db (pl-parse "add(X, Acc, NAcc) :- NAcc is Acc + X."))
|
||||
|
||||
;; ── foldl/4 ────────────────────────────────────────────────────────
|
||||
|
||||
(define pl-sp-env-fl1 {:S (pl-mk-rt-var "S")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "foldl(add, [1,2,3,4], 0, S)" pl-sp-env-fl1)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"foldl(add,[1,2,3,4],0,S) -> S=10"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-sp-env-fl1 "S")))
|
||||
10)
|
||||
|
||||
(define pl-sp-env-fl2 {:S (pl-mk-rt-var "S")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "foldl(add, [], 5, S)" pl-sp-env-fl2)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"foldl(add,[],5,S) -> S=5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-sp-env-fl2 "S")))
|
||||
5)
|
||||
|
||||
(define pl-sp-env-fl3 {:S (pl-mk-rt-var "S")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "foldl(add, [1,2,3], 0, S)" pl-sp-env-fl3)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"foldl(add,[1,2,3],0,S) -> S=6"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-sp-env-fl3 "S")))
|
||||
6)
|
||||
|
||||
;; ── list_to_set/2 ──────────────────────────────────────────────────
|
||||
|
||||
(define pl-sp-env-lts1 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "list_to_set([1,2,3,2,1], R)" pl-sp-env-lts1)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"list_to_set([1,2,3,2,1],R) -> [1,2,3]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-lts1 "R")))
|
||||
".(1, .(2, .(3, [])))")
|
||||
|
||||
(define pl-sp-env-lts2 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "list_to_set([], R)" pl-sp-env-lts2)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"list_to_set([],R) -> []"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-lts2 "R")))
|
||||
"[]")
|
||||
|
||||
(define pl-sp-env-lts3 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "list_to_set([a,b,a,c], R)" pl-sp-env-lts3)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"list_to_set([a,b,a,c],R) -> [a,b,c]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-lts3 "R")))
|
||||
".(a, .(b, .(c, [])))")
|
||||
|
||||
;; ── intersection/3 ─────────────────────────────────────────────────
|
||||
|
||||
(define pl-sp-env-int1 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "intersection([1,2,3,4], [2,4,6], R)" pl-sp-env-int1)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"intersection([1,2,3,4],[2,4,6],R) -> [2,4]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-int1 "R")))
|
||||
".(2, .(4, []))")
|
||||
|
||||
(define pl-sp-env-int2 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "intersection([1,2,3], [4,5,6], R)" pl-sp-env-int2)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"intersection([1,2,3],[4,5,6],R) -> []"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-int2 "R")))
|
||||
"[]")
|
||||
|
||||
(define pl-sp-env-int3 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "intersection([], [1,2,3], R)" pl-sp-env-int3)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"intersection([],[1,2,3],R) -> []"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-int3 "R")))
|
||||
"[]")
|
||||
|
||||
;; ── subtract/3 ─────────────────────────────────────────────────────
|
||||
|
||||
(define pl-sp-env-sub1 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "subtract([1,2,3,4], [2,4], R)" pl-sp-env-sub1)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"subtract([1,2,3,4],[2,4],R) -> [1,3]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-sub1 "R")))
|
||||
".(1, .(3, []))")
|
||||
|
||||
(define pl-sp-env-sub2 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "subtract([1,2,3], [], R)" pl-sp-env-sub2)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"subtract([1,2,3],[],R) -> [1,2,3]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-sub2 "R")))
|
||||
".(1, .(2, .(3, [])))")
|
||||
|
||||
(define pl-sp-env-sub3 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "subtract([], [1,2], R)" pl-sp-env-sub3)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"subtract([],[1,2],R) -> []"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-sub3 "R")))
|
||||
"[]")
|
||||
|
||||
;; ── union/3 ────────────────────────────────────────────────────────
|
||||
|
||||
(define pl-sp-env-uni1 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "union([1,2,3], [2,3,4], R)" pl-sp-env-uni1)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"union([1,2,3],[2,3,4],R) -> [1,2,3,4]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-uni1 "R")))
|
||||
".(1, .(2, .(3, .(4, []))))")
|
||||
|
||||
(define pl-sp-env-uni2 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "union([], [1,2], R)" pl-sp-env-uni2)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"union([],[1,2],R) -> [1,2]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-uni2 "R")))
|
||||
".(1, .(2, []))")
|
||||
|
||||
(define pl-sp-env-uni3 {:R (pl-mk-rt-var "R")})
|
||||
(pl-solve-once!
|
||||
pl-sp-db
|
||||
(pl-sp-goal "union([1,2], [], R)" pl-sp-env-uni3)
|
||||
(pl-mk-trail))
|
||||
(pl-sp-test!
|
||||
"union([1,2],[],R) -> [1,2]"
|
||||
(pl-format-term (pl-walk-deep (dict-get pl-sp-env-uni3 "R")))
|
||||
".(1, .(2, []))")
|
||||
|
||||
;; ── Runner ─────────────────────────────────────────────────────────
|
||||
|
||||
(define pl-set-predicates-tests-run! (fn () {:failed pl-sp-test-fail :passed pl-sp-test-pass :total pl-sp-test-count :failures pl-sp-test-failures}))
|
||||
@@ -1,618 +0,0 @@
|
||||
;; lib/prolog/tests/solve.sx — DFS solver unit tests
|
||||
|
||||
(define pl-s-test-count 0)
|
||||
(define pl-s-test-pass 0)
|
||||
(define pl-s-test-fail 0)
|
||||
(define pl-s-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-s-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-s-test-count (+ pl-s-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-s-test-pass (+ pl-s-test-pass 1))
|
||||
(begin
|
||||
(set! pl-s-test-fail (+ pl-s-test-fail 1))
|
||||
(append!
|
||||
pl-s-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-s-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-s-empty-db (pl-mk-db))
|
||||
|
||||
(pl-s-test!
|
||||
"true succeeds"
|
||||
(pl-solve-once! pl-s-empty-db (pl-s-goal "true" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"fail fails"
|
||||
(pl-solve-once! pl-s-empty-db (pl-s-goal "fail" {}) (pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"= identical atoms"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "=(a, a)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"= different atoms"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "=(a, b)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"= var to atom"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "=(X, foo)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-s-env-bind {})
|
||||
(define pl-s-trail-bind (pl-mk-trail))
|
||||
(define pl-s-goal-bind (pl-s-goal "=(X, foo)" pl-s-env-bind))
|
||||
(pl-solve-once! pl-s-empty-db pl-s-goal-bind pl-s-trail-bind)
|
||||
|
||||
(pl-s-test!
|
||||
"X bound to foo after =(X, foo)"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-s-env-bind "X")))
|
||||
"foo")
|
||||
|
||||
(pl-s-test!
|
||||
"true , true succeeds"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "true, true" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"true , fail fails"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "true, fail" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"consistent X bindings succeed"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "=(X, a), =(X, a)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"conflicting X bindings fail"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "=(X, a), =(X, b)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-s-db1 (pl-mk-db))
|
||||
(pl-db-load!
|
||||
pl-s-db1
|
||||
(pl-parse "parent(tom, bob). parent(bob, liz). parent(bob, ann)."))
|
||||
|
||||
(pl-s-test!
|
||||
"fact lookup hit"
|
||||
(pl-solve-once!
|
||||
pl-s-db1
|
||||
(pl-s-goal "parent(tom, bob)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"fact lookup miss"
|
||||
(pl-solve-once!
|
||||
pl-s-db1
|
||||
(pl-s-goal "parent(tom, liz)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"all parent solutions"
|
||||
(pl-solve-count!
|
||||
pl-s-db1
|
||||
(pl-s-goal "parent(X, Y)" {})
|
||||
(pl-mk-trail))
|
||||
3)
|
||||
|
||||
(pl-s-test!
|
||||
"fixed first arg solutions"
|
||||
(pl-solve-count!
|
||||
pl-s-db1
|
||||
(pl-s-goal "parent(bob, Y)" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
(define pl-s-db2 (pl-mk-db))
|
||||
(pl-db-load!
|
||||
pl-s-db2
|
||||
(pl-parse
|
||||
"parent(tom, bob). parent(bob, ann). ancestor(X, Y) :- parent(X, Y). ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z)."))
|
||||
|
||||
(pl-s-test!
|
||||
"rule direct ancestor"
|
||||
(pl-solve-once!
|
||||
pl-s-db2
|
||||
(pl-s-goal "ancestor(tom, bob)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"rule transitive ancestor"
|
||||
(pl-solve-once!
|
||||
pl-s-db2
|
||||
(pl-s-goal "ancestor(tom, ann)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"rule no path"
|
||||
(pl-solve-once!
|
||||
pl-s-db2
|
||||
(pl-s-goal "ancestor(ann, tom)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-s-env-undo {})
|
||||
(define pl-s-trail-undo (pl-mk-trail))
|
||||
(define pl-s-goal-undo (pl-s-goal "=(X, a), fail" pl-s-env-undo))
|
||||
(pl-solve-once! pl-s-empty-db pl-s-goal-undo pl-s-trail-undo)
|
||||
|
||||
(pl-s-test!
|
||||
"trail undone after failure leaves X unbound"
|
||||
(pl-var-bound? (dict-get pl-s-env-undo "X"))
|
||||
false)
|
||||
|
||||
(define pl-s-db-cut1 (pl-mk-db))
|
||||
|
||||
(pl-db-load! pl-s-db-cut1 (pl-parse "g :- !. g :- true."))
|
||||
|
||||
(pl-s-test!
|
||||
"bare cut succeeds"
|
||||
(pl-solve-once! pl-s-db-cut1 (pl-s-goal "g" {}) (pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"cut commits to first matching clause"
|
||||
(pl-solve-count! pl-s-db-cut1 (pl-s-goal "g" {}) (pl-mk-trail))
|
||||
1)
|
||||
|
||||
(define pl-s-db-cut2 (pl-mk-db))
|
||||
|
||||
(pl-db-load! pl-s-db-cut2 (pl-parse "a(1). a(2). g(X) :- a(X), !."))
|
||||
|
||||
(pl-s-test!
|
||||
"cut commits to first a solution"
|
||||
(pl-solve-count! pl-s-db-cut2 (pl-s-goal "g(X)" {}) (pl-mk-trail))
|
||||
1)
|
||||
|
||||
(define pl-s-db-cut3 (pl-mk-db))
|
||||
|
||||
(pl-db-load!
|
||||
pl-s-db-cut3
|
||||
(pl-parse "a(1). a(2). g(X) :- a(X), !, fail. g(99)."))
|
||||
|
||||
(pl-s-test!
|
||||
"cut then fail blocks alt clauses"
|
||||
(pl-solve-count! pl-s-db-cut3 (pl-s-goal "g(X)" {}) (pl-mk-trail))
|
||||
0)
|
||||
|
||||
(define pl-s-db-cut4 (pl-mk-db))
|
||||
|
||||
(pl-db-load!
|
||||
pl-s-db-cut4
|
||||
(pl-parse "a(1). b(10). b(20). g(X, Y) :- a(X), !, b(Y)."))
|
||||
|
||||
(pl-s-test!
|
||||
"post-cut goal backtracks freely"
|
||||
(pl-solve-count!
|
||||
pl-s-db-cut4
|
||||
(pl-s-goal "g(X, Y)" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
(define pl-s-db-cut5 (pl-mk-db))
|
||||
|
||||
(pl-db-load!
|
||||
pl-s-db-cut5
|
||||
(pl-parse "r(1). r(2). q :- r(X), !. p :- q. p :- true."))
|
||||
|
||||
(pl-s-test!
|
||||
"inner cut does not commit outer predicate"
|
||||
(pl-solve-count! pl-s-db-cut5 (pl-s-goal "p" {}) (pl-mk-trail))
|
||||
2)
|
||||
|
||||
(pl-s-test!
|
||||
"\\= different atoms succeeds"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "\\=(a, b)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"\\= same atoms fails"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "\\=(a, a)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"\\= var-vs-atom would unify so fails"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "\\=(X, a)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-s-env-ne {})
|
||||
|
||||
(define pl-s-trail-ne (pl-mk-trail))
|
||||
|
||||
(define pl-s-goal-ne (pl-s-goal "\\=(X, a)" pl-s-env-ne))
|
||||
|
||||
(pl-solve-once! pl-s-empty-db pl-s-goal-ne pl-s-trail-ne)
|
||||
|
||||
(pl-s-test!
|
||||
"\\= leaves no bindings"
|
||||
(pl-var-bound? (dict-get pl-s-env-ne "X"))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"; left succeeds"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal ";(true, fail)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"; right succeeds when left fails"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal ";(fail, true)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"; both fail"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal ";(fail, fail)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"; both branches counted"
|
||||
(pl-solve-count!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal ";(true, true)" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
(define pl-s-db-call (pl-mk-db))
|
||||
|
||||
(pl-db-load! pl-s-db-call (pl-parse "p(1). p(2)."))
|
||||
|
||||
(pl-s-test!
|
||||
"call(true) succeeds"
|
||||
(pl-solve-once!
|
||||
pl-s-db-call
|
||||
(pl-s-goal "call(true)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"call(p(X)) yields all solutions"
|
||||
(pl-solve-count!
|
||||
pl-s-db-call
|
||||
(pl-s-goal "call(p(X))" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
(pl-s-test!
|
||||
"call of bound goal var resolves"
|
||||
(pl-solve-once!
|
||||
pl-s-db-call
|
||||
(pl-s-goal "=(G, true), call(G)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-s-db-ite (pl-mk-db))
|
||||
|
||||
(pl-db-load! pl-s-db-ite (pl-parse "p(1). p(2). q(yes). q(no)."))
|
||||
|
||||
(pl-s-test!
|
||||
"if-then-else: cond true → then runs"
|
||||
(pl-solve-once!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal ";(->(true, =(X, ok)), =(X, fallback))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-s-env-ite1 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal ";(->(true, =(X, ok)), =(X, fallback))" pl-s-env-ite1)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"if-then-else: cond true binds via then"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-s-env-ite1 "X")))
|
||||
"ok")
|
||||
|
||||
(pl-s-test!
|
||||
"if-then-else: cond false → else"
|
||||
(pl-solve-once!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal ";(->(fail, =(X, ok)), =(X, fallback))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-s-env-ite2 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal ";(->(fail, =(X, ok)), =(X, fallback))" pl-s-env-ite2)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"if-then-else: cond false binds via else"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-s-env-ite2 "X")))
|
||||
"fallback")
|
||||
|
||||
(pl-s-test!
|
||||
"if-then-else: cond commits to first solution (count = 1)"
|
||||
(pl-solve-count!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal ";(->(p(X), =(Y, found)), =(Y, none))" {})
|
||||
(pl-mk-trail))
|
||||
1)
|
||||
|
||||
(pl-s-test!
|
||||
"if-then-else: then can backtrack"
|
||||
(pl-solve-count!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal ";(->(true, p(X)), =(X, none))" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
(pl-s-test!
|
||||
"if-then-else: else can backtrack"
|
||||
(pl-solve-count!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal ";(->(fail, =(X, ignored)), p(X))" {})
|
||||
(pl-mk-trail))
|
||||
2)
|
||||
|
||||
(pl-s-test!
|
||||
"standalone -> with true cond succeeds"
|
||||
(pl-solve-once!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal "->(true, =(X, hi))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"standalone -> with false cond fails"
|
||||
(pl-solve-once!
|
||||
pl-s-db-ite
|
||||
(pl-s-goal "->(fail, =(X, hi))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"write(hello)"
|
||||
(begin
|
||||
(pl-output-clear!)
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "write(hello)" {})
|
||||
(pl-mk-trail))
|
||||
pl-output-buffer)
|
||||
"hello")
|
||||
|
||||
(pl-s-test!
|
||||
"nl outputs newline"
|
||||
(begin
|
||||
(pl-output-clear!)
|
||||
(pl-solve-once! pl-s-empty-db (pl-s-goal "nl" {}) (pl-mk-trail))
|
||||
pl-output-buffer)
|
||||
"\n")
|
||||
|
||||
(pl-s-test!
|
||||
"write(42) outputs digits"
|
||||
(begin
|
||||
(pl-output-clear!)
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "write(42)" {})
|
||||
(pl-mk-trail))
|
||||
pl-output-buffer)
|
||||
"42")
|
||||
|
||||
(pl-s-test!
|
||||
"write(foo(a, b)) formats compound"
|
||||
(begin
|
||||
(pl-output-clear!)
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "write(foo(a, b))" {})
|
||||
(pl-mk-trail))
|
||||
pl-output-buffer)
|
||||
"foo(a, b)")
|
||||
|
||||
(pl-s-test!
|
||||
"write conjunction"
|
||||
(begin
|
||||
(pl-output-clear!)
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "write(a), write(b)" {})
|
||||
(pl-mk-trail))
|
||||
pl-output-buffer)
|
||||
"ab")
|
||||
|
||||
(pl-s-test!
|
||||
"write of bound var walks binding"
|
||||
(begin
|
||||
(pl-output-clear!)
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "=(X, hello), write(X)" {})
|
||||
(pl-mk-trail))
|
||||
pl-output-buffer)
|
||||
"hello")
|
||||
|
||||
(pl-s-test!
|
||||
"write then nl"
|
||||
(begin
|
||||
(pl-output-clear!)
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "write(hi), nl" {})
|
||||
(pl-mk-trail))
|
||||
pl-output-buffer)
|
||||
"hi\n")
|
||||
|
||||
(define pl-s-env-arith1 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(X, 42)" pl-s-env-arith1)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"is(X, 42) binds X to 42"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith1 "X")))
|
||||
42)
|
||||
|
||||
(define pl-s-env-arith2 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(X, +(2, 3))" pl-s-env-arith2)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"is(X, +(2, 3)) binds X to 5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith2 "X")))
|
||||
5)
|
||||
|
||||
(define pl-s-env-arith3 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(X, *(2, 3))" pl-s-env-arith3)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"is(X, *(2, 3)) binds X to 6"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith3 "X")))
|
||||
6)
|
||||
|
||||
(define pl-s-env-arith4 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(X, -(10, 3))" pl-s-env-arith4)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"is(X, -(10, 3)) binds X to 7"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith4 "X")))
|
||||
7)
|
||||
|
||||
(define pl-s-env-arith5 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(X, /(10, 2))" pl-s-env-arith5)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"is(X, /(10, 2)) binds X to 5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith5 "X")))
|
||||
5)
|
||||
|
||||
(define pl-s-env-arith6 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(X, mod(10, 3))" pl-s-env-arith6)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"is(X, mod(10, 3)) binds X to 1"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith6 "X")))
|
||||
1)
|
||||
|
||||
(define pl-s-env-arith7 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(X, abs(-(0, 5)))" pl-s-env-arith7)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"is(X, abs(-(0, 5))) binds X to 5"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith7 "X")))
|
||||
5)
|
||||
|
||||
(define pl-s-env-arith8 {})
|
||||
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(X, +(2, *(3, 4)))" pl-s-env-arith8)
|
||||
(pl-mk-trail))
|
||||
|
||||
(pl-s-test!
|
||||
"is(X, +(2, *(3, 4))) binds X to 14 (nested)"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-s-env-arith8 "X")))
|
||||
14)
|
||||
|
||||
(pl-s-test!
|
||||
"is(5, +(2, 3)) succeeds (LHS num matches)"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(5, +(2, 3))" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-s-test!
|
||||
"is(6, +(2, 3)) fails (LHS num mismatch)"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "is(6, +(2, 3))" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-s-test!
|
||||
"is propagates bound vars on RHS"
|
||||
(pl-solve-once!
|
||||
pl-s-empty-db
|
||||
(pl-s-goal "=(Y, 4), is(X, +(Y, 1)), =(X, 5)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-solve-tests-run! (fn () {:failed pl-s-test-fail :passed pl-s-test-pass :total pl-s-test-count :failures pl-s-test-failures}))
|
||||
@@ -1,273 +0,0 @@
|
||||
;; lib/prolog/tests/string_agg.sx -- sub_atom/5 + aggregate_all/3
|
||||
|
||||
(define pl-sa-test-count 0)
|
||||
(define pl-sa-test-pass 0)
|
||||
(define pl-sa-test-fail 0)
|
||||
(define pl-sa-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-sa-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-sa-test-count (+ pl-sa-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-sa-test-pass (+ pl-sa-test-pass 1))
|
||||
(begin
|
||||
(set! pl-sa-test-fail (+ pl-sa-test-fail 1))
|
||||
(append!
|
||||
pl-sa-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-sa-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-sa-db (pl-mk-db))
|
||||
|
||||
(define
|
||||
pl-sa-num-val
|
||||
(fn (env key) (pl-num-val (pl-walk-deep (dict-get env key)))))
|
||||
|
||||
(define
|
||||
pl-sa-list-to-atoms
|
||||
(fn
|
||||
(t)
|
||||
(let
|
||||
((w (pl-walk-deep t)))
|
||||
(cond
|
||||
((and (pl-atom? w) (= (pl-atom-name w) "[]")) (list))
|
||||
((and (pl-compound? w) (= (pl-fun w) ".") (= (len (pl-args w)) 2))
|
||||
(cons
|
||||
(pl-atom-name (first (pl-args w)))
|
||||
(pl-sa-list-to-atoms (nth (pl-args w) 1))))
|
||||
(true (list))))))
|
||||
|
||||
(define pl-sa-prog-src "member(X, [X|_]). member(X, [_|T]) :- member(X, T).")
|
||||
(pl-db-load! pl-sa-db (pl-parse pl-sa-prog-src))
|
||||
|
||||
;; -- sub_atom/5 --
|
||||
|
||||
(pl-sa-test!
|
||||
"sub_atom ground: sub_atom(abcde,0,3,2,abc)"
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "sub_atom(abcde, 0, 3, 2, abc)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-sa-test!
|
||||
"sub_atom ground: sub_atom(abcde,2,2,1,cd)"
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "sub_atom(abcde, 2, 2, 1, cd)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-sa-test!
|
||||
"sub_atom ground mismatch fails"
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "sub_atom(abcde, 0, 2, 3, cd)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-sa-test!
|
||||
"sub_atom empty sub at start"
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "sub_atom(abcde, 0, 0, 5, '')" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-sa-test!
|
||||
"sub_atom whole string"
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "sub_atom(hello, 0, 5, 0, hello)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-sa-env-b1 {})
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "sub_atom(abcde, B, 2, A, cd)" pl-sa-env-b1)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-test!
|
||||
"sub_atom bound SubAtom gives B=2"
|
||||
(pl-sa-num-val pl-sa-env-b1 "B")
|
||||
2)
|
||||
(pl-sa-test!
|
||||
"sub_atom bound SubAtom gives A=1"
|
||||
(pl-sa-num-val pl-sa-env-b1 "A")
|
||||
1)
|
||||
|
||||
(define pl-sa-env-b2 {})
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "sub_atom(hello, B, L, A, ello)" pl-sa-env-b2)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-test! "sub_atom ello: B=1" (pl-sa-num-val pl-sa-env-b2 "B") 1)
|
||||
(pl-sa-test! "sub_atom ello: L=4" (pl-sa-num-val pl-sa-env-b2 "L") 4)
|
||||
(pl-sa-test! "sub_atom ello: A=0" (pl-sa-num-val pl-sa-env-b2 "A") 0)
|
||||
|
||||
(pl-sa-test!
|
||||
"sub_atom ab: 6 total solutions"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(count, sub_atom(ab, _, _, _, _), N)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-num-val env "N"))
|
||||
6)
|
||||
|
||||
(pl-sa-test!
|
||||
"sub_atom a: 3 total solutions"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(count, sub_atom(a, _, _, _, _), N)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-num-val env "N"))
|
||||
3)
|
||||
|
||||
;; -- aggregate_all/3 --
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all count member [a,b,c] = 3"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(count, member(_, [a,b,c]), N)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-num-val env "N"))
|
||||
3)
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all count fail = 0"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(count, fail, N)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-num-val env "N"))
|
||||
0)
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all count always succeeds"
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(count, fail, _)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(define pl-sa-env-bag1 {})
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(bag(X), member(X, [a,b,c]), L)" pl-sa-env-bag1)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-test!
|
||||
"aggregate_all bag [a,b,c]"
|
||||
(pl-sa-list-to-atoms (dict-get pl-sa-env-bag1 "L"))
|
||||
(list "a" "b" "c"))
|
||||
|
||||
(define pl-sa-env-bag2 {})
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(bag(X), member(X, []), L)" pl-sa-env-bag2)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-test!
|
||||
"aggregate_all bag empty goal = []"
|
||||
(pl-sa-list-to-atoms (dict-get pl-sa-env-bag2 "L"))
|
||||
(list))
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all sum [1,2,3,4] = 10"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(sum(X), member(X, [1,2,3,4]), S)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-num-val env "S"))
|
||||
10)
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all max [3,1,4,1,5,9,2,6] = 9"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(max(X), member(X, [3,1,4,1,5,9,2,6]), M)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-num-val env "M"))
|
||||
9)
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all max empty fails"
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(max(X), member(X, []), M)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all min [3,1,4,1,5,9,2,6] = 1"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(min(X), member(X, [3,1,4,1,5,9,2,6]), M)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-num-val env "M"))
|
||||
1)
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all min empty fails"
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(min(X), member(X, []), M)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-sa-env-set1 {})
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal
|
||||
"aggregate_all(set(X), member(X, [b,a,c,a,b]), S)"
|
||||
pl-sa-env-set1)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-test!
|
||||
"aggregate_all set [b,a,c,a,b] = [a,b,c]"
|
||||
(pl-sa-list-to-atoms (dict-get pl-sa-env-set1 "S"))
|
||||
(list "a" "b" "c"))
|
||||
|
||||
(define pl-sa-env-set2 {})
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(set(X), fail, S)" pl-sa-env-set2)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-test!
|
||||
"aggregate_all set fail = []"
|
||||
(pl-sa-list-to-atoms (dict-get pl-sa-env-set2 "S"))
|
||||
(list))
|
||||
|
||||
(pl-sa-test!
|
||||
"aggregate_all sum empty = 0"
|
||||
(let
|
||||
((env {}))
|
||||
(pl-solve-once!
|
||||
pl-sa-db
|
||||
(pl-sa-goal "aggregate_all(sum(X), fail, S)" env)
|
||||
(pl-mk-trail))
|
||||
(pl-sa-num-val env "S"))
|
||||
0)
|
||||
|
||||
(define pl-string-agg-tests-run! (fn () {:failed pl-sa-test-fail :passed pl-sa-test-pass :total pl-sa-test-count :failures pl-sa-test-failures}))
|
||||
@@ -1,147 +0,0 @@
|
||||
;; lib/prolog/tests/term_inspect.sx — copy_term/2, functor/3, arg/3.
|
||||
|
||||
(define pl-tt-test-count 0)
|
||||
(define pl-tt-test-pass 0)
|
||||
(define pl-tt-test-fail 0)
|
||||
(define pl-tt-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-tt-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(begin
|
||||
(set! pl-tt-test-count (+ pl-tt-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-tt-test-pass (+ pl-tt-test-pass 1))
|
||||
(begin
|
||||
(set! pl-tt-test-fail (+ pl-tt-test-fail 1))
|
||||
(append!
|
||||
pl-tt-test-failures
|
||||
(str name "\n expected: " expected "\n got: " got)))))))
|
||||
|
||||
(define
|
||||
pl-tt-goal
|
||||
(fn
|
||||
(src env)
|
||||
(pl-instantiate (nth (first (pl-parse (str "g :- " src "."))) 2) env)))
|
||||
|
||||
(define pl-tt-db (pl-mk-db))
|
||||
|
||||
;; ── copy_term/2 ──
|
||||
|
||||
(pl-tt-test!
|
||||
"copy_term ground compound succeeds + copy = original"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "copy_term(foo(a, b), X), X = foo(a, b)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-tt-test!
|
||||
"copy_term preserves var aliasing in source"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "copy_term(p(Y, Y), p(A, B)), A = 5, B = 5" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-tt-test!
|
||||
"copy_term distinct vars stay distinct"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "copy_term(p(Y, Y), p(A, B)), A = 5, B = 6" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-tt-env-1 {})
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "copy_term(X, Y), Y = 5" pl-tt-env-1)
|
||||
(pl-mk-trail))
|
||||
(pl-tt-test!
|
||||
"copy_term: binding the copy doesn't bind the source"
|
||||
(pl-var-bound? (dict-get pl-tt-env-1 "X"))
|
||||
false)
|
||||
|
||||
;; ── functor/3 ──
|
||||
|
||||
(define pl-tt-env-2 {})
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "functor(foo(a, b, c), F, N)" pl-tt-env-2)
|
||||
(pl-mk-trail))
|
||||
(pl-tt-test!
|
||||
"functor of compound: F = foo"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-tt-env-2 "F")))
|
||||
"foo")
|
||||
(pl-tt-test!
|
||||
"functor of compound: N = 3"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-tt-env-2 "N")))
|
||||
3)
|
||||
|
||||
(define pl-tt-env-3 {})
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "functor(hello, F, N)" pl-tt-env-3)
|
||||
(pl-mk-trail))
|
||||
(pl-tt-test!
|
||||
"functor of atom: F = hello"
|
||||
(pl-atom-name (pl-walk-deep (dict-get pl-tt-env-3 "F")))
|
||||
"hello")
|
||||
(pl-tt-test!
|
||||
"functor of atom: N = 0"
|
||||
(pl-num-val (pl-walk-deep (dict-get pl-tt-env-3 "N")))
|
||||
0)
|
||||
|
||||
(pl-tt-test!
|
||||
"functor construct compound: T unifies with foo(a, b)"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "functor(T, foo, 2), T = foo(a, b)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-tt-test!
|
||||
"functor construct atom: T = hello"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "functor(T, hello, 0), T = hello" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
;; ── arg/3 ──
|
||||
|
||||
(pl-tt-test!
|
||||
"arg(1, foo(a, b, c), a)"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "arg(1, foo(a, b, c), a)" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-tt-test!
|
||||
"arg(2, foo(a, b, c), X) → X = b"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "arg(2, foo(a, b, c), X), X = b" {})
|
||||
(pl-mk-trail))
|
||||
true)
|
||||
|
||||
(pl-tt-test!
|
||||
"arg out-of-range high fails"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "arg(4, foo(a, b, c), X)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(pl-tt-test!
|
||||
"arg(0, ...) fails (1-indexed)"
|
||||
(pl-solve-once!
|
||||
pl-tt-db
|
||||
(pl-tt-goal "arg(0, foo(a), X)" {})
|
||||
(pl-mk-trail))
|
||||
false)
|
||||
|
||||
(define pl-term-inspect-tests-run! (fn () {:failed pl-tt-test-fail :passed pl-tt-test-pass :total pl-tt-test-count :failures pl-tt-test-failures}))
|
||||
@@ -73,7 +73,10 @@
|
||||
|
||||
(define string->symbol make-symbol)
|
||||
|
||||
(define number->string (fn (n) (str n)))
|
||||
(define number->string
|
||||
(let ((prim-n->s number->string))
|
||||
(fn (n &rest r)
|
||||
(if (= (len r) 0) (str n) (prim-n->s n (first r))))))
|
||||
|
||||
(define
|
||||
string->number
|
||||
|
||||
352
lib/ruby/runtime.sx
Normal file
352
lib/ruby/runtime.sx
Normal file
@@ -0,0 +1,352 @@
|
||||
;; lib/ruby/runtime.sx — Ruby primitives on SX
|
||||
;;
|
||||
;; Provides Ruby-idiomatic wrappers over SX built-ins.
|
||||
;; Primitives used:
|
||||
;; call/cc (core evaluator)
|
||||
;; make-set/set-add!/set-member?/set-remove!/set->list (Phase 18)
|
||||
;; make-regexp/regexp-match/regexp-match-all/... (Phase 19)
|
||||
;; make-bytevector/bytevector-u8-ref/... (Phase 20)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 0. Internal list helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(rb-list-set-nth lst i newval)
|
||||
(letrec
|
||||
((go (fn (ps j) (if (= (len ps) 0) (list) (cons (if (= j i) newval (first ps)) (go (rest ps) (+ j 1)))))))
|
||||
(go lst 0)))
|
||||
|
||||
(define
|
||||
(rb-list-remove-nth lst i)
|
||||
(letrec
|
||||
((go (fn (ps j) (if (= (len ps) 0) (list) (if (= j i) (go (rest ps) (+ j 1)) (cons (first ps) (go (rest ps) (+ j 1))))))))
|
||||
(go lst 0)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Hash (mutable, any-key, dict-backed list-of-pairs)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(rb-hash-new)
|
||||
(let
|
||||
((h (dict)))
|
||||
(dict-set! h "_rb_hash" true)
|
||||
(dict-set! h "_pairs" (list))
|
||||
(dict-set! h "_size" 0)
|
||||
h))
|
||||
|
||||
(define (rb-hash? v) (and (dict? v) (dict-has? v "_rb_hash")))
|
||||
|
||||
(define (rb-hash-size h) (get h "_size"))
|
||||
|
||||
(define
|
||||
(rb-hash-find-idx pairs k)
|
||||
(letrec
|
||||
((go (fn (ps i) (cond ((= (len ps) 0) -1) ((= (first (first ps)) k) i) (else (go (rest ps) (+ i 1)))))))
|
||||
(go pairs 0)))
|
||||
|
||||
(define
|
||||
(rb-hash-at h k)
|
||||
(letrec
|
||||
((go (fn (ps) (if (= (len ps) 0) nil (if (= (first (first ps)) k) (nth (first ps) 1) (go (rest ps)))))))
|
||||
(go (get h "_pairs"))))
|
||||
|
||||
(define
|
||||
(rb-hash-at-or h k default)
|
||||
(if (rb-hash-has-key? h k) (rb-hash-at h k) default))
|
||||
|
||||
(define
|
||||
(rb-hash-at-put! h k v)
|
||||
(let
|
||||
((pairs (get h "_pairs")) (idx (rb-hash-find-idx (get h "_pairs") k)))
|
||||
(if
|
||||
(= idx -1)
|
||||
(begin
|
||||
(dict-set! h "_pairs" (append pairs (list (list k v))))
|
||||
(dict-set! h "_size" (+ (get h "_size") 1)))
|
||||
(dict-set! h "_pairs" (rb-list-set-nth pairs idx (list k v)))))
|
||||
h)
|
||||
|
||||
(define
|
||||
(rb-hash-has-key? h k)
|
||||
(not (= (rb-hash-find-idx (get h "_pairs") k) -1)))
|
||||
|
||||
(define
|
||||
(rb-hash-delete! h k)
|
||||
(let
|
||||
((idx (rb-hash-find-idx (get h "_pairs") k)))
|
||||
(when
|
||||
(not (= idx -1))
|
||||
(dict-set! h "_pairs" (rb-list-remove-nth (get h "_pairs") idx))
|
||||
(dict-set! h "_size" (- (get h "_size") 1))))
|
||||
h)
|
||||
|
||||
(define (rb-hash-keys h) (map first (get h "_pairs")))
|
||||
|
||||
(define
|
||||
(rb-hash-values h)
|
||||
(map (fn (p) (nth p 1)) (get h "_pairs")))
|
||||
|
||||
(define
|
||||
(rb-hash-each h callback)
|
||||
(for-each
|
||||
(fn (p) (callback (first p) (nth p 1)))
|
||||
(get h "_pairs")))
|
||||
|
||||
(define (rb-hash->list h) (get h "_pairs"))
|
||||
|
||||
(define
|
||||
(rb-list->hash pairs)
|
||||
(let
|
||||
((h (rb-hash-new)))
|
||||
(for-each
|
||||
(fn (p) (rb-hash-at-put! h (first p) (nth p 1)))
|
||||
pairs)
|
||||
h))
|
||||
|
||||
(define
|
||||
(rb-hash-merge h1 h2)
|
||||
(let
|
||||
((result (rb-hash-new)))
|
||||
(for-each
|
||||
(fn (p) (rb-hash-at-put! result (first p) (nth p 1)))
|
||||
(get h1 "_pairs"))
|
||||
(for-each
|
||||
(fn (p) (rb-hash-at-put! result (first p) (nth p 1)))
|
||||
(get h2 "_pairs"))
|
||||
result))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Set (uniqueness collection backed by SX make-set)
|
||||
;; Note: set-member?/set-add!/set-remove! take (set item) order.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(rb-set-new)
|
||||
(let
|
||||
((s (dict)))
|
||||
(dict-set! s "_rb_set" true)
|
||||
(dict-set! s "_set" (make-set))
|
||||
(dict-set! s "_size" 0)
|
||||
s))
|
||||
|
||||
(define (rb-set? v) (and (dict? v) (dict-has? v "_rb_set")))
|
||||
|
||||
(define (rb-set-size s) (get s "_size"))
|
||||
|
||||
(define
|
||||
(rb-set-add! s v)
|
||||
(let
|
||||
((sx (get s "_set")))
|
||||
(when
|
||||
(not (set-member? sx v))
|
||||
(set-add! sx v)
|
||||
(dict-set! s "_size" (+ (get s "_size") 1))))
|
||||
s)
|
||||
|
||||
(define (rb-set-include? s v) (set-member? (get s "_set") v))
|
||||
|
||||
(define
|
||||
(rb-set-delete! s v)
|
||||
(let
|
||||
((sx (get s "_set")))
|
||||
(when
|
||||
(set-member? sx v)
|
||||
(set-remove! sx v)
|
||||
(dict-set! s "_size" (- (get s "_size") 1))))
|
||||
s)
|
||||
|
||||
(define (rb-set->list s) (set->list (get s "_set")))
|
||||
|
||||
(define
|
||||
(rb-set-each s callback)
|
||||
(for-each callback (set->list (get s "_set"))))
|
||||
|
||||
(define
|
||||
(rb-set-union s1 s2)
|
||||
(let
|
||||
((result (rb-set-new)))
|
||||
(for-each (fn (v) (rb-set-add! result v)) (rb-set->list s1))
|
||||
(for-each (fn (v) (rb-set-add! result v)) (rb-set->list s2))
|
||||
result))
|
||||
|
||||
(define
|
||||
(rb-set-intersection s1 s2)
|
||||
(let
|
||||
((result (rb-set-new)))
|
||||
(for-each
|
||||
(fn (v) (when (rb-set-include? s2 v) (rb-set-add! result v)))
|
||||
(rb-set->list s1))
|
||||
result))
|
||||
|
||||
(define
|
||||
(rb-set-difference s1 s2)
|
||||
(let
|
||||
((result (rb-set-new)))
|
||||
(for-each
|
||||
(fn (v) (when (not (rb-set-include? s2 v)) (rb-set-add! result v)))
|
||||
(rb-set->list s1))
|
||||
result))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Regexp (thin wrappers over Phase-19 make-regexp primitives)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(rb-regexp-new pattern flags)
|
||||
(make-regexp pattern (if (= flags nil) "" flags)))
|
||||
|
||||
(define (rb-regexp? v) (regexp? v))
|
||||
|
||||
(define (rb-regexp-match rx str) (regexp-match rx str))
|
||||
|
||||
(define (rb-regexp-match-all rx str) (regexp-match-all rx str))
|
||||
|
||||
(define (rb-regexp-match? rx str) (not (= (regexp-match rx str) nil)))
|
||||
|
||||
(define
|
||||
(rb-regexp-replace rx str replacement)
|
||||
(regexp-replace rx str replacement))
|
||||
|
||||
(define
|
||||
(rb-regexp-replace-all rx str replacement)
|
||||
(regexp-replace-all rx str replacement))
|
||||
|
||||
(define (rb-regexp-split rx str) (regexp-split rx str))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. StringIO (write buffer + char-by-char read after rewind)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(rb-string-io-new)
|
||||
(let
|
||||
((io (dict)))
|
||||
(dict-set! io "_rb_string_io" true)
|
||||
(dict-set! io "_buf" "")
|
||||
(dict-set! io "_chars" (list))
|
||||
(dict-set! io "_pos" 0)
|
||||
io))
|
||||
|
||||
(define (rb-string-io? v) (and (dict? v) (dict-has? v "_rb_string_io")))
|
||||
|
||||
(define
|
||||
(rb-string-io-write! io s)
|
||||
(dict-set! io "_buf" (str (get io "_buf") s))
|
||||
io)
|
||||
|
||||
(define (rb-string-io-string io) (get io "_buf"))
|
||||
|
||||
(define
|
||||
(rb-string-io-rewind! io)
|
||||
(dict-set! io "_chars" (string->list (get io "_buf")))
|
||||
(dict-set! io "_pos" 0)
|
||||
io)
|
||||
|
||||
(define
|
||||
(rb-string-io-eof? io)
|
||||
(>= (get io "_pos") (len (get io "_chars"))))
|
||||
|
||||
(define
|
||||
(rb-string-io-read-char io)
|
||||
(if
|
||||
(rb-string-io-eof? io)
|
||||
nil
|
||||
(let
|
||||
((c (nth (get io "_chars") (get io "_pos"))))
|
||||
(dict-set! io "_pos" (+ (get io "_pos") 1))
|
||||
c)))
|
||||
|
||||
(define
|
||||
(rb-string-io-read io)
|
||||
(letrec
|
||||
((go (fn (acc) (let ((c (rb-string-io-read-char io))) (if (= c nil) (list->string (reverse acc)) (go (cons c acc)))))))
|
||||
(go (list))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Bytevectors (thin wrappers over Phase-20 bytevector primitives)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(rb-bytes-new n fill)
|
||||
(make-bytevector n (if (= fill nil) 0 fill)))
|
||||
|
||||
(define (rb-bytes? v) (bytevector? v))
|
||||
|
||||
(define (rb-bytes-length v) (bytevector-length v))
|
||||
|
||||
(define (rb-bytes-get v i) (bytevector-u8-ref v i))
|
||||
|
||||
(define (rb-bytes-set! v i b) (bytevector-u8-set! v i b) v)
|
||||
|
||||
(define (rb-bytes-copy v) (bytevector-copy v))
|
||||
|
||||
(define (rb-bytes-append v1 v2) (bytevector-append v1 v2))
|
||||
|
||||
(define (rb-bytes-to-string v) (utf8->string v))
|
||||
|
||||
(define (rb-bytes-from-string s) (string->utf8 s))
|
||||
|
||||
(define (rb-bytes->list v) (bytevector->list v))
|
||||
|
||||
(define (rb-list->bytes lst) (list->bytevector lst))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. Fiber (call/cc coroutines)
|
||||
;; Body wrapped so completion always routes through _resumer, ensuring
|
||||
;; rb-fiber-resume always returns via the captured continuation.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define rb-current-fiber nil)
|
||||
|
||||
(define
|
||||
(rb-fiber-new body)
|
||||
(let
|
||||
((f (dict)))
|
||||
(dict-set! f "_rb_fiber" true)
|
||||
(dict-set! f "_state" "new")
|
||||
(dict-set! f "_cont" nil)
|
||||
(dict-set! f "_resumer" nil)
|
||||
(dict-set! f "_parent" nil)
|
||||
(dict-set!
|
||||
f
|
||||
"_body"
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((result (body)))
|
||||
(dict-set! f "_state" "dead")
|
||||
(set! rb-current-fiber (get f "_parent"))
|
||||
((get f "_resumer") result))))
|
||||
f))
|
||||
|
||||
(define (rb-fiber? v) (and (dict? v) (dict-has? v "_rb_fiber")))
|
||||
|
||||
(define (rb-fiber-alive? f) (not (= (get f "_state") "dead")))
|
||||
|
||||
(define
|
||||
(rb-fiber-yield val)
|
||||
(call/cc
|
||||
(fn
|
||||
(resume-k)
|
||||
(let
|
||||
((cur rb-current-fiber))
|
||||
(dict-set! cur "_cont" resume-k)
|
||||
(dict-set! cur "_state" "suspended")
|
||||
(set! rb-current-fiber (get cur "_parent"))
|
||||
((get cur "_resumer") val)))))
|
||||
|
||||
(define
|
||||
(rb-fiber-resume f)
|
||||
(call/cc
|
||||
(fn
|
||||
(return-k)
|
||||
(dict-set! f "_parent" rb-current-fiber)
|
||||
(dict-set! f "_resumer" return-k)
|
||||
(set! rb-current-fiber f)
|
||||
(dict-set! f "_state" "running")
|
||||
(if
|
||||
(= (get f "_cont") nil)
|
||||
((get f "_body"))
|
||||
((get f "_cont") nil)))))
|
||||
62
lib/ruby/test.sh
Executable file
62
lib/ruby/test.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/ruby/test.sh — smoke-test the Ruby runtime layer.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "lib/ruby/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/ruby/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(list rb-test-pass rb-test-fail)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}')
|
||||
if [ -z "$LINE" ]; then
|
||||
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 3 //; s/\)$//')
|
||||
fi
|
||||
if [ -z "$LINE" ]; then
|
||||
echo "ERROR: could not extract summary"
|
||||
echo "$OUTPUT" | tail -20
|
||||
exit 1
|
||||
fi
|
||||
|
||||
P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/')
|
||||
F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/')
|
||||
TOTAL=$((P + F))
|
||||
|
||||
if [ "$F" -eq 0 ]; then
|
||||
echo "ok $P/$TOTAL lib/ruby tests passed"
|
||||
else
|
||||
echo "FAIL $P/$TOTAL passed, $F failed"
|
||||
TMPFILE2=$(mktemp)
|
||||
cat > "$TMPFILE2" << 'EPOCHS2'
|
||||
(epoch 1)
|
||||
(load "lib/ruby/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/ruby/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(map (fn (f) (get f \"name\")) rb-test-fails)")
|
||||
EPOCHS2
|
||||
FAILS=$(timeout 60 "$SX_SERVER" < "$TMPFILE2" 2>/dev/null | grep -E '^\(ok 3 ' || true)
|
||||
echo " Failed: $FAILS"
|
||||
rm -f "$TMPFILE2"
|
||||
fi
|
||||
|
||||
[ "$F" -eq 0 ]
|
||||
207
lib/ruby/tests/runtime.sx
Normal file
207
lib/ruby/tests/runtime.sx
Normal file
@@ -0,0 +1,207 @@
|
||||
;; lib/ruby/tests/runtime.sx — Tests for lib/ruby/runtime.sx
|
||||
|
||||
(define rb-test-pass 0)
|
||||
(define rb-test-fail 0)
|
||||
(define rb-test-fails (list))
|
||||
|
||||
(define
|
||||
(rb-test name got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! rb-test-pass (+ rb-test-pass 1))
|
||||
(begin
|
||||
(set! rb-test-fail (+ rb-test-fail 1))
|
||||
(set! rb-test-fails (append rb-test-fails (list {:got got :expected expected :name name}))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Hash
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define h1 (rb-hash-new))
|
||||
(rb-test "hash? new" (rb-hash? h1) true)
|
||||
(rb-test "hash? non-hash" (rb-hash? 42) false)
|
||||
(rb-test "hash size empty" (rb-hash-size h1) 0)
|
||||
(rb-hash-at-put! h1 "a" 1)
|
||||
(rb-hash-at-put! h1 "b" 2)
|
||||
(rb-hash-at-put! h1 "c" 3)
|
||||
(rb-test "hash at a" (rb-hash-at h1 "a") 1)
|
||||
(rb-test "hash at b" (rb-hash-at h1 "b") 2)
|
||||
(rb-test "hash at missing" (rb-hash-at h1 "z") nil)
|
||||
(rb-test "hash at-or default" (rb-hash-at-or h1 "z" 99) 99)
|
||||
(rb-test "hash has-key yes" (rb-hash-has-key? h1 "a") true)
|
||||
(rb-test "hash has-key no" (rb-hash-has-key? h1 "z") false)
|
||||
(rb-test "hash size after inserts" (rb-hash-size h1) 3)
|
||||
(rb-hash-at-put! h1 "a" 10)
|
||||
(rb-test "hash at-put update" (rb-hash-at h1 "a") 10)
|
||||
(rb-test "hash size unchanged after update" (rb-hash-size h1) 3)
|
||||
(rb-hash-delete! h1 "b")
|
||||
(rb-test "hash delete" (rb-hash-has-key? h1 "b") false)
|
||||
(rb-test "hash size after delete" (rb-hash-size h1) 2)
|
||||
(rb-test "hash keys" (rb-hash-keys h1) (list "a" "c"))
|
||||
(rb-test "hash values" (rb-hash-values h1) (list 10 3))
|
||||
|
||||
(define
|
||||
h2
|
||||
(rb-list->hash (list (list "x" 7) (list "y" 8))))
|
||||
(rb-test "list->hash x" (rb-hash-at h2 "x") 7)
|
||||
(rb-test "list->hash y" (rb-hash-at h2 "y") 8)
|
||||
|
||||
(define h3 (rb-hash-merge h1 h2))
|
||||
(rb-test "hash-merge a" (rb-hash-at h3 "a") 10)
|
||||
(rb-test "hash-merge x" (rb-hash-at h3 "x") 7)
|
||||
(rb-test "hash-merge size" (rb-hash-size h3) 4)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Set
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define s1 (rb-set-new))
|
||||
(rb-test "set? new" (rb-set? s1) true)
|
||||
(rb-test "set? non-set" (rb-set? "hello") false)
|
||||
(rb-test "set size empty" (rb-set-size s1) 0)
|
||||
(rb-set-add! s1 1)
|
||||
(rb-set-add! s1 2)
|
||||
(rb-set-add! s1 3)
|
||||
(rb-set-add! s1 2)
|
||||
(rb-test "set include yes" (rb-set-include? s1 1) true)
|
||||
(rb-test "set include no" (rb-set-include? s1 9) false)
|
||||
(rb-test "set size dedup" (rb-set-size s1) 3)
|
||||
(rb-set-delete! s1 2)
|
||||
(rb-test "set delete" (rb-set-include? s1 2) false)
|
||||
(rb-test "set size after delete" (rb-set-size s1) 2)
|
||||
|
||||
(define s2 (rb-set-new))
|
||||
(rb-set-add! s2 2)
|
||||
(rb-set-add! s2 3)
|
||||
(rb-set-add! s2 4)
|
||||
|
||||
(define su (rb-set-union s1 s2))
|
||||
(rb-test "set union includes 1" (rb-set-include? su 1) true)
|
||||
(rb-test "set union includes 4" (rb-set-include? su 4) true)
|
||||
(rb-test "set union size" (rb-set-size su) 4)
|
||||
|
||||
(define si (rb-set-intersection s1 s2))
|
||||
(rb-test "set intersection includes 3" (rb-set-include? si 3) true)
|
||||
(rb-test "set intersection excludes 1" (rb-set-include? si 1) false)
|
||||
(rb-test "set intersection size" (rb-set-size si) 1)
|
||||
|
||||
(define sd (rb-set-difference s1 s2))
|
||||
(rb-test "set difference includes 1" (rb-set-include? sd 1) true)
|
||||
(rb-test "set difference excludes 3" (rb-set-include? sd 3) false)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Regexp
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define rx1 (rb-regexp-new "hel+" ""))
|
||||
(rb-test "regexp?" (rb-regexp? rx1) true)
|
||||
(rb-test "regexp match? yes" (rb-regexp-match? rx1 "say hello") true)
|
||||
(rb-test "regexp match? no" (rb-regexp-match? rx1 "goodbye") false)
|
||||
|
||||
(define m1 (rb-regexp-match rx1 "say hello world"))
|
||||
(rb-test "regexp match :match" (get m1 "match") "hell")
|
||||
|
||||
(define rx2 (rb-regexp-new "[0-9]+" ""))
|
||||
(define all (rb-regexp-match-all rx2 "a1b22c333"))
|
||||
(rb-test "regexp match-all count" (len all) 3)
|
||||
(rb-test "regexp match-all first" (get (first all) "match") "1")
|
||||
|
||||
(rb-test "regexp replace" (rb-regexp-replace rx2 "a1b2" "N") "aNb2")
|
||||
(rb-test "regexp replace-all" (rb-regexp-replace-all rx2 "a1b2" "N") "aNbN")
|
||||
(rb-test
|
||||
"regexp split"
|
||||
(rb-regexp-split (rb-regexp-new "," "") "a,b,c")
|
||||
(list "a" "b" "c"))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. StringIO
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define sio1 (rb-string-io-new))
|
||||
(rb-test "string-io?" (rb-string-io? sio1) true)
|
||||
(rb-string-io-write! sio1 "hello")
|
||||
(rb-string-io-write! sio1 " world")
|
||||
(rb-test "string-io string" (rb-string-io-string sio1) "hello world")
|
||||
(rb-string-io-rewind! sio1)
|
||||
(rb-test "string-io eof? no" (rb-string-io-eof? sio1) false)
|
||||
(define ch1 (rb-string-io-read-char sio1))
|
||||
(define ch2 (rb-string-io-read-char sio1))
|
||||
;; Compare char codepoints since = uses reference equality for chars
|
||||
(rb-test "string-io read-char h" (char->integer ch1) 104)
|
||||
(rb-test "string-io read-char e" (char->integer ch2) 101)
|
||||
(rb-test "string-io read rest" (rb-string-io-read sio1) "llo world")
|
||||
(rb-test "string-io eof? yes" (rb-string-io-eof? sio1) true)
|
||||
(rb-test "string-io read at eof" (rb-string-io-read sio1) "")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Bytevectors
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define bv1 (rb-bytes-new 4 0))
|
||||
(rb-test "bytes?" (rb-bytes? bv1) true)
|
||||
(rb-test "bytes length" (rb-bytes-length bv1) 4)
|
||||
(rb-test "bytes get zero" (rb-bytes-get bv1 0) 0)
|
||||
(rb-bytes-set! bv1 0 65)
|
||||
(rb-bytes-set! bv1 1 66)
|
||||
(rb-test "bytes get A" (rb-bytes-get bv1 0) 65)
|
||||
(rb-test "bytes get B" (rb-bytes-get bv1 1) 66)
|
||||
(define bv2 (rb-bytes-from-string "hi"))
|
||||
(rb-test "bytes from-string length" (rb-bytes-length bv2) 2)
|
||||
(rb-test "bytes to-string" (rb-bytes-to-string bv2) "hi")
|
||||
(define
|
||||
bv3
|
||||
(rb-bytes-append (rb-bytes-from-string "foo") (rb-bytes-from-string "bar")))
|
||||
(rb-test "bytes append" (rb-bytes-to-string bv3) "foobar")
|
||||
(rb-test
|
||||
"bytes->list"
|
||||
(rb-bytes->list (rb-bytes-from-string "AB"))
|
||||
(list 65 66))
|
||||
(rb-test
|
||||
"list->bytes"
|
||||
(rb-bytes-to-string (rb-list->bytes (list 72 105)))
|
||||
"Hi")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. Fiber
|
||||
;; Note: rb-fiber-yield from inside a letrec (JIT-compiled) doesn't
|
||||
;; properly escape via call/cc continuations. Use top-level helper fns
|
||||
;; or explicit sequential yields instead of letrec-bound recursion.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
fib1
|
||||
(rb-fiber-new
|
||||
(fn
|
||||
()
|
||||
(rb-fiber-yield 10)
|
||||
(rb-fiber-yield 20)
|
||||
30)))
|
||||
|
||||
(rb-test "fiber?" (rb-fiber? fib1) true)
|
||||
(rb-test "fiber alive? before" (rb-fiber-alive? fib1) true)
|
||||
(define fr1 (rb-fiber-resume fib1))
|
||||
(rb-test "fiber resume 1" fr1 10)
|
||||
(rb-test "fiber alive? mid" (rb-fiber-alive? fib1) true)
|
||||
(define fr2 (rb-fiber-resume fib1))
|
||||
(rb-test "fiber resume 2" fr2 20)
|
||||
(define fr3 (rb-fiber-resume fib1))
|
||||
(rb-test "fiber resume 3 (completion)" fr3 30)
|
||||
(rb-test "fiber alive? dead" (rb-fiber-alive? fib1) false)
|
||||
|
||||
;; Loop via a top-level helper (avoid letrec — see note above)
|
||||
(define
|
||||
(rb-fiber-loop-helper i)
|
||||
(when
|
||||
(<= i 3)
|
||||
(rb-fiber-yield i)
|
||||
(rb-fiber-loop-helper (+ i 1))))
|
||||
|
||||
(define
|
||||
fib2
|
||||
(rb-fiber-new (fn () (rb-fiber-loop-helper 1) "done")))
|
||||
|
||||
(rb-test "fiber loop resume 1" (rb-fiber-resume fib2) 1)
|
||||
(rb-test "fiber loop resume 2" (rb-fiber-resume fib2) 2)
|
||||
(rb-test "fiber loop resume 3" (rb-fiber-resume fib2) 3)
|
||||
(rb-test "fiber loop resume done" (rb-fiber-resume fib2) "done")
|
||||
(rb-test "fiber loop dead" (rb-fiber-alive? fib2) false)
|
||||
370
lib/smalltalk/runtime.sx
Normal file
370
lib/smalltalk/runtime.sx
Normal file
@@ -0,0 +1,370 @@
|
||||
;; lib/smalltalk/runtime.sx — Smalltalk primitives on SX
|
||||
;;
|
||||
;; Provides Smalltalk-idiomatic wrappers over SX built-ins.
|
||||
;; Primitives used:
|
||||
;; make-set/set-add!/set-member?/set-remove!/set->list (Phase 18)
|
||||
;; char->integer/integer->char/list->string (Phase 5)
|
||||
;; bitwise-and/or/xor/not/arithmetic-shift (Phase 7)
|
||||
;; gcd/lcm/quotient/remainder/modulo (Phase 15)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 0. Internal list helpers (used by Array and Dictionary)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(st-list-set-nth lst i newval)
|
||||
(letrec
|
||||
((go (fn (ps j) (if (= (len ps) 0) (list) (cons (if (= j i) newval (first ps)) (go (rest ps) (+ j 1)))))))
|
||||
(go lst 0)))
|
||||
|
||||
(define
|
||||
(st-list-remove-nth lst i)
|
||||
(letrec
|
||||
((go (fn (ps j) (if (= (len ps) 0) (list) (if (= j i) (go (rest ps) (+ j 1)) (cons (first ps) (go (rest ps) (+ j 1))))))))
|
||||
(go lst 0)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Numeric helpers
|
||||
;; Thin wrappers or direct aliases for Smalltalk Number protocol.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (st-abs x) (abs x))
|
||||
(define (st-max a b) (if (> a b) a b))
|
||||
(define (st-min a b) (if (< a b) a b))
|
||||
(define (st-gcd a b) (gcd a b))
|
||||
(define (st-lcm a b) (lcm a b))
|
||||
(define (st-quo a b) (quotient a b))
|
||||
(define (st-rem a b) (remainder a b))
|
||||
(define (st-mod a b) (modulo a b))
|
||||
(define (st-even? n) (= (remainder n 2) 0))
|
||||
(define (st-odd? n) (not (st-even? n)))
|
||||
(define (st-sqrt x) (sqrt x))
|
||||
(define (st-floor x) (floor x))
|
||||
(define (st-ceiling x) (ceil x))
|
||||
(define (st-truncated x) (truncate x))
|
||||
(define (st-rounded x) (round x))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Character
|
||||
;; Smalltalk $A = char 65. Operations mirror Character class.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (st-char-value c) (char->integer c))
|
||||
(define (st-char-from-int n) (integer->char n))
|
||||
(define (st-char? v) (= (type-of v) "char"))
|
||||
|
||||
(define
|
||||
(st-char-is-letter? c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(or
|
||||
(and (>= n 65) (<= n 90))
|
||||
(and (>= n 97) (<= n 122)))))
|
||||
|
||||
(define
|
||||
(st-char-is-digit? c)
|
||||
(let ((n (char->integer c))) (and (>= n 48) (<= n 57))))
|
||||
|
||||
(define
|
||||
(st-char-is-uppercase? c)
|
||||
(let ((n (char->integer c))) (and (>= n 65) (<= n 90))))
|
||||
|
||||
(define
|
||||
(st-char-is-lowercase? c)
|
||||
(let ((n (char->integer c))) (and (>= n 97) (<= n 122))))
|
||||
|
||||
(define
|
||||
(st-char-is-separator? c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(or
|
||||
(= n 32)
|
||||
(= n 9)
|
||||
(= n 10)
|
||||
(= n 13))))
|
||||
|
||||
(define
|
||||
(st-char-as-uppercase c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(if
|
||||
(and (>= n 97) (<= n 122))
|
||||
(integer->char (- n 32))
|
||||
c)))
|
||||
|
||||
(define
|
||||
(st-char-as-lowercase c)
|
||||
(let
|
||||
((n (char->integer c)))
|
||||
(if
|
||||
(and (>= n 65) (<= n 90))
|
||||
(integer->char (+ n 32))
|
||||
c)))
|
||||
|
||||
(define (st-char-digit-value c) (- (char->integer c) 48))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Array (1-indexed, mutable, fixed-size)
|
||||
;; Backed as {:__st_array__ true :size N "1" v1 "2" v2 ...}
|
||||
;; Unset elements read as nil.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(st-array-new n)
|
||||
(let
|
||||
((a (dict)))
|
||||
(dict-set! a "__st_array__" true)
|
||||
(dict-set! a "size" n)
|
||||
a))
|
||||
|
||||
(define (st-array? v) (and (dict? v) (dict-has? v "__st_array__")))
|
||||
|
||||
(define (st-array-size a) (get a "size"))
|
||||
|
||||
(define
|
||||
(st-array-at a i)
|
||||
(let ((v (get a (str i)))) (if (= v nil) nil v)))
|
||||
|
||||
(define (st-array-at-put! a i v) (dict-set! a (str i) v) a)
|
||||
|
||||
(define
|
||||
(st-array-do a fn)
|
||||
(letrec
|
||||
((go (fn (i) (when (<= i (st-array-size a)) (fn (st-array-at a i)) (go (+ i 1))))))
|
||||
(go 1)))
|
||||
|
||||
(define
|
||||
(st-array->list a)
|
||||
(letrec
|
||||
((go (fn (i acc) (if (< i 1) acc (go (- i 1) (cons (st-array-at a i) acc))))))
|
||||
(go (st-array-size a) (list))))
|
||||
|
||||
(define
|
||||
(st-list->array xs)
|
||||
(let
|
||||
((a (st-array-new (len xs))))
|
||||
(letrec
|
||||
((go (fn (ys i) (when (> (len ys) 0) (st-array-at-put! a i (first ys)) (go (rest ys) (+ i 1))))))
|
||||
(go xs 1))
|
||||
a))
|
||||
|
||||
(define
|
||||
(st-array-copy-from-to a start stop)
|
||||
(let
|
||||
((result (st-array-new (- stop start -1))))
|
||||
(letrec
|
||||
((go (fn (i j) (when (<= i stop) (st-array-at-put! result j (st-array-at a i)) (go (+ i 1) (+ j 1))))))
|
||||
(go start 1))
|
||||
result))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Dictionary (hash map with any key via linear scan)
|
||||
;; {:__st_dict__ true :size N :_pairs ((key val) ...)}
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(st-dict-new)
|
||||
(let
|
||||
((d (dict)))
|
||||
(dict-set! d "__st_dict__" true)
|
||||
(dict-set! d "size" 0)
|
||||
(dict-set! d "_pairs" (list))
|
||||
d))
|
||||
|
||||
(define (st-dict? v) (and (dict? v) (dict-has? v "__st_dict__")))
|
||||
|
||||
(define (st-dict-size d) (get d "size"))
|
||||
|
||||
(define
|
||||
(st-dict-find-idx pairs k)
|
||||
(letrec
|
||||
((go (fn (ps i) (cond ((= (len ps) 0) -1) ((= (first (first ps)) k) i) (else (go (rest ps) (+ i 1)))))))
|
||||
(go pairs 0)))
|
||||
|
||||
(define
|
||||
(st-dict-at d k)
|
||||
(letrec
|
||||
((go (fn (ps) (if (= (len ps) 0) nil (if (= (first (first ps)) k) (nth (first ps) 1) (go (rest ps)))))))
|
||||
(go (get d "_pairs"))))
|
||||
|
||||
(define
|
||||
(st-dict-at-put! d k v)
|
||||
(let
|
||||
((pairs (get d "_pairs")) (idx (st-dict-find-idx (get d "_pairs") k)))
|
||||
(if
|
||||
(= idx -1)
|
||||
(begin
|
||||
(dict-set! d "_pairs" (append pairs (list (list k v))))
|
||||
(dict-set! d "size" (+ (get d "size") 1)))
|
||||
(dict-set! d "_pairs" (st-list-set-nth pairs idx (list k v)))))
|
||||
d)
|
||||
|
||||
(define
|
||||
(st-dict-includes-key? d k)
|
||||
(not (= (st-dict-find-idx (get d "_pairs") k) -1)))
|
||||
|
||||
(define
|
||||
(st-dict-at-default d k def)
|
||||
(if (st-dict-includes-key? d k) (st-dict-at d k) def))
|
||||
|
||||
(define
|
||||
(st-dict-remove-key! d k)
|
||||
(let
|
||||
((idx (st-dict-find-idx (get d "_pairs") k)))
|
||||
(when
|
||||
(not (= idx -1))
|
||||
(dict-set! d "_pairs" (st-list-remove-nth (get d "_pairs") idx))
|
||||
(dict-set! d "size" (- (get d "size") 1))))
|
||||
d)
|
||||
|
||||
(define (st-dict-keys d) (map first (get d "_pairs")))
|
||||
(define
|
||||
(st-dict-values d)
|
||||
(map (fn (p) (nth p 1)) (get d "_pairs")))
|
||||
|
||||
(define
|
||||
(st-dict-do d fn)
|
||||
(for-each (fn (p) (fn (nth p 1))) (get d "_pairs")))
|
||||
|
||||
(define
|
||||
(st-dict-do-associations d fn)
|
||||
(for-each (fn (p) (fn (first p) (nth p 1))) (get d "_pairs")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Set (uniqueness via SX make-set)
|
||||
;; Note: set-member?/set-add!/set-remove! take (set item) order.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(st-set-new)
|
||||
(let
|
||||
((s (dict)))
|
||||
(dict-set! s "__st_set__" true)
|
||||
(dict-set! s "size" 0)
|
||||
(dict-set! s "_set" (make-set))
|
||||
s))
|
||||
|
||||
(define (st-set? v) (and (dict? v) (dict-has? v "__st_set__")))
|
||||
|
||||
(define (st-set-size s) (get s "size"))
|
||||
|
||||
(define
|
||||
(st-set-add! s v)
|
||||
(let
|
||||
((sx (get s "_set")))
|
||||
(when
|
||||
(not (set-member? sx v))
|
||||
(set-add! sx v)
|
||||
(dict-set! s "size" (+ (get s "size") 1))))
|
||||
s)
|
||||
|
||||
(define (st-set-includes? s v) (set-member? (get s "_set") v))
|
||||
|
||||
(define
|
||||
(st-set-remove! s v)
|
||||
(let
|
||||
((sx (get s "_set")))
|
||||
(when
|
||||
(set-member? sx v)
|
||||
(set-remove! sx v)
|
||||
(dict-set! s "size" (- (get s "size") 1))))
|
||||
s)
|
||||
|
||||
(define (st-set->list s) (set->list (get s "_set")))
|
||||
|
||||
(define (st-set-do s fn) (for-each fn (st-set->list s)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. String / Stream utilities
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Join list of strings with separator
|
||||
(define
|
||||
(st-join-strings strs sep)
|
||||
(if
|
||||
(= (len strs) 0)
|
||||
""
|
||||
(letrec
|
||||
((go (fn (ss acc) (if (= (len ss) 0) acc (go (rest ss) (str acc sep (first ss)))))))
|
||||
(go (rest strs) (first strs)))))
|
||||
|
||||
;; printString — Smalltalk textual representation
|
||||
(define
|
||||
(st-print-string v)
|
||||
(cond
|
||||
((= v nil) "nil")
|
||||
((= v true) "true")
|
||||
((= v false) "false")
|
||||
((= (type-of v) "number") (str v))
|
||||
((= (type-of v) "string") (str "'" v "'"))
|
||||
((= (type-of v) "symbol") (str "#" (str v)))
|
||||
((= (type-of v) "char") (str "$" (list->string (list v))))
|
||||
((= (type-of v) "list")
|
||||
(str "(" (st-join-strings (map st-print-string v) " ") ")"))
|
||||
((st-array? v)
|
||||
(str
|
||||
"(#("
|
||||
(st-join-strings (map st-print-string (st-array->list v)) " ")
|
||||
"))"))
|
||||
(else (str v))))
|
||||
|
||||
;; WriteStream — accumulates strings/chars to a buffer
|
||||
(define
|
||||
(st-write-stream-new)
|
||||
(let
|
||||
((ws (dict)))
|
||||
(dict-set! ws "__st_ws__" true)
|
||||
(dict-set! ws "contents" "")
|
||||
ws))
|
||||
|
||||
(define (st-write-stream? v) (and (dict? v) (dict-has? v "__st_ws__")))
|
||||
|
||||
(define
|
||||
(st-write-stream-put-string! ws s)
|
||||
(dict-set! ws "contents" (str (get ws "contents") s))
|
||||
ws)
|
||||
|
||||
(define
|
||||
(st-write-stream-next-put! ws c)
|
||||
(st-write-stream-put-string! ws (list->string (list c))))
|
||||
|
||||
(define
|
||||
(st-write-stream-print! ws v)
|
||||
(st-write-stream-put-string! ws (st-print-string v)))
|
||||
|
||||
(define (st-write-stream-contents ws) (get ws "contents"))
|
||||
|
||||
;; ReadStream — reads characters from a string one at a time
|
||||
(define
|
||||
(st-read-stream-new s)
|
||||
(let
|
||||
((rs (dict)))
|
||||
(dict-set! rs "__st_rs__" true)
|
||||
(dict-set! rs "_chars" (string->list s))
|
||||
(dict-set! rs "pos" 0)
|
||||
rs))
|
||||
|
||||
(define (st-read-stream? v) (and (dict? v) (dict-has? v "__st_rs__")))
|
||||
|
||||
(define
|
||||
(st-read-stream-at-end? rs)
|
||||
(>= (get rs "pos") (len (get rs "_chars"))))
|
||||
|
||||
(define
|
||||
(st-read-stream-next rs)
|
||||
(if
|
||||
(st-read-stream-at-end? rs)
|
||||
nil
|
||||
(let
|
||||
((c (nth (get rs "_chars") (get rs "pos"))))
|
||||
(dict-set! rs "pos" (+ (get rs "pos") 1))
|
||||
c)))
|
||||
|
||||
(define
|
||||
(st-read-stream-peek rs)
|
||||
(if
|
||||
(st-read-stream-at-end? rs)
|
||||
nil
|
||||
(nth (get rs "_chars") (get rs "pos"))))
|
||||
|
||||
(define (st-read-stream-source rs) (list->string (get rs "_chars")))
|
||||
71
lib/smalltalk/test.sh
Executable file
71
lib/smalltalk/test.sh
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/smalltalk/test.sh — smoke-test the Smalltalk runtime layer.
|
||||
# Uses sx_server.exe epoch protocol.
|
||||
#
|
||||
# Usage:
|
||||
# bash lib/smalltalk/test.sh
|
||||
# bash lib/smalltalk/test.sh -v
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found. Run: cd hosts/ocaml && dune build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERBOSE="${1:-}"
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "spec/stdlib.sx")
|
||||
(load "lib/smalltalk/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/smalltalk/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(list st-test-pass st-test-fail)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}')
|
||||
if [ -z "$LINE" ]; then
|
||||
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 3 //; s/\)$//')
|
||||
fi
|
||||
if [ -z "$LINE" ]; then
|
||||
echo "ERROR: could not extract summary"
|
||||
echo "$OUTPUT" | tail -10
|
||||
exit 1
|
||||
fi
|
||||
|
||||
P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/')
|
||||
F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/')
|
||||
TOTAL=$((P + F))
|
||||
|
||||
if [ "$F" -eq 0 ]; then
|
||||
echo "ok $P/$TOTAL lib/smalltalk tests passed"
|
||||
else
|
||||
echo "FAIL $P/$TOTAL passed, $F failed"
|
||||
# Print failure details
|
||||
TMPFILE2=$(mktemp)
|
||||
cat > "$TMPFILE2" << 'EPOCHS2'
|
||||
(epoch 1)
|
||||
(load "spec/stdlib.sx")
|
||||
(load "lib/smalltalk/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/smalltalk/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(map (fn (f) (get f \"name\")) st-test-fails)")
|
||||
EPOCHS2
|
||||
FAILS=$(timeout 60 "$SX_SERVER" < "$TMPFILE2" 2>/dev/null | grep -E '^\(ok 3 ' || true)
|
||||
rm -f "$TMPFILE2"
|
||||
echo " Failures: $FAILS"
|
||||
fi
|
||||
|
||||
[ "$F" -eq 0 ]
|
||||
241
lib/smalltalk/tests/runtime.sx
Normal file
241
lib/smalltalk/tests/runtime.sx
Normal file
@@ -0,0 +1,241 @@
|
||||
;; lib/smalltalk/tests/runtime.sx — Tests for lib/smalltalk/runtime.sx
|
||||
;;
|
||||
;; Uses the same hk-test framework as lib/haskell/tests/runtime.sx.
|
||||
;; Load: lib/smalltalk/runtime.sx first.
|
||||
|
||||
;; --- Test framework ---
|
||||
(define st-test-pass 0)
|
||||
(define st-test-fail 0)
|
||||
(define st-test-fails (list))
|
||||
|
||||
(define
|
||||
(st-test name got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! st-test-pass (+ st-test-pass 1))
|
||||
(begin
|
||||
(set! st-test-fail (+ st-test-fail 1))
|
||||
(set! st-test-fails (append st-test-fails (list {:got got :expected expected :name name}))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Numeric helpers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(st-test "abs -5" (st-abs -5) 5)
|
||||
(st-test "abs 3" (st-abs 3) 3)
|
||||
(st-test "max 3 7" (st-max 3 7) 7)
|
||||
(st-test "min 3 7" (st-min 3 7) 3)
|
||||
(st-test "gcd 12 8" (st-gcd 12 8) 4)
|
||||
(st-test "lcm 4 6" (st-lcm 4 6) 12)
|
||||
(st-test "quo 10 3" (st-quo 10 3) 3)
|
||||
(st-test "quo -10 3" (st-quo -10 3) -3)
|
||||
(st-test "rem 10 3" (st-rem 10 3) 1)
|
||||
(st-test "rem -10 3" (st-rem -10 3) -1)
|
||||
(st-test "mod 10 3" (st-mod 10 3) 1)
|
||||
(st-test "mod -10 3" (st-mod -10 3) 2)
|
||||
(st-test "even? 4" (st-even? 4) true)
|
||||
(st-test "even? 3" (st-even? 3) false)
|
||||
(st-test "odd? 7" (st-odd? 7) true)
|
||||
(st-test "floor 3.7" (st-floor 3.7) 3)
|
||||
(st-test "ceiling 3.2" (st-ceiling 3.2) 4)
|
||||
(st-test "truncated 3.9" (st-truncated 3.9) 3)
|
||||
(st-test "rounded 3.5" (st-rounded 3.5) 4)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. Character
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(st-test
|
||||
"char-value A"
|
||||
(st-char-value (st-char-from-int 65))
|
||||
65)
|
||||
(st-test "char-from-int" (st-char? (st-char-from-int 65)) true)
|
||||
(st-test "char? true" (st-char? (integer->char 65)) true)
|
||||
(st-test "char? false" (st-char? 65) false)
|
||||
(st-test "is-letter? A" (st-char-is-letter? (integer->char 65)) true)
|
||||
(st-test
|
||||
"is-letter? 1"
|
||||
(st-char-is-letter? (integer->char 49))
|
||||
false)
|
||||
(st-test "is-digit? 5" (st-char-is-digit? (integer->char 53)) true)
|
||||
(st-test "is-digit? A" (st-char-is-digit? (integer->char 65)) false)
|
||||
(st-test
|
||||
"is-uppercase? A"
|
||||
(st-char-is-uppercase? (integer->char 65))
|
||||
true)
|
||||
(st-test
|
||||
"is-uppercase? a"
|
||||
(st-char-is-uppercase? (integer->char 97))
|
||||
false)
|
||||
(st-test
|
||||
"is-lowercase? a"
|
||||
(st-char-is-lowercase? (integer->char 97))
|
||||
true)
|
||||
(st-test
|
||||
"is-lowercase? A"
|
||||
(st-char-is-lowercase? (integer->char 65))
|
||||
false)
|
||||
(st-test
|
||||
"is-separator? sp"
|
||||
(st-char-is-separator? (integer->char 32))
|
||||
true)
|
||||
(st-test
|
||||
"is-separator? A"
|
||||
(st-char-is-separator? (integer->char 65))
|
||||
false)
|
||||
(st-test
|
||||
"as-uppercase a"
|
||||
(st-char-value (st-char-as-uppercase (integer->char 97)))
|
||||
65)
|
||||
(st-test
|
||||
"as-uppercase A"
|
||||
(st-char-value (st-char-as-uppercase (integer->char 65)))
|
||||
65)
|
||||
(st-test
|
||||
"as-lowercase A"
|
||||
(st-char-value (st-char-as-lowercase (integer->char 65)))
|
||||
97)
|
||||
(st-test
|
||||
"digit-value 5"
|
||||
(st-char-digit-value (integer->char 53))
|
||||
5)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Array
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(st-test
|
||||
"array-new size"
|
||||
(st-array-size (st-array-new 5))
|
||||
5)
|
||||
(st-test "array? yes" (st-array? (st-array-new 3)) true)
|
||||
(st-test "array? no" (st-array? 42) false)
|
||||
(st-test
|
||||
"array-at nil"
|
||||
(st-array-at (st-array-new 3) 1)
|
||||
nil)
|
||||
|
||||
(let
|
||||
((a (st-array-new 3)))
|
||||
(st-array-at-put! a 1 10)
|
||||
(st-array-at-put! a 2 20)
|
||||
(st-array-at-put! a 3 30)
|
||||
(st-test "array-at 1" (st-array-at a 1) 10)
|
||||
(st-test "array-at 2" (st-array-at a 2) 20)
|
||||
(st-test "array-at 3" (st-array-at a 3) 30))
|
||||
|
||||
(st-test
|
||||
"list->array->list"
|
||||
(st-array->list (st-list->array (list 1 2 3)))
|
||||
(list 1 2 3))
|
||||
|
||||
(let
|
||||
((a (st-list->array (list 10 20 30 40 50))))
|
||||
(st-test
|
||||
"copy-from-to"
|
||||
(st-array->list (st-array-copy-from-to a 2 4))
|
||||
(list 20 30 40)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Dictionary
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(st-test "dict? yes" (st-dict? (st-dict-new)) true)
|
||||
(st-test "dict? no" (st-dict? 42) false)
|
||||
(st-test "dict empty size" (st-dict-size (st-dict-new)) 0)
|
||||
(st-test "dict at absent" (st-dict-at (st-dict-new) "k") nil)
|
||||
|
||||
(let
|
||||
((d (st-dict-new)))
|
||||
(st-dict-at-put! d "a" 1)
|
||||
(st-dict-at-put! d "b" 2)
|
||||
(st-test "dict at a" (st-dict-at d "a") 1)
|
||||
(st-test "dict at b" (st-dict-at d "b") 2)
|
||||
(st-test "dict size 2" (st-dict-size d) 2)
|
||||
(st-test "includes-key? yes" (st-dict-includes-key? d "a") true)
|
||||
(st-test "includes-key? no" (st-dict-includes-key? d "z") false)
|
||||
(st-dict-at-put! d "a" 99)
|
||||
(st-test "dict update" (st-dict-at d "a") 99)
|
||||
(st-test "size unchanged" (st-dict-size d) 2)
|
||||
(st-dict-remove-key! d "a")
|
||||
(st-test "size after remove" (st-dict-size d) 1)
|
||||
(st-test "at-default hit" (st-dict-at-default d "b" 0) 2)
|
||||
(st-test "at-default miss" (st-dict-at-default d "z" -1) -1))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Set
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(st-test "set? yes" (st-set? (st-set-new)) true)
|
||||
(st-test "set? no" (st-set? 42) false)
|
||||
(st-test "set empty size" (st-set-size (st-set-new)) 0)
|
||||
|
||||
(let
|
||||
((s (st-set-new)))
|
||||
(st-set-add! s 1)
|
||||
(st-set-add! s 2)
|
||||
(st-set-add! s 1)
|
||||
(st-test "set includes 1" (st-set-includes? s 1) true)
|
||||
(st-test "set includes 2" (st-set-includes? s 2) true)
|
||||
(st-test "set not includes 3" (st-set-includes? s 3) false)
|
||||
(st-test "set dedup size" (st-set-size s) 2)
|
||||
(st-set-remove! s 1)
|
||||
(st-test "size after remove" (st-set-size s) 1)
|
||||
(st-test "removed gone" (st-set-includes? s 1) false))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 6. String / Stream
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(st-test "join-strings 3" (st-join-strings (list "a" "b" "c") "-") "a-b-c")
|
||||
(st-test "join-strings 1" (st-join-strings (list "x") ",") "x")
|
||||
(st-test "join-strings empty" (st-join-strings (list) ",") "")
|
||||
|
||||
(st-test "print nil" (st-print-string nil) "nil")
|
||||
(st-test "print true" (st-print-string true) "true")
|
||||
(st-test "print false" (st-print-string false) "false")
|
||||
(st-test "print number" (st-print-string 42) "42")
|
||||
(st-test "print string" (st-print-string "hi") "'hi'")
|
||||
(st-test "print char" (st-print-string (integer->char 65)) "$A")
|
||||
(st-test "print list" (st-print-string (list 1 2)) "(1 2)")
|
||||
|
||||
(let
|
||||
((ws (st-write-stream-new)))
|
||||
(st-write-stream-put-string! ws "hello")
|
||||
(st-write-stream-put-string! ws " world")
|
||||
(st-test
|
||||
"write-stream contents"
|
||||
(st-write-stream-contents ws)
|
||||
"hello world"))
|
||||
|
||||
(let
|
||||
((ws (st-write-stream-new)))
|
||||
(st-write-stream-next-put! ws (integer->char 72))
|
||||
(st-write-stream-next-put! ws (integer->char 105))
|
||||
(st-test "write-stream next-put!" (st-write-stream-contents ws) "Hi"))
|
||||
|
||||
(let
|
||||
((rs (st-read-stream-new "ABC")))
|
||||
(st-test
|
||||
"read-stream next A"
|
||||
(st-char-value (st-read-stream-next rs))
|
||||
65)
|
||||
(st-test
|
||||
"read-stream next B"
|
||||
(st-char-value (st-read-stream-next rs))
|
||||
66)
|
||||
(st-test
|
||||
"read-stream peek C"
|
||||
(st-char-value (st-read-stream-peek rs))
|
||||
67)
|
||||
(st-test
|
||||
"read-stream next C"
|
||||
(st-char-value (st-read-stream-next rs))
|
||||
67)
|
||||
(st-test "read-stream at-end" (st-read-stream-at-end? rs) true))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Summary (must be last form — test.sh reads this)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(list st-test-pass st-test-fail)
|
||||
279
lib/tcl/runtime.sx
Normal file
279
lib/tcl/runtime.sx
Normal file
@@ -0,0 +1,279 @@
|
||||
;; lib/tcl/runtime.sx — Tcl primitives on SX
|
||||
;;
|
||||
;; Provides Tcl-idiomatic wrappers over SX built-ins.
|
||||
;; Primitives used:
|
||||
;; make-regexp/regexp-match/regexp-match-all/... (Phase 19)
|
||||
;; make-set/set-add!/set-member?/set-remove!/set->list (Phase 18)
|
||||
;; call/cc (core evaluator)
|
||||
;; quotient/remainder (Phase 15 / builtin)
|
||||
;; string->list/list->string/char->integer (Phase 13)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. String buffer — Tcl append / string accumulation
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(tcl-sb-new)
|
||||
(let
|
||||
((sb (dict)))
|
||||
(dict-set! sb "_tcl_sb" true)
|
||||
(dict-set! sb "_buf" "")
|
||||
sb))
|
||||
|
||||
(define (tcl-sb? v) (and (dict? v) (dict-has? v "_tcl_sb")))
|
||||
|
||||
(define
|
||||
(tcl-sb-append! sb s)
|
||||
(dict-set! sb "_buf" (str (get sb "_buf") s))
|
||||
sb)
|
||||
|
||||
(define (tcl-sb-value sb) (get sb "_buf"))
|
||||
|
||||
(define (tcl-sb-clear! sb) (dict-set! sb "_buf" "") sb)
|
||||
|
||||
(define (tcl-sb-length sb) (len (get sb "_buf")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. String port (channel) — Tcl channel abstraction
|
||||
;; Read channel: created from a string, supports gets/read.
|
||||
;; Write channel: accumulates puts output, queryable via tcl-chan-string.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
(tcl-chan-in-new str)
|
||||
(let
|
||||
((c (dict)))
|
||||
(dict-set! c "_tcl_chan" true)
|
||||
(dict-set! c "_mode" "read")
|
||||
(dict-set! c "_chars" (string->list str))
|
||||
(dict-set! c "_pos" 0)
|
||||
c))
|
||||
|
||||
(define
|
||||
(tcl-chan-out-new)
|
||||
(let
|
||||
((c (dict)))
|
||||
(dict-set! c "_tcl_chan" true)
|
||||
(dict-set! c "_mode" "write")
|
||||
(dict-set! c "_buf" "")
|
||||
c))
|
||||
|
||||
(define (tcl-chan? v) (and (dict? v) (dict-has? v "_tcl_chan")))
|
||||
|
||||
(define
|
||||
(tcl-chan-eof? c)
|
||||
(and
|
||||
(= (get c "_mode") "read")
|
||||
(>= (get c "_pos") (len (get c "_chars")))))
|
||||
|
||||
(define
|
||||
(tcl-chan-read-char c)
|
||||
(if
|
||||
(tcl-chan-eof? c)
|
||||
nil
|
||||
(let
|
||||
((ch (nth (get c "_chars") (get c "_pos"))))
|
||||
(dict-set! c "_pos" (+ (get c "_pos") 1))
|
||||
ch)))
|
||||
|
||||
;; gets — read one line (up to newline or EOF), return without trailing newline
|
||||
(define
|
||||
(tcl-chan-gets c)
|
||||
(letrec
|
||||
((go (fn (acc) (let ((ch (tcl-chan-read-char c))) (cond ((= ch nil) (list->string (reverse acc))) ((= (char->integer ch) 10) (list->string (reverse acc))) (else (go (cons ch acc))))))))
|
||||
(go (list))))
|
||||
|
||||
;; read — read all remaining chars
|
||||
(define
|
||||
(tcl-chan-read c)
|
||||
(letrec
|
||||
((go (fn (acc) (let ((ch (tcl-chan-read-char c))) (if (= ch nil) (list->string (reverse acc)) (go (cons ch acc)))))))
|
||||
(go (list))))
|
||||
|
||||
;; puts — write string to write channel (no newline)
|
||||
(define
|
||||
(tcl-chan-puts! c s)
|
||||
(when
|
||||
(= (get c "_mode") "write")
|
||||
(dict-set! c "_buf" (str (get c "_buf") s)))
|
||||
c)
|
||||
|
||||
;; puts-line — write string + newline (Tcl default puts behaviour)
|
||||
(define (tcl-chan-puts-line! c s) (tcl-chan-puts! c (str s "\n")))
|
||||
|
||||
;; string — get accumulated content of write channel
|
||||
(define (tcl-chan-string c) (get c "_buf"))
|
||||
|
||||
;; tell — current read position
|
||||
(define (tcl-chan-tell c) (get c "_pos"))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Regexp — Tcl regexp / regsub wrappers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define (tcl-re-new pattern) (make-regexp pattern ""))
|
||||
|
||||
(define (tcl-re-new-flags pattern flags) (make-regexp pattern flags))
|
||||
|
||||
(define (tcl-re? v) (regexp? v))
|
||||
|
||||
(define (tcl-re-match? rx str) (not (= (regexp-match rx str) nil)))
|
||||
|
||||
(define (tcl-re-match rx str) (regexp-match rx str))
|
||||
|
||||
(define (tcl-re-match-all rx str) (regexp-match-all rx str))
|
||||
|
||||
(define (tcl-re-sub rx str replacement) (regexp-replace rx str replacement))
|
||||
|
||||
(define
|
||||
(tcl-re-sub-all rx str replacement)
|
||||
(regexp-replace-all rx str replacement))
|
||||
|
||||
(define (tcl-re-split rx str) (regexp-split rx str))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Format — Tcl format command (%s %d %f %x %o %%)
|
||||
;; tcl-format takes a format string and a list of arguments.
|
||||
;; Example: (tcl-format "%s is %d" (list "Alice" 30)) → "Alice is 30"
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Digit characters for base conversion
|
||||
(define tcl-hex-chars (string->list "0123456789abcdef"))
|
||||
|
||||
(define
|
||||
(tcl-digits-for-base n base digit-chars)
|
||||
(let
|
||||
((abs-n (if (< n 0) (- 0 n) n)))
|
||||
(letrec
|
||||
((go (fn (n acc) (if (= n 0) (if (= (len acc) 0) "0" (list->string acc)) (go (quotient n base) (cons (nth digit-chars (remainder n base)) acc))))))
|
||||
(let
|
||||
((unsigned (go abs-n (list))))
|
||||
(if (< n 0) (str "-" unsigned) unsigned)))))
|
||||
|
||||
(define
|
||||
(tcl-format-hex n)
|
||||
(tcl-digits-for-base (truncate n) 16 tcl-hex-chars))
|
||||
|
||||
(define
|
||||
(tcl-format-oct n)
|
||||
(tcl-digits-for-base (truncate n) 8 (string->list "01234567")))
|
||||
|
||||
(define
|
||||
(tcl-format fmt args)
|
||||
(letrec
|
||||
((chars (string->list fmt))
|
||||
(go
|
||||
(fn
|
||||
(cs arg-list result)
|
||||
(if
|
||||
(= (len cs) 0)
|
||||
result
|
||||
(let
|
||||
((c-int (char->integer (first cs))))
|
||||
(if
|
||||
(= c-int 37)
|
||||
(if
|
||||
(= (len (rest cs)) 0)
|
||||
result
|
||||
(let
|
||||
((spec-int (char->integer (first (rest cs)))))
|
||||
(cond
|
||||
((= spec-int 37)
|
||||
(go (rest (rest cs)) arg-list (str result "%")))
|
||||
((= spec-int 115)
|
||||
(go
|
||||
(rest (rest cs))
|
||||
(rest arg-list)
|
||||
(str result (str (first arg-list)))))
|
||||
((= spec-int 100)
|
||||
(go
|
||||
(rest (rest cs))
|
||||
(rest arg-list)
|
||||
(str result (str (truncate (first arg-list))))))
|
||||
((= spec-int 102)
|
||||
(go
|
||||
(rest (rest cs))
|
||||
(rest arg-list)
|
||||
(str result (str (+ 0 (first arg-list))))))
|
||||
((= spec-int 120)
|
||||
(go
|
||||
(rest (rest cs))
|
||||
(rest arg-list)
|
||||
(str result (tcl-format-hex (first arg-list)))))
|
||||
((= spec-int 111)
|
||||
(go
|
||||
(rest (rest cs))
|
||||
(rest arg-list)
|
||||
(str result (tcl-format-oct (first arg-list)))))
|
||||
(else
|
||||
(go
|
||||
(rest (rest cs))
|
||||
arg-list
|
||||
(str
|
||||
result
|
||||
"%"
|
||||
(list->string (list (first (rest cs))))))))))
|
||||
(go
|
||||
(rest cs)
|
||||
arg-list
|
||||
(str result (list->string (list (first cs)))))))))))
|
||||
(go chars args "")))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Coroutine — Tcl-style coroutine using call/cc
|
||||
;; tcl-co-yield works reliably when called from top-level fns.
|
||||
;; Avoid calling tcl-co-yield from letrec-bound lambdas (JIT limitation).
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define tcl-current-co nil)
|
||||
|
||||
(define
|
||||
(tcl-co-new body)
|
||||
(let
|
||||
((co (dict)))
|
||||
(dict-set! co "_tcl_co" true)
|
||||
(dict-set! co "_state" "new")
|
||||
(dict-set! co "_cont" nil)
|
||||
(dict-set! co "_resumer" nil)
|
||||
(dict-set! co "_parent" nil)
|
||||
(dict-set!
|
||||
co
|
||||
"_body"
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((result (body)))
|
||||
(dict-set! co "_state" "dead")
|
||||
(set! tcl-current-co (get co "_parent"))
|
||||
((get co "_resumer") result))))
|
||||
co))
|
||||
|
||||
(define (tcl-co? v) (and (dict? v) (dict-has? v "_tcl_co")))
|
||||
|
||||
(define (tcl-co-alive? co) (not (= (get co "_state") "dead")))
|
||||
|
||||
(define
|
||||
(tcl-co-yield val)
|
||||
(call/cc
|
||||
(fn
|
||||
(resume-k)
|
||||
(let
|
||||
((cur tcl-current-co))
|
||||
(dict-set! cur "_cont" resume-k)
|
||||
(dict-set! cur "_state" "suspended")
|
||||
(set! tcl-current-co (get cur "_parent"))
|
||||
((get cur "_resumer") val)))))
|
||||
|
||||
(define
|
||||
(tcl-co-resume co)
|
||||
(call/cc
|
||||
(fn
|
||||
(return-k)
|
||||
(dict-set! co "_parent" tcl-current-co)
|
||||
(dict-set! co "_resumer" return-k)
|
||||
(set! tcl-current-co co)
|
||||
(dict-set! co "_state" "running")
|
||||
(if
|
||||
(= (get co "_cont") nil)
|
||||
((get co "_body"))
|
||||
((get co "_cont") nil)))))
|
||||
62
lib/tcl/test.sh
Executable file
62
lib/tcl/test.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tcl/test.sh — smoke-test the Tcl runtime layer.
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
|
||||
|
||||
cat > "$TMPFILE" << 'EPOCHS'
|
||||
(epoch 1)
|
||||
(load "lib/tcl/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/tcl/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(list tcl-test-pass tcl-test-fail)")
|
||||
EPOCHS
|
||||
|
||||
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
|
||||
|
||||
LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}')
|
||||
if [ -z "$LINE" ]; then
|
||||
LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \
|
||||
| sed -E 's/^\(ok 3 //; s/\)$//')
|
||||
fi
|
||||
if [ -z "$LINE" ]; then
|
||||
echo "ERROR: could not extract summary"
|
||||
echo "$OUTPUT" | tail -20
|
||||
exit 1
|
||||
fi
|
||||
|
||||
P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/')
|
||||
F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/')
|
||||
TOTAL=$((P + F))
|
||||
|
||||
if [ "$F" -eq 0 ]; then
|
||||
echo "ok $P/$TOTAL lib/tcl tests passed"
|
||||
else
|
||||
echo "FAIL $P/$TOTAL passed, $F failed"
|
||||
TMPFILE2=$(mktemp)
|
||||
cat > "$TMPFILE2" << 'EPOCHS2'
|
||||
(epoch 1)
|
||||
(load "lib/tcl/runtime.sx")
|
||||
(epoch 2)
|
||||
(load "lib/tcl/tests/runtime.sx")
|
||||
(epoch 3)
|
||||
(eval "(map (fn (f) (list (get f :name) (get f :got) (get f :expected))) tcl-test-fails)")
|
||||
EPOCHS2
|
||||
FAILS=$(timeout 60 "$SX_SERVER" < "$TMPFILE2" 2>/dev/null | grep -E '^\(ok-len 3' -A1 | tail -1 || true)
|
||||
echo " Details: $FAILS"
|
||||
rm -f "$TMPFILE2"
|
||||
fi
|
||||
|
||||
[ "$F" -eq 0 ]
|
||||
146
lib/tcl/tests/runtime.sx
Normal file
146
lib/tcl/tests/runtime.sx
Normal file
@@ -0,0 +1,146 @@
|
||||
;; lib/tcl/tests/runtime.sx — Tests for lib/tcl/runtime.sx
|
||||
|
||||
(define tcl-test-pass 0)
|
||||
(define tcl-test-fail 0)
|
||||
(define tcl-test-fails (list))
|
||||
|
||||
(define
|
||||
(tcl-test name got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! tcl-test-pass (+ tcl-test-pass 1))
|
||||
(begin
|
||||
(set! tcl-test-fail (+ tcl-test-fail 1))
|
||||
(set! tcl-test-fails (append tcl-test-fails (list {:got got :expected expected :name name}))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. String buffer
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define sb1 (tcl-sb-new))
|
||||
(tcl-test "sb? new" (tcl-sb? sb1) true)
|
||||
(tcl-test "sb? non-sb" (tcl-sb? "hello") false)
|
||||
(tcl-test "sb value empty" (tcl-sb-value sb1) "")
|
||||
(tcl-test "sb length empty" (tcl-sb-length sb1) 0)
|
||||
(tcl-sb-append! sb1 "hello")
|
||||
(tcl-test "sb value after append" (tcl-sb-value sb1) "hello")
|
||||
(tcl-sb-append! sb1 " ")
|
||||
(tcl-sb-append! sb1 "world")
|
||||
(tcl-test "sb value after multi-append" (tcl-sb-value sb1) "hello world")
|
||||
(tcl-test "sb length" (tcl-sb-length sb1) 11)
|
||||
(tcl-sb-clear! sb1)
|
||||
(tcl-test "sb value after clear" (tcl-sb-value sb1) "")
|
||||
(tcl-test "sb length after clear" (tcl-sb-length sb1) 0)
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 2. String port (channel)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define chin1 (tcl-chan-in-new "hello\nworld\nfoo"))
|
||||
(tcl-test "chan? read" (tcl-chan? chin1) true)
|
||||
(tcl-test "chan eof? no" (tcl-chan-eof? chin1) false)
|
||||
(tcl-test "chan gets line1" (tcl-chan-gets chin1) "hello")
|
||||
(tcl-test "chan gets line2" (tcl-chan-gets chin1) "world")
|
||||
(tcl-test "chan gets line3" (tcl-chan-gets chin1) "foo")
|
||||
(tcl-test "chan eof? yes" (tcl-chan-eof? chin1) true)
|
||||
(tcl-test "chan gets at eof" (tcl-chan-gets chin1) "")
|
||||
|
||||
(define chin2 (tcl-chan-in-new "abcdef"))
|
||||
(tcl-test "chan read all" (tcl-chan-read chin2) "abcdef")
|
||||
(tcl-test "chan read empty" (tcl-chan-read chin2) "")
|
||||
|
||||
(define chout1 (tcl-chan-out-new))
|
||||
(tcl-test "chan? write" (tcl-chan? chout1) true)
|
||||
(tcl-chan-puts! chout1 "hello")
|
||||
(tcl-chan-puts! chout1 " world")
|
||||
(tcl-test "chan string" (tcl-chan-string chout1) "hello world")
|
||||
(tcl-chan-puts-line! chout1 "!")
|
||||
(tcl-test "chan string with newline" (tcl-chan-string chout1) "hello world!\n")
|
||||
|
||||
(define chout2 (tcl-chan-out-new))
|
||||
(tcl-chan-puts-line! chout2 "line1")
|
||||
(tcl-chan-puts-line! chout2 "line2")
|
||||
(tcl-test "chan multi-line" (tcl-chan-string chout2) "line1\nline2\n")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 3. Regexp
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define rx1 (tcl-re-new "hel+o"))
|
||||
(tcl-test "re? yes" (tcl-re? rx1) true)
|
||||
(tcl-test "re? no" (tcl-re? "hello") false)
|
||||
(tcl-test "re match? yes" (tcl-re-match? rx1 "say hello") true)
|
||||
(tcl-test "re match? no" (tcl-re-match? rx1 "goodbye") false)
|
||||
|
||||
(define m1 (tcl-re-match rx1 "say hello world"))
|
||||
(tcl-test "re match result" (get m1 "match") "hello")
|
||||
|
||||
(define rx2 (tcl-re-new "[0-9]+"))
|
||||
(define all (tcl-re-match-all rx2 "a1b22c333"))
|
||||
(tcl-test "re match-all count" (len all) 3)
|
||||
(tcl-test "re match-all last" (get (nth all 2) "match") "333")
|
||||
|
||||
(tcl-test "re sub" (tcl-re-sub rx2 "a1b2" "N") "aNb2")
|
||||
(tcl-test "re sub-all" (tcl-re-sub-all rx2 "a1b2" "N") "aNbN")
|
||||
|
||||
(define rx3 (tcl-re-new "[ ,]+"))
|
||||
(tcl-test "re split" (tcl-re-split rx3 "a b,c") (list "a" "b" "c"))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 4. Format
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(tcl-test "format %s" (tcl-format "hello %s" (list "world")) "hello world")
|
||||
(tcl-test "format %d" (tcl-format "n=%d" (list 42)) "n=42")
|
||||
(tcl-test "format %d truncates float" (tcl-format "n=%d" (list 3.9)) "n=3")
|
||||
(tcl-test
|
||||
"format %s %d"
|
||||
(tcl-format "%s is %d" (list "age" 30))
|
||||
"age is 30")
|
||||
(tcl-test "format %%" (tcl-format "100%% done" (list)) "100% done")
|
||||
(tcl-test "format %x" (tcl-format "%x" (list 255)) "ff")
|
||||
(tcl-test "format %x 16" (tcl-format "0x%x" (list 16)) "0x10")
|
||||
(tcl-test "format %o" (tcl-format "%o" (list 8)) "10")
|
||||
(tcl-test "format %o 255" (tcl-format "%o" (list 255)) "377")
|
||||
(tcl-test "format no spec" (tcl-format "plain text" (list)) "plain text")
|
||||
(tcl-test
|
||||
"format multiple"
|
||||
(tcl-format "%s=%d (0x%x)" (list "val" 255 255))
|
||||
"val=255 (0xff)")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 5. Coroutine
|
||||
;; tcl-co-yield works from top-level helper functions.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define
|
||||
co1
|
||||
(tcl-co-new
|
||||
(fn () (tcl-co-yield 1) (tcl-co-yield 2) 3)))
|
||||
|
||||
(tcl-test "co? yes" (tcl-co? co1) true)
|
||||
(tcl-test "co? no" (tcl-co? 42) false)
|
||||
(tcl-test "co alive? before" (tcl-co-alive? co1) true)
|
||||
(define cor1 (tcl-co-resume co1))
|
||||
(tcl-test "co resume 1" cor1 1)
|
||||
(tcl-test "co alive? mid" (tcl-co-alive? co1) true)
|
||||
(define cor2 (tcl-co-resume co1))
|
||||
(tcl-test "co resume 2" cor2 2)
|
||||
(define cor3 (tcl-co-resume co1))
|
||||
(tcl-test "co resume 3 completion" cor3 3)
|
||||
(tcl-test "co alive? dead" (tcl-co-alive? co1) false)
|
||||
|
||||
;; Top-level helper for recursive yield (avoids JIT letrec limitation)
|
||||
(define
|
||||
(tcl-co-count-down i)
|
||||
(when
|
||||
(>= i 1)
|
||||
(tcl-co-yield i)
|
||||
(tcl-co-count-down (- i 1))))
|
||||
|
||||
(define co2 (tcl-co-new (fn () (tcl-co-count-down 3) "done")))
|
||||
(tcl-test "co loop 3" (tcl-co-resume co2) 3)
|
||||
(tcl-test "co loop 2" (tcl-co-resume co2) 2)
|
||||
(tcl-test "co loop 1" (tcl-co-resume co2) 1)
|
||||
(tcl-test "co loop done" (tcl-co-resume co2) "done")
|
||||
(tcl-test "co loop dead" (tcl-co-alive? co2) false)
|
||||
81
plans/agent-briefings/apl-loop.md
Normal file
81
plans/agent-briefings/apl-loop.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# apl-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/apl-on-sx.md` forever. Rank-polymorphic primitives + 6 operators on the JIT is the headline showcase — APL is the densest combinator algebra you can put on top of a primitive table. Every program is `array → array` pure pipelines, exactly what the JIT was built for.
|
||||
|
||||
```
|
||||
description: apl-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/apl-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/apl-on-sx.md` — roadmap + Progress log.
|
||||
2. `ls lib/apl/` — pick up from the most advanced file.
|
||||
3. If `lib/apl/tests/*.sx` exist, run them. Green before new work.
|
||||
4. If `lib/apl/scoreboard.md` exists, that's your baseline.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/apl-on-sx.md`:
|
||||
|
||||
- **Phase 1** — tokenizer + parser. Unicode glyphs, `¯` for negative, strands (juxtaposition), right-to-left, valence resolution by syntactic position
|
||||
- **Phase 2** — array model + scalar primitives. `make-array {shape, ravel}`, scalar promotion, broadcast for `+ - × ÷ ⌈ ⌊ * ⍟ | ! ○`, comparison, logical, `⍳`, `⎕IO`
|
||||
- **Phase 3** — structural primitives + indexing. `⍴ , ⍉ ↑ ↓ ⌽ ⊖ ⌷ ⍋ ⍒ ⊂ ⊃ ∊`
|
||||
- **Phase 4** — **THE SHOWCASE**: operators. `f/` (reduce), `f¨` (each), `∘.f` (outer), `f.g` (inner), `f⍨` (commute), `f∘g` (compose), `f⍣n` (power), `f⍤k` (rank), `@` (at)
|
||||
- **Phase 5** — dfns + tradfns + control flow. `{⍺+⍵}`, `∇` recurse, `⍺←default`, tradfn header, `:If/:While/:For/:Select`
|
||||
- **Phase 6** — classic programs (life, mandelbrot, primes, n-queens, quicksort) + idiom corpus + drive to 100+
|
||||
|
||||
Within a phase, pick the checkbox that unlocks the most tests per effort.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/apl/**` and `plans/apl-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. APL primitives go in `lib/apl/runtime.sx`.
|
||||
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
|
||||
- **Shared-file issues** → plan's Blockers with minimal repro.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
||||
- **Unicode in `.sx`:** raw UTF-8 only, never `\uXXXX` escapes. Glyphs land directly in source.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
|
||||
## APL-specific gotchas
|
||||
|
||||
- **Right-to-left, no precedence among functions.** `2 × 3 + 4` is `2 × (3 + 4)` = 14, not 10. Operators bind tighter than functions: `+/ ⍳5` is `+/(⍳5)`, and `2 +.× 3 4` is `2 (+.×) 3 4`.
|
||||
- **Valence by position.** `-3` is monadic negate (`-` with no left arg). `5-3` is dyadic subtract. The parser must look left to decide. Same glyph; different fn.
|
||||
- **`¯` is part of a number literal**, not a prefix function. `¯3` is the literal negative three; `-3` is the function call. Tokenizer eats `¯` into the numeric token.
|
||||
- **Strands.** `1 2 3` is a 3-element vector, not three separate calls. Adjacent literals fuse into a strand at parse time. Adjacent names do *not* fuse — `a b c` is three separate references.
|
||||
- **Scalar promotion.** `1 + 2 3 4` ↦ `3 4 5`. Any scalar broadcasts against any-rank conformable shape.
|
||||
- **Conformability** = exactly matching shapes, OR one side scalar, OR (in some dialects) one side rank-1 cycling against rank-N. Keep strict in v1: matching shape or scalar only.
|
||||
- **`⍳` is overloaded.** Monadic `⍳N` = vector 1..N (or 0..N-1 if `⎕IO=0`). Dyadic `V ⍳ W` = first-index lookup, returns `≢V+1` for not-found.
|
||||
- **Reduce with `+/⍳0`** = `0` (identity for `+`). Each scalar primitive has a defined identity used by reduce-on-empty. Don't crash; return identity.
|
||||
- **Reduce direction.** `f/` reduces the *last* axis. `f⌿` reduces the *first*. Matters for matrices.
|
||||
- **Indexing is 1-based** by default (`⎕IO=1`). Do not silently translate to 0-based; respect `⎕IO`.
|
||||
- **Bracket indexing** `A[I]` is sugar for `I⌷A` (squad-quad). Multi-axis: `A[I;J]` is `I J⌷A` with semicolon-separated axes; `A[;J]` selects all of axis 0.
|
||||
- **Dfn `{...}`** — `⍺` = left arg (may be unbound for monadic call → check with `⍺←default`), `⍵` = right arg, `∇` = recurse. Default left arg syntax: `⍺←0`.
|
||||
- **Tradfn vs dfn** — tradfns use line-numbered `→linenum` for goto; dfns use guards `cond:expr`. Pick the right one for the user's syntax.
|
||||
- **Empty array** = rank-N array where some dim is 0. `0⍴⍳0` is empty rank-1. Scalar prototype matters for empty-array operations; ignore in v1, return 0/space.
|
||||
- **Test corpus:** custom + idioms. Place programs in `lib/apl/tests/programs/` with `.apl` extension.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr.
|
||||
- `type-of` on user fn returns `"lambda"`.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/apl-on-sx.md` inline.
|
||||
- Short, factual commit messages (`apl: outer product ∘. (+9)`).
|
||||
- One feature per iteration. Commit. Log. Next.
|
||||
|
||||
Go. Read the plan; find first `[ ]`; implement.
|
||||
80
plans/agent-briefings/common-lisp-loop.md
Normal file
80
plans/agent-briefings/common-lisp-loop.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# common-lisp-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/common-lisp-on-sx.md` forever. Conditions + restarts on delimited continuations is the headline showcase — every other Lisp reinvents resumable exceptions on the host stack. On SX `signal`/`invoke-restart` is just a captured continuation. Plus CLOS, the LOOP macro, packages.
|
||||
|
||||
```
|
||||
description: common-lisp-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/common-lisp-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/common-lisp-on-sx.md` — roadmap + Progress log.
|
||||
2. `ls lib/common-lisp/` — pick up from the most advanced file.
|
||||
3. If `lib/common-lisp/tests/*.sx` exist, run them. Green before new work.
|
||||
4. If `lib/common-lisp/scoreboard.md` exists, that's your baseline.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/common-lisp-on-sx.md`:
|
||||
|
||||
- **Phase 1** — reader + parser (read macros `#'` `'` `` ` `` `,` `,@` `#( … )` `#:` `#\char` `#xFF` `#b1010`, ratios, dispatch chars, lambda lists with `&optional`/`&rest`/`&key`/`&aux`)
|
||||
- **Phase 2** — sequential eval + special forms (`let`/`let*`/`flet`/`labels`, `block`/`return-from`, `tagbody`/`go`, `unwind-protect`, multiple values, `setf` subset, dynamic variables)
|
||||
- **Phase 3** — **THE SHOWCASE**: condition system + restarts. `define-condition`, `signal`/`error`/`cerror`/`warn`, `handler-bind` (non-unwinding), `handler-case` (unwinding), `restart-case`, `restart-bind`, `find-restart`/`invoke-restart`/`compute-restarts`, `with-condition-restarts`. Classic programs (restart-demo, parse-recover, interactive-debugger) green.
|
||||
- **Phase 4** — CLOS: `defclass`, `defgeneric`, `defmethod` with `:before`/`:after`/`:around`, `call-next-method`, multiple dispatch
|
||||
- **Phase 5** — macros + LOOP macro + reader macros
|
||||
- **Phase 6** — packages + stdlib (sequence functions, FORMAT directives, drive corpus to 200+)
|
||||
|
||||
Within a phase, pick the checkbox that unlocks the most tests per effort.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/common-lisp/**` and `plans/common-lisp-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. CL primitives go in `lib/common-lisp/runtime.sx`.
|
||||
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
|
||||
- **Shared-file issues** → plan's Blockers with minimal repro.
|
||||
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
|
||||
## Common-Lisp-specific gotchas
|
||||
|
||||
- **`handler-bind` is non-unwinding** — handlers can decline by returning normally, in which case `signal` keeps walking the chain. **`handler-case` is unwinding** — picking a handler aborts the protected form via a captured continuation. Don't conflate them.
|
||||
- **Restarts are not handlers.** `restart-case` establishes named *resumption points*; `signal` runs handler code with restarts visible; the handler chooses a restart by calling `invoke-restart`, which abandons handler stack and resumes at the restart point. Two stacks: handlers walk down, restarts wait to be invoked.
|
||||
- **`block` / `return-from`** is lexical. `block name … (return-from name v) …` captures `^k` once at entry; `return-from` invokes it. `return-from` to a name not in scope is an error (don't fall back to outer block).
|
||||
- **`tagbody` / `go`** — each tag in tagbody is a continuation; `go tag` invokes it. Tags are lexical, can only target tagbodies in scope.
|
||||
- **`unwind-protect`** runs cleanup on *any* non-local exit (return-from, throw, condition unwind). Implement as a scope frame fired by the cleanup machinery.
|
||||
- **Multiple values**: primary-value-only contexts (function args, `if` test, etc.) drop extras silently. `values` produces multiple. `multiple-value-bind` / `multiple-value-call` consume them. Don't auto-list.
|
||||
- **CLOS dispatch:** sort applicable methods by argument-list specificity (`subclassp` per arg, left-to-right); standard method combination calls primary methods most-specific-first via `call-next-method` chain. `:before` runs all before primaries; `:after` runs all after, in reverse-specificity. `:around` wraps everything.
|
||||
- **`call-next-method`** is a *continuation* available only inside a method body. Implement as a thunk stored in a dynamic-extent variable.
|
||||
- **Generalised reference (`setf`)**: `(setf (foo x) v)` ↦ `(setf-foo v x)`. Look up the setf-expander, not just a writer fn. `define-setf-expander` is mandatory for non-trivial places. Start with the symbolic / list / aref / slot-value cases.
|
||||
- **Dynamic variables (specials):** `defvar`/`defparameter` mark a symbol as special. `let` over a special name *rebinds* in dynamic extent (use parameterize-style scope), not lexical.
|
||||
- **Symbols are package-qualified.** Reader resolves `cl:car`, `mypkg::internal`, bare `foo` (current package). Internal vs external matters for `:` (one colon) reads.
|
||||
- **`nil` is also `()` is also the empty list.** Same object. `nil` is also false. CL has no distinct unit value.
|
||||
- **LOOP macro is huge.** Build incrementally — start with `for/in`, `for/from`, `collect`, `sum`, `count`, `repeat`. Add conditional clauses (`when`, `if`, `else`) once iteration drivers stable. `named` blocks + `return-from named` last.
|
||||
- **Test corpus:** custom + curated `ansi-test` slice. Place programs in `lib/common-lisp/tests/programs/` with `.lisp` extension.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr.
|
||||
- `type-of` on user fn returns `"lambda"`.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/common-lisp-on-sx.md` inline.
|
||||
- Short, factual commit messages (`common-lisp: handler-bind + 12 tests`).
|
||||
- One feature per iteration. Commit. Log. Next.
|
||||
|
||||
Go. Read the plan; find first `[ ]`; implement.
|
||||
789
plans/agent-briefings/primitives-loop.md
Normal file
789
plans/agent-briefings/primitives-loop.md
Normal file
@@ -0,0 +1,789 @@
|
||||
# SX Primitives — Meta-Loop Briefing
|
||||
|
||||
Goal: add fundamental missing SX primitives in sequence, then sweep all language
|
||||
implementations to replace their workarounds. Full rationale: vectors fix O(n) array
|
||||
access across every language; numeric tower fixes float/int conflation; dynamic-wind
|
||||
fixes cleanup semantics; coroutine primitive unifies Ruby/Lua/Tcl; string buffer fixes
|
||||
O(n²) concat; algebraic data types eliminate the tagged-dict pattern everywhere.
|
||||
|
||||
**Each fire: find the first unchecked `[ ]`, do it, commit, tick it, stop.**
|
||||
Sub-items within a Phase may span multiple fires — just commit progress and tick what's done.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0 — Prep (gate)
|
||||
|
||||
- [x] Stop new-language loops: send `/exit` to sx-loops windows for the four blank-slate
|
||||
languages that haven't committed workarounds yet:
|
||||
```
|
||||
tmux send-keys -t sx-loops:common-lisp "/exit" Enter
|
||||
tmux send-keys -t sx-loops:apl "/exit" Enter
|
||||
tmux send-keys -t sx-loops:ruby "/exit" Enter
|
||||
tmux send-keys -t sx-loops:tcl "/exit" Enter
|
||||
```
|
||||
Verify all four windows are idle (claude prompt, no active task).
|
||||
|
||||
- [x] E38 + E39 landed: check both Bucket-E branches for implementation commits.
|
||||
```
|
||||
git log --oneline hs-e38-sourceinfo | head -5
|
||||
git log --oneline hs-e39-webworker | head -5
|
||||
```
|
||||
If either branch has only its base commit (no impl work yet): note "pending" and stop —
|
||||
next fire re-checks. Proceed only when both have at least one implementation commit.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1 — Vectors
|
||||
|
||||
Native mutable integer-indexed arrays. Fix: Lua O(n) sort, APL rank polymorphism, Ruby
|
||||
Array, Tcl lists, Common Lisp vectors, all using string-keyed dicts today.
|
||||
|
||||
Primitives to add:
|
||||
- `make-vector` `n` `[fill]` → vector of length n
|
||||
- `vector?` `v` → bool
|
||||
- `vector-ref` `v` `i` → element at index i (0-based)
|
||||
- `vector-set!` `v` `i` `x` → mutate in place
|
||||
- `vector-length` `v` → integer
|
||||
- `vector->list` `v` → list
|
||||
- `list->vector` `lst` → vector
|
||||
- `vector-fill!` `v` `x` → fill all elements
|
||||
- `vector-copy` `v` `[start]` `[end]` → fresh copy of slice
|
||||
|
||||
Steps:
|
||||
- [x] OCaml: add `SxVector of value array` to `hosts/ocaml/sx_types.ml`; implement all
|
||||
primitives in `hosts/ocaml/sx_primitives.ml` (or equivalent); wire into evaluator.
|
||||
Note: Vector type + most prims were already present; added bounds-checked vector-ref/set!
|
||||
and optional start/end to vector-copy. 10/10 vector tests pass (r7rs suite).
|
||||
- [x] Spec: add vector entries to `spec/primitives.sx` with type signatures and descriptions.
|
||||
All 10 vector primitives now have :as type annotations, :returns, and :doc strings.
|
||||
make-vector: optional fill param; vector-copy: optional start/end (done prev step).
|
||||
- [x] JS bootstrapper: implement vectors in `hosts/javascript/platform.js` (or equivalent);
|
||||
ensure `sx-browser.js` rebuild picks them up.
|
||||
Fixed index-of for lists (was returning -1 not NIL, breaking bind-lambda-params),
|
||||
added _lastErrorKont_/hostError/try-catch/without-io-hook stubs. Vectors work.
|
||||
- [x] Tests: 40+ tests in `spec/tests/test-vectors.sx` covering construction, ref, set!,
|
||||
length, conversions, fill, copy, bounds behaviour.
|
||||
42 tests, all pass. 1847 standard / 2362 full passing (up from 5).
|
||||
- [x] Verify: full test suite still passes (`node hosts/javascript/run_tests.js --full`).
|
||||
2362/4924 pass (improvement from pre-existing lambda binding bug, no regressions).
|
||||
- [x] Commit: `spec: vector primitive (make-vector/vector-ref/vector-set!/etc)`
|
||||
Committed as: js: fix lambda binding (index-of on lists), add vectors + R7RS platform stubs
|
||||
|
||||
---
|
||||
|
||||
## Phase 2 — Numeric tower
|
||||
|
||||
Float ≠ integer distinction. Fix: Erlang `=:=`, Lua `math.type()`, Haskell `Num`/`Integral`,
|
||||
Common Lisp `integerp`/`floatp`/`ratio`, JS `Number.isInteger`.
|
||||
|
||||
Changes:
|
||||
- `parse-number` preserves float identity: `"1.0"` → float 1.0, not integer 1
|
||||
- New predicates: `integer?`, `float?`, `exact?`, `inexact?`
|
||||
- New coercions: `exact->inexact`, `inexact->exact`
|
||||
- Fix `floor`/`ceiling`/`truncate`/`round` to return integers when applied to floats
|
||||
- `number->string` renders `1.0` as `"1.0"`, `1` as `"1"`
|
||||
- Arithmetic: `(+ 1 1.0)` → `2.0` (float contagion), `(+ 1 1)` → `2` (integer)
|
||||
|
||||
Steps:
|
||||
- [x] OCaml: distinguish `Integer of int` / `Number of float` in `sx_types.ml`; update all
|
||||
arithmetic primitives for float contagion; fix `parse-number`.
|
||||
92/92 numeric tower tests pass; 4874 total (394 pre-existing hs-upstream fails unchanged).
|
||||
- [x] Spec: update `spec/primitives.sx` with new predicates + coercions; document contagion rules.
|
||||
Added integer?/float? predicates; updated number? body; / returns "float"; floor/ceil/truncate
|
||||
return "integer"; +/-/* doc float contagion; fixed double-paren params; 4874/394 baseline.
|
||||
- [x] JS bootstrapper: update number representation and arithmetic.
|
||||
Added integer?/float?/exact?/inexact?/truncate/remainder/modulo/random-int/exact->inexact/
|
||||
inexact->exact/parse-number. Fixed sx_server.ml epoch protocol for Integer type.
|
||||
JS: 1940 passed (+60); OCaml: 4874/394 unchanged. 6 tests JS-only fail (float≡int limitation).
|
||||
- [x] Tests: 92 tests in `spec/tests/test-numeric-tower.sx` — int-arithmetic, float-contagion,
|
||||
division, predicates, coercions, rounding, parse-number, equality, modulo, min-max, stringify.
|
||||
- [x] Verify: full suite passes. OCaml 4874/394 (baseline unchanged). JS 1940/2500 (+60 vs pre-tower).
|
||||
No regressions on any test that relied on `1.0 = 1` — those tests were already using integer
|
||||
literals which remain identical in JS. 6 JS-only failures are platform-inherent (JS float≡int).
|
||||
- [x] Commit: all work landed across 4 commits (c70bbdeb, 45ec5535, b12a22e6, f5acb31c).
|
||||
|
||||
---
|
||||
|
||||
## Phase 3 — Dynamic-wind
|
||||
|
||||
Fix: Common Lisp `unwind-protect`, Ruby `ensure`, JS `finally`, Tcl `catch`+cleanup,
|
||||
Erlang `try...after` (currently uses double-nested guard workaround).
|
||||
|
||||
- [x] Spec: implement `dynamic-wind` in `spec/evaluator.sx` such that the after-thunk fires
|
||||
on both normal return AND non-local exit (raise/call-cc escape). Must compose with
|
||||
`guard` — currently they don't interact.
|
||||
- [x] OCaml: wire `dynamic-wind` through the CEK machine with a `WindFrame` continuation.
|
||||
- [x] JS bootstrapper: update.
|
||||
- [x] Tests: 20+ tests covering normal return, raise, call/cc escape, nested dynamic-winds.
|
||||
- [x] Commit: `spec: dynamic-wind + guard integration`
|
||||
|
||||
---
|
||||
|
||||
## Phase 4 — Coroutine primitive
|
||||
|
||||
Unify Ruby fibers, Lua coroutines, Tcl coroutines — all currently reimplemented separately
|
||||
using call/cc+perform/resume.
|
||||
|
||||
- [x] Spec: add `make-coroutine`, `coroutine-resume`, `coroutine-yield`, `coroutine?`,
|
||||
`coroutine-alive?` to `spec/primitives.sx`. Build on existing `perform`/`cek-resume`
|
||||
machinery — coroutines ARE perform/resume with a stable identity.
|
||||
Implemented as `spec/coroutines.sx` define-library; `make-coroutine` stub in evaluator.sx.
|
||||
17/17 coroutine tests pass (OCaml). Drives iteration via define+fn recursion (not named let —
|
||||
named let uses cek_call→cek_run which errors on IO suspension).
|
||||
- [x] OCaml: implement coroutine type; wire resume/yield through CEK suspension.
|
||||
No new native type needed — dict-based coroutine identity + existing cek-step-loop/
|
||||
cek-resume/perform primitives in run_tests.ml ARE the OCaml implementation. 17/17 pass.
|
||||
- [x] JS bootstrapper: update.
|
||||
All CEK primitives already in sx-browser.js. Fix: pre-load spec/coroutines.sx +
|
||||
spec/signals.sx in run_tests.js so (import (sx coroutines)) resolves without suspension.
|
||||
17/17 pass in JS. 1965/2500 (+25 vs 1940 baseline). Zero new failures.
|
||||
- [x] Tests: 25+ tests — multi-yield, final return, arg passthrough, alive? predicate,
|
||||
nested coroutines, "final return vs yield" distinction (the Lua gotcha).
|
||||
27 tests: added 10 new — state field inspection (ready/suspended/dead), yield from
|
||||
nested helper, initial resume arg ignored, mutable closure state, complex yield values,
|
||||
round-robin scheduling, factory-shared-no-state, non-coroutine error. 27/27 OCaml+JS.
|
||||
- [x] Commit: `spec: coroutine primitive (make-coroutine/resume/yield)`
|
||||
Phase 4 landed across 4 commits: 21cb9cf5 (spec library), 9eb12c66 (ocaml verified),
|
||||
b78e06a7 (js pre-load), 0ffe208e (27 tests). Phase 4 complete.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — String buffer
|
||||
|
||||
Fix O(n²) string concatenation in loops across Lua, Ruby, Common Lisp, Tcl.
|
||||
|
||||
- [x] Spec + OCaml: add `make-string-buffer`, `string-buffer-append!`, `string-buffer->string`,
|
||||
`string-buffer-length` to primitives. OCaml: `Buffer.t` wrapper. JS: array+join.
|
||||
Also: string-buffer? predicate; SxStringBuffer._string_buffer marker for typeOf/dict?
|
||||
exclusion; inspect case in sx_types.ml. 17/17 tests OCaml+JS.
|
||||
- [x] Tests: 15+ tests.
|
||||
17 tests written inline with Spec+OCaml step: construction, type-of, empty/length,
|
||||
single/multi-append, append-returns-nil, empty-string-append, reuse-after-to-string,
|
||||
independence, loop-building, CSV-row, unicode, repeated-to-string, join-pattern.
|
||||
17/17 OCaml+JS.
|
||||
- [x] Commit: `spec: string-buffer primitive`
|
||||
Committed as d98b5fa2 — all work in one commit (OCaml type + primitives + JS + spec + 17 tests).
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Algebraic data types
|
||||
|
||||
The deepest structural gap. Every language uses `{:tag "..." :field ...}` tagged dicts to
|
||||
simulate sum types. A native `define-type` + `match` form eliminates this everywhere.
|
||||
|
||||
- [x] Design: write `plans/designs/sx-adt.md` covering syntax, CEK dispatch, interaction with
|
||||
existing `cond`/`case`, exhaustiveness checking, recursive types, pattern variables.
|
||||
Draft, then stop — next fire reviews design before implementing.
|
||||
Written: define-type/match syntax, AdtValue runtime rep, stepSfDefineType + MatchFrame
|
||||
CEK dispatch, exhaustiveness warnings via _adt_registry, recursive types, nested patterns,
|
||||
wildcard _, 3-phase impl plan (basic/nested/exhaustiveness), open questions on accessors/singletons/inspect.
|
||||
|
||||
- [x] Spec: implement `define-type` special form in `spec/evaluator.sx`:
|
||||
`(define-type Name (Ctor1 field...) (Ctor2 field...) ...)`
|
||||
Creates constructor functions `Ctor1`, `Ctor2` + predicate `Name?`.
|
||||
|
||||
- [x] Spec: implement `match` special form:
|
||||
`(match expr ((Ctor1 a b) body) ((Ctor2 x) body) (else body))`
|
||||
Exhaustiveness warning if not all constructors covered and no `else`.
|
||||
|
||||
- [x] OCaml: add `SxAdt of string * value array` to types; implement constructors + match.
|
||||
Dict-based ADT (no native type needed — matches spec). Hand-written sf_define_type
|
||||
in bootstrap.py FIXUPS; registered via register_special_form. 172 assertions pass.
|
||||
4280/1080 full suite (37 improvement over old baseline 4243/1117).
|
||||
- [x] JS bootstrapper: update.
|
||||
No changes needed — define-type/match are spec-level; sx-browser.js rebuilt at 0dc7e159.
|
||||
40/40 ADT tests pass JS. 2032/2500 total (+67 vs 1965 phase-4 baseline).
|
||||
- [x] Tests: 40+ tests in `spec/tests/test-adt.sx`.
|
||||
40 tests written across two spec commits (6c872107+0dc7e159). All pass OCaml+JS.
|
||||
- [x] Commit: `spec: algebraic data types (define-type + match)`
|
||||
Phase 6 landed across 5 commits: 6c872107 (define-type spec), 0dc7e159 (match spec),
|
||||
5d1913e7 (ocaml bootstrap), f63b2147 (plan tick). JS already current.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Bitwise operations
|
||||
|
||||
Completely absent today. Needed by: Forth (core), APL (array masks), Erlang (bitmatch),
|
||||
JS (typed arrays, bitfields), Common Lisp (`logand`/`logior`/`logxor`/`lognot`/`ash`).
|
||||
|
||||
Primitives to add:
|
||||
- `bitwise-and` `a` `b` → integer
|
||||
- `bitwise-or` `a` `b` → integer
|
||||
- `bitwise-xor` `a` `b` → integer
|
||||
- `bitwise-not` `a` → integer
|
||||
- `arithmetic-shift` `a` `count` → integer (left if count > 0, right if count < 0)
|
||||
- `bit-count` `a` → number of set bits (popcount)
|
||||
- `integer-length` `a` → number of bits needed to represent a
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add entries to `spec/primitives.sx` with type signatures.
|
||||
stdlib.bitwise module with 7 entries appended to spec/primitives.sx.
|
||||
- [x] OCaml: implement in `hosts/ocaml/sx_primitives.ml` using OCaml `land`/`lor`/`lxor`/`lnot`/`lsl`/`asr`.
|
||||
land/lor/lxor/lnot/lsl/asr in sx_primitives.ml. bit-count: Kernighan loop. integer-length: lsr loop.
|
||||
- [x] JS bootstrapper: implement in `hosts/javascript/platform.js` using JS `&`/`|`/`^`/`~`/`<<`/`>>`.
|
||||
stdlib.bitwise module added to PRIMITIVES_JS_MODULES. bit-count: Hamming weight. integer-length: Math.clz32.
|
||||
- [x] Tests: 25+ tests in `spec/tests/test-bitwise.sx` — basic ops, shift left/right, negative numbers, popcount.
|
||||
26 tests, 158 assertions, all pass OCaml+JS.
|
||||
- [x] Commit: `spec: bitwise operations (bitwise-and/or/xor/not, arithmetic-shift, bit-count)`
|
||||
Committed a8a79dc9. Phase 7 complete in single commit.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8 — Multiple values
|
||||
|
||||
R7RS standard. Common Lisp uses them heavily; Haskell tuples map naturally; Erlang
|
||||
multi-return. Without them, every function returning two things encodes it as a list or dict.
|
||||
|
||||
Primitives / forms to add:
|
||||
- `values` `v...` → multiple-value object
|
||||
- `call-with-values` `producer` `consumer` → applies consumer to values from producer
|
||||
- `let-values` `(((a b) expr) ...)` `body` — binding form (special form in evaluator)
|
||||
- `define-values` `(a b ...)` `expr` — top-level multi-value bind
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add `SxValues` type to evaluator; implement `values` + `call-with-values` in
|
||||
`spec/evaluator.sx`; add `let-values` / `define-values` special forms.
|
||||
- [x] OCaml: add `SxValues of value list` to `sx_types.ml`; wire through CEK.
|
||||
- [x] JS bootstrapper: implement values type + forms.
|
||||
- [x] Tests: 25+ tests in `spec/tests/test-values.sx` — basic producer/consumer, let-values
|
||||
destructuring, define-values, interaction with `begin`/`do`.
|
||||
- [x] Commit: `spec: multiple values (values/call-with-values/let-values)`
|
||||
|
||||
---
|
||||
|
||||
## Phase 9 — Promises (lazy evaluation)
|
||||
|
||||
Critical for Haskell — lazy evaluation is so central that without it the Haskell
|
||||
implementation can't be idiomatic. Also useful for lazy lists in Common Lisp and
|
||||
lazy streams in Scheme-style code generally.
|
||||
|
||||
Primitives / forms to add:
|
||||
- `delay` `expr` → promise (special form — expr not evaluated yet)
|
||||
- `force` `p` → evaluate promise, cache result, return it
|
||||
- `make-promise` `v` → already-forced promise wrapping v
|
||||
- `promise?` `v` → bool
|
||||
- `delay-force` `expr` → for iterative lazy sequences (avoids stack growth in lazy streams)
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add `delay` / `delay-force` special forms to `spec/evaluator.sx`; add promise
|
||||
type with mutable forced/value slots; `force` checks if already forced before eval.
|
||||
- [x] OCaml: add `SxPromise of { mutable forced: bool; mutable value: value; thunk: value }`;
|
||||
wire `delay`/`force`/`delay-force` through CEK.
|
||||
- [x] JS bootstrapper: implement promise type + forms.
|
||||
- [x] Tests: 25+ tests in `spec/tests/test-promises.sx` — basic delay/force, memoisation
|
||||
(forced only once), delay-force lazy stream, promise? predicate, make-promise.
|
||||
- [x] Commit: `spec: promises — delay/force/delay-force for lazy evaluation`
|
||||
|
||||
---
|
||||
|
||||
## Phase 10 — Mutable hash tables
|
||||
|
||||
Distinct from SX's immutable dicts. Dict primitives copy on every update — fine for
|
||||
functional code, wrong for table-heavy language implementations. Lua tables, Smalltalk
|
||||
dicts, Erlang process dictionaries, and JS Map all need O(1) mutable associative storage.
|
||||
|
||||
Primitives to add:
|
||||
- `make-hash-table` `[capacity]` → fresh mutable hash table
|
||||
- `hash-table?` `v` → bool
|
||||
- `hash-table-set!` `ht` `key` `val` → mutate in place
|
||||
- `hash-table-ref` `ht` `key` `[default]` → value or default/error
|
||||
- `hash-table-delete!` `ht` `key` → remove entry
|
||||
- `hash-table-size` `ht` → integer
|
||||
- `hash-table-keys` `ht` → list of keys
|
||||
- `hash-table-values` `ht` → list of values
|
||||
- `hash-table->alist` `ht` → list of (key . value) pairs
|
||||
- `hash-table-for-each` `ht` `fn` → iterate (fn key val) for side effects
|
||||
- `hash-table-merge!` `dst` `src` → merge src into dst in place
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add entries to `spec/primitives.sx`.
|
||||
stdlib.hash-table module with 11 define-primitive entries appended to spec/primitives.sx.
|
||||
- [x] OCaml: add `HashTable of (value, value) Hashtbl.t` to `sx_types.ml`; implement
|
||||
all primitives in `hosts/ocaml/sx_primitives.ml`.
|
||||
HashTable variant in sx_types.ml; type_of/inspect cases added; 11 primitives in sx_primitives.ml;
|
||||
fixed _cek_call_ref reference for hash-table-for-each. 4385/1080 (+28).
|
||||
- [x] JS bootstrapper: implement using JS `Map` in `hosts/javascript/platform.js`.
|
||||
SxHashTable class with Map; _hash_table marker; dict?/type-of exclusion; apply() for for-each.
|
||||
2137/2500 (+4 vs phase-9 baseline).
|
||||
- [x] Tests: 30+ tests in `spec/tests/test-hash-table.sx` — set/ref/delete, size, iteration,
|
||||
default on missing key, merge, keys/values lists.
|
||||
28 tests; all pass OCaml+JS. Used empty? not assert= for empty-list comparisons.
|
||||
- [x] Commit: `spec: mutable hash tables (make-hash-table/ref/set!/delete!/etc)`
|
||||
Committed 133bdf52. Phase 10 complete.
|
||||
|
||||
---
|
||||
|
||||
## Phase 11 — Sequence protocol
|
||||
|
||||
Unified iteration over lists and vectors without conversion. Currently `map`/`filter`/
|
||||
`for-each` only work on lists — you must `vector->list` first, which defeats the purpose
|
||||
of vectors. A sequence protocol makes all collection operations polymorphic.
|
||||
|
||||
Approach: extend existing `map`/`filter`/`reduce`/`for-each`/`some`/`every?` to dispatch
|
||||
on type (list → existing path, vector → index loop, string → char iteration). Add:
|
||||
- `in-range` `start` `[end]` `[step]` → lazy range sequence (works with `for-each`/`map`)
|
||||
- `sequence->list` `s` → coerce any sequence to list
|
||||
- `sequence->vector` `s` → coerce any sequence to vector
|
||||
- `sequence-length` `s` → length of any sequence
|
||||
- `sequence-ref` `s` `i` → element by index (lists and vectors)
|
||||
- `sequence-append` `s1` `s2` → concatenate two same-type sequences
|
||||
|
||||
Steps:
|
||||
- [x] Spec: extend `map`/`filter`/`reduce`/`for-each`/`some`/`every?` in `spec/evaluator.sx`
|
||||
to type-dispatch; add `in-range` lazy sequence type + helpers.
|
||||
- [x] OCaml: update HO form dispatch; add `SxRange` or use lazy list; implement `sequence-*`
|
||||
primitives.
|
||||
seq_to_list helper before let-rec block; ho_setup_dispatch wraps all 7 coll bindings;
|
||||
seq-to-list/sequence-to-list/vector/length/ref/append/in-range in sx_primitives.ml.
|
||||
4385/1080 (all failures pre-existing hs-*/regex; 0 regressions).
|
||||
- [x] JS bootstrapper: update.
|
||||
Already done in Spec step (da4b526a) — sx-browser.js rebuilt with seqToList/sequenceToList/
|
||||
sequenceToVector/sequenceLength/sequenceRef/sequenceAppend/inRange. 2137/2500 JS tests pass.
|
||||
- [x] Tests: 30+ tests in `spec/tests/test-sequences.sx` — map over vector, filter over
|
||||
range, for-each over string chars, sequence-append, sequence->list/vector coercions.
|
||||
45 tests all passing: JS 2185/2498 (+48), OCaml 4424/1087 (+39). Fixed: vector? rename
|
||||
(isVector), vectorLength/vectorRef/reverse aliases, in-range letrec→build-range,
|
||||
sequence-length nil=0, assert-equal for list comparisons. Committed 0fe00bf7.
|
||||
- [x] Commit: `spec: sequence protocol — polymorphic map/filter/for-each over list/vector/range`
|
||||
Work landed across da4b526a (Spec), 7286629c (OCaml), 06a3eee1 (JS bootstrap), 0fe00bf7 (Tests).
|
||||
|
||||
---
|
||||
|
||||
## Phase 12 — gensym + symbol interning
|
||||
|
||||
Unique symbol generation. Tiny to implement; broadly needed: Prolog uses it for fresh
|
||||
variable names, Common Lisp uses it constantly in macros, any hygienic macro system needs
|
||||
it, and Smalltalk uses it for anonymous class/method naming.
|
||||
|
||||
Primitives to add:
|
||||
- `gensym` `[prefix]` → unique symbol, e.g. `g42`, `var-17`. Counter-based, monotonically increasing.
|
||||
- `symbol-interned?` `s` → bool — whether the symbol is in the global intern table
|
||||
- `intern` `str` → symbol — intern a string as a symbol (string->symbol already exists; this is
|
||||
the explicit interning operation for languages that distinguish interned vs uninterned)
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add `gensym` counter to evaluator state; implement in `spec/evaluator.sx`.
|
||||
`string->symbol` already exists — `gensym` is just a counter-suffixed variant.
|
||||
Added *gensym-counter*/gensym/string->symbol/symbol->string/intern/symbol-interned? to
|
||||
evaluator.sx. Added string->symbol/symbol->string transpiler renames + platform.py aliases.
|
||||
JS 2186/+1. OCaml builds. Committed edf4e525.
|
||||
- [x] OCaml: add global gensym counter; implement primitives.
|
||||
gensym_counter ref + gensym/string->symbol/symbol->string/intern/symbol-interned? in sx_primitives.ml.
|
||||
Also fixed ListRef case in seq_to_list (both sx_ref.ml + sx_primitives.ml). 4431/1080 (was 4385/1080).
|
||||
- [x] JS bootstrapper: implement.
|
||||
Already done in Spec step. JS 2186/2497, all sequence tests pass.
|
||||
- [x] Tests: 15+ tests in `spec/tests/test-gensym.sx` — uniqueness, prefix, symbol?, string->symbol round-trip.
|
||||
19 tests. OCaml 4450/1080, JS 2205/2497, zero regressions.
|
||||
- [x] Commit: `spec: gensym + symbol interning` — 0862a614
|
||||
|
||||
---
|
||||
|
||||
## Phase 13 — Character type
|
||||
|
||||
Common Lisp and Haskell have a distinct `Char` type that is not a string. Without it both
|
||||
implementations are approximations — CL's `#\a` literal and Haskell's `'a'` both need a
|
||||
real char value, not a length-1 string.
|
||||
|
||||
Primitives to add:
|
||||
- `char?` `v` → bool
|
||||
- `char->integer` `c` → Unicode codepoint integer
|
||||
- `integer->char` `n` → char
|
||||
- `char=?` `char<?` `char>?` `char<=?` `char>=?` → comparators
|
||||
- `char-ci=?` `char-ci<?` etc. → case-insensitive comparators
|
||||
- `char-alphabetic?` `char-numeric?` `char-whitespace?` → predicates
|
||||
- `char-upper-case?` `char-lower-case?` → predicates
|
||||
- `char-upcase` `char-downcase` → char → char
|
||||
- `string->list` extended to return chars (not length-1 strings)
|
||||
- `list->string` accepting chars
|
||||
|
||||
Also: `#\a` reader syntax for char literals (parser addition).
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add `SxChar` type to evaluator; add char literal syntax `#\a`/`#\space`/`#\newline`
|
||||
to `spec/parser.sx`; implement all predicates + comparators.
|
||||
- [x] OCaml: add `SxChar of char` to `sx_types.ml`; implement primitives.
|
||||
- [x] JS bootstrapper: implement char type wrapping a codepoint integer.
|
||||
- [x] Tests: 30+ tests in `spec/tests/test-chars.sx` — literals, char->integer round-trip,
|
||||
comparators, predicates, upcase/downcase, string<->list with chars.
|
||||
- [x] Commit: `spec: character type (char? char->integer #\\a literals + predicates)`
|
||||
|
||||
---
|
||||
|
||||
## Phase 14 — String ports
|
||||
|
||||
Needed for any language with a reader protocol: Common Lisp's `read`, Prolog's term parser,
|
||||
Smalltalk's `printString`. Without string ports these all do their own character walking
|
||||
on raw strings rather than treating a string as an I/O stream.
|
||||
|
||||
Primitives to add:
|
||||
- `open-input-string` `str` → input port
|
||||
- `open-output-string` → output port
|
||||
- `get-output-string` `port` → string (flush output port to string)
|
||||
- `input-port?` `output-port?` `port?` → predicates
|
||||
- `read-char` `[port]` → char or eof-object
|
||||
- `peek-char` `[port]` → char or eof-object (non-consuming)
|
||||
- `read-line` `[port]` → string or eof-object
|
||||
- `write-char` `char` `[port]` → void
|
||||
- `write-string` `str` `[port]` → void
|
||||
- `eof-object` → the eof sentinel
|
||||
- `eof-object?` `v` → bool
|
||||
- `close-port` `port` → void
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add port type + eof-object to evaluator; implement all primitives.
|
||||
Ports are mutable objects with a position cursor (input) or accumulation buffer (output).
|
||||
- [x] OCaml: add `SxPort` variant covering string-input-port and string-output-port;
|
||||
Buffer.t for output, string+offset for input.
|
||||
- [x] JS bootstrapper: implement port type.
|
||||
- [x] Tests: 25+ tests in `spec/tests/test-ports.sx` — open/read/peek/eof, output accumulation,
|
||||
read-line, write-char, close.
|
||||
- [x] Commit: `spec: string ports (open-input-string/open-output-string/read-char/etc)` — 3d8937d7
|
||||
|
||||
---
|
||||
|
||||
## Phase 15 — Math completeness
|
||||
|
||||
Filling specific gaps that multiple language implementations need.
|
||||
|
||||
### 15a — modulo / remainder / quotient distinction
|
||||
They differ on negative numbers — critical for Erlang `rem`, Haskell `mod`/`rem`, CL `mod`/`rem`:
|
||||
- `quotient` `a` `b` → truncate toward zero (same sign as dividend)
|
||||
- `remainder` `a` `b` → sign follows dividend (truncation division)
|
||||
- `modulo` `a` `b` → sign follows divisor (floor division) — R7RS
|
||||
|
||||
### 15b — Trigonometry and transcendentals
|
||||
Lua, Haskell, Erlang, CL all need: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `exp`,
|
||||
`log`, `sqrt`, `expt`. Check which are already present; add missing ones.
|
||||
|
||||
### 15c — GCD / LCM
|
||||
`gcd` `a` `b` → greatest common divisor; `lcm` `a` `b` → least common multiple.
|
||||
Needed by Haskell `Rational`, CL, and any language doing fraction arithmetic.
|
||||
|
||||
### 15d — Radix number parsing / formatting
|
||||
`(number->string n radix)` → e.g. `(number->string 255 16)` → `"ff"`.
|
||||
`(string->number s radix)` → e.g. `(string->number "ff" 16)` → `255`.
|
||||
Needed by: Common Lisp, Smalltalk, Erlang integer formatting.
|
||||
|
||||
Steps:
|
||||
- [x] Audit which trig / math functions are already in `spec/primitives.sx`; note gaps.
|
||||
- [x] Spec + OCaml + JS: implement missing trig (`sin`/`cos`/`tan`/`asin`/`acos`/`atan`/`exp`/`log`).
|
||||
- [x] Spec + OCaml + JS: `quotient`/`remainder`/`modulo` with correct negative semantics.
|
||||
- [x] Spec + OCaml + JS: `gcd`/`lcm`.
|
||||
- [x] Spec + OCaml + JS: radix variants of `number->string`/`string->number`.
|
||||
- [x] Tests: 40+ tests in `spec/tests/test-math.sx`.
|
||||
- [x] Commit: `spec: math completeness — trig, quotient/remainder/modulo, gcd/lcm, radix`
|
||||
|
||||
---
|
||||
|
||||
## Phase 16 — Rational numbers
|
||||
|
||||
Haskell's `Rational` type and Common Lisp ratios (`1/3`) both need this. Natural extension
|
||||
of the numeric tower (Phase 2) — rationals are the third numeric type alongside int and float.
|
||||
|
||||
Primitives to add:
|
||||
- `make-rational` `numerator` `denominator` → rational (auto-reduced by GCD)
|
||||
- `rational?` `v` → bool
|
||||
- `numerator` `r` → integer
|
||||
- `denominator` `r` → integer
|
||||
- Reader syntax: `1/3` parsed as rational literal
|
||||
- Arithmetic: `(+ 1/3 1/6)` → `1/2`; `(* 1/3 3)` → `1`; mixed int/rational → rational
|
||||
- `exact->inexact` on rational → float; `inexact->exact` on float → rational approximation
|
||||
- `(number->string 1/3)` → `"1/3"`
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add `SxRational` type; add `n/d` reader syntax to `spec/parser.sx`; extend
|
||||
all arithmetic primitives for rational contagion (int op rational → rational, rational
|
||||
op float → float).
|
||||
- [x] OCaml: add `SxRational of int * int` (stored in reduced form); implement all arithmetic.
|
||||
as_number + safe_eq extended for cross-type rational equality (= 2.5 5/2) → true.
|
||||
- [x] JS bootstrapper: implement rational type.
|
||||
JS keeps int/int → float for CSS backward compatibility; SxRational class with _rational marker.
|
||||
- [x] Tests: 30+ tests in `spec/tests/test-rationals.sx` — literals, arithmetic, reduction,
|
||||
mixed numeric tower, exact<->inexact conversion. 62 tests, all pass.
|
||||
- [x] Commit: `spec: rational numbers — 1/3 literals, arithmetic, numeric tower integration`
|
||||
Committed 036022cc. JS: 2232 passed. OCaml: 4532 passed (+11).
|
||||
|
||||
---
|
||||
|
||||
## Phase 17 — read / write / display
|
||||
|
||||
Completes the I/O model. Builds on string ports (Phase 14) and char type (Phase 13).
|
||||
`read` parses any SX value from a port; `write` serializes with quoting (round-trippable);
|
||||
`display` serializes without quoting (human-readable). Common Lisp's `read` macro,
|
||||
Prolog term I/O, and Smalltalk's `printString` all need this.
|
||||
|
||||
Primitives to add:
|
||||
- `read` `[port]` → SX value or eof-object — full SX parser reading from a port
|
||||
- `read-char` already in Phase 14; `read` uses it internally
|
||||
- `write` `val` `[port]` → void — serializes with quotes: `"hello"`, `#\a`, `(1 2 3)`
|
||||
- `display` `val` `[port]` → void — serializes without quotes: `hello`, `a`, `(1 2 3)`
|
||||
- `newline` `[port]` → void — writes `\n`
|
||||
- `write-to-string` `val` → string — convenience: `(write val (open-output-string))`
|
||||
- `display-to-string` `val` → string — convenience
|
||||
|
||||
Steps:
|
||||
- [x] Spec: implement `read` in `spec/evaluator.sx` — wraps the existing parser to read
|
||||
one datum from a port cursor; handles eof gracefully.
|
||||
- [x] Spec: implement `write`/`display`/`newline` — extend the existing serializer for
|
||||
port output; `write` quotes strings + uses `#\` for chars, `display` does not.
|
||||
- [x] OCaml: wire `read` through port type; implement `write`/`display` output path.
|
||||
- [x] JS bootstrapper: implement.
|
||||
- [x] Tests: 25+ tests in `spec/tests/test-read-write.sx` — read string literal, read list,
|
||||
read eof, write round-trip, display vs write quoting, newline, write-to-string.
|
||||
- [x] Commit: `spec: read/write/display — S-expression reader/writer on ports`
|
||||
|
||||
---
|
||||
|
||||
## Phase 18 — Sets
|
||||
|
||||
O(1) membership testing. Distinct from hash tables (unkeyed) and lists (O(n)).
|
||||
Erlang has sets as a stdlib staple, Haskell `Data.Set`, APL uses set operations
|
||||
constantly, Common Lisp has `union`/`intersection` on lists but a native set is O(1).
|
||||
|
||||
Primitives to add:
|
||||
- `make-set` `[list]` → fresh set, optionally seeded from list
|
||||
- `set?` `v` → bool
|
||||
- `set-add!` `s` `val` → void
|
||||
- `set-member?` `s` `val` → bool
|
||||
- `set-remove!` `s` `val` → void
|
||||
- `set-size` `s` → integer
|
||||
- `set->list` `s` → list (unspecified order)
|
||||
- `list->set` `lst` → set
|
||||
- `set-union` `s1` `s2` → new set
|
||||
- `set-intersection` `s1` `s2` → new set
|
||||
- `set-difference` `s1` `s2` → new set (elements in s1 not in s2)
|
||||
- `set-for-each` `s` `fn` → iterate for side effects
|
||||
- `set-map` `s` `fn` → new set of mapped values
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add entries to `spec/primitives.sx`.
|
||||
- [x] OCaml: implement using `Hashtbl.t` with unit values (or a proper `Set` functor
|
||||
with a comparison function); add `SxSet` to `sx_types.ml`.
|
||||
- [x] JS bootstrapper: implement using JS `Set`.
|
||||
- [x] Tests: 30+ tests in `spec/tests/test-sets.sx` — add/member/remove, union/intersection/
|
||||
difference, list conversion, for-each, size.
|
||||
- [x] Commit: `spec: sets (make-set/set-add!/set-member?/union/intersection/etc)`
|
||||
|
||||
---
|
||||
|
||||
## Phase 19 — Regular expressions as primitives
|
||||
|
||||
`lib/js/regex.sx` is a pure-SX regex engine already written. Promoting it to a primitive
|
||||
gives every language free regex without reinventing: Lua patterns, Tcl `regexp`, Ruby regex,
|
||||
JS regex, Erlang `re` module. Mostly a wiring job — the implementation exists.
|
||||
|
||||
Primitives to add:
|
||||
- `make-regexp` `pattern` `[flags]` → regexp object (`flags`: `"i"` case-insensitive, `"g"` global, `"m"` multiline)
|
||||
- `regexp?` `v` → bool
|
||||
- `regexp-match` `re` `str` → match dict `{:match "..." :start N :end N :groups (...)}` or nil
|
||||
- `regexp-match-all` `re` `str` → list of match dicts
|
||||
- `regexp-replace` `re` `str` `replacement` → string with first match replaced
|
||||
- `regexp-replace-all` `re` `str` `replacement` → string with all matches replaced
|
||||
- `regexp-split` `re` `str` → list of strings (split on matches)
|
||||
- Reader syntax: `#/pattern/flags` for regexp literals (parser addition)
|
||||
|
||||
Steps:
|
||||
- [x] Audit `lib/js/regex.sx` — understand the API it already exposes; map to the
|
||||
primitive API above.
|
||||
- [x] Spec: add `SxRegexp` type to evaluator; add `#/pattern/flags` literal syntax to
|
||||
`spec/parser.sx`; wire `lib/js/regex.sx` engine as the implementation.
|
||||
- [x] OCaml: implement using OCaml `Re` library (or `Str`); add `SxRegexp` to types.
|
||||
- [x] JS bootstrapper: use native JS `RegExp`; wrap in the primitive API.
|
||||
- [x] Tests: 30+ tests in `spec/tests/test-regexp.sx` — basic match, groups, replace,
|
||||
replace-all, split, flags (case-insensitive), no-match nil return.
|
||||
- [x] Commit: `spec: regular expressions (make-regexp/regexp-match/regexp-replace + #/pat/ literals)`
|
||||
|
||||
---
|
||||
|
||||
## Phase 20 — Bytevectors
|
||||
|
||||
R7RS standard. Needed for WebSocket binary frames (E36), binary protocol parsing, and
|
||||
efficient string encoding. Also the foundation for proper Unicode: `string->utf8` /
|
||||
`utf8->string` require a byte array type.
|
||||
|
||||
Primitives to add:
|
||||
- `make-bytevector` `n` `[fill]` → bytevector of n bytes (fill defaults to 0)
|
||||
- `bytevector?` `v` → bool
|
||||
- `bytevector-length` `bv` → integer
|
||||
- `bytevector-u8-ref` `bv` `i` → byte 0–255
|
||||
- `bytevector-u8-set!` `bv` `i` `byte` → void
|
||||
- `bytevector-copy` `bv` `[start]` `[end]` → fresh copy
|
||||
- `bytevector-copy!` `dst` `at` `src` `[start]` `[end]` → in-place copy
|
||||
- `bytevector-append` `bv...` → concatenated bytevector
|
||||
- `utf8->string` `bv` `[start]` `[end]` → string decoded as UTF-8
|
||||
- `string->utf8` `str` `[start]` `[end]` → bytevector UTF-8 encoded
|
||||
- `bytevector->list` / `list->bytevector` → conversion
|
||||
|
||||
Steps:
|
||||
- [x] Spec: add `SxBytevector` type; implement all primitives in `spec/evaluator.sx` / `spec/primitives.sx`.
|
||||
- [x] OCaml: add `SxBytevector of bytes` to `sx_types.ml`; implement primitives using
|
||||
OCaml `Bytes`.
|
||||
- [x] JS bootstrapper: implement using `Uint8Array`.
|
||||
- [x] Tests: 30+ tests in `spec/tests/test-bytevectors.sx` — construction, ref/set, copy,
|
||||
append, utf8 round-trip, slice.
|
||||
- [x] Commit: `spec: bytevectors (make-bytevector/u8-ref/u8-set!/utf8->string/etc)`
|
||||
|
||||
---
|
||||
|
||||
## Phase 21 — format
|
||||
|
||||
CL-style string formatting beyond `str`. `(format "Hello ~a, age ~d" name age)`.
|
||||
Haskell `printf`, Erlang `io:format`, CL `format`, and general string templating all use this idiom.
|
||||
|
||||
Directives:
|
||||
- `~a` — display (no quotes)
|
||||
- `~s` — write (with quotes)
|
||||
- `~d` — decimal integer
|
||||
- `~x` — hexadecimal integer
|
||||
- `~o` — octal integer
|
||||
- `~b` — binary integer
|
||||
- `~f` — fixed-point float
|
||||
- `~e` — scientific notation float
|
||||
- `~%` — newline
|
||||
- `~&` — fresh line (newline only if not already at start of line)
|
||||
- `~~` — literal tilde
|
||||
- `~t` — tab
|
||||
|
||||
Signature: `(format template arg...)` → string.
|
||||
Optional: `(format port template arg...)` — write to port directly.
|
||||
|
||||
Steps:
|
||||
- [x] Spec: implement `format` as a pure SX function in `spec/stdlib.sx` — parses
|
||||
`~X` directives, dispatches to `display`/`write`/`number->string` as appropriate.
|
||||
Pure SX: no host calls needed. Self-hosting — uses string-buffer (Phase 5) internally.
|
||||
- [x] OCaml: expose as a primitive (or let it run as SX through the evaluator).
|
||||
Added format-decimal OCaml primitive; fixed lib/r7rs.sx number->string to support radix.
|
||||
- [x] JS bootstrapper: same.
|
||||
- [x] Tests: 28 tests in `spec/tests/test-format.sx` — each directive, multiple args,
|
||||
nested format, `~~` escape. 28/28 pass on both JS and OCaml.
|
||||
- [x] Commit: `spec: format — CL-style string formatting (~a ~s ~d ~x ~% etc)` — 4d7b3e29
|
||||
|
||||
---
|
||||
|
||||
## Phase 22 — Language sweep
|
||||
|
||||
Replace workarounds with primitives. One language per fire (or per sub-item for big ones).
|
||||
Start with blank slates (CL, APL, Ruby, Tcl) — they haven't committed to workarounds yet.
|
||||
|
||||
**Scope per language:** only `lib/<lang>/**`. Don't touch spec or other languages.
|
||||
Brief each language's loop agent (or do inline) after rebasing their branch onto architecture.
|
||||
|
||||
- [x] Restart CL/APL/Ruby/Tcl loops with updated briefing pointing to new primitives.
|
||||
Added `## SX primitive baseline` section to plans/common-lisp-on-sx.md,
|
||||
plans/apl-on-sx.md, plans/ruby-on-sx.md, plans/tcl-on-sx.md. f43659ce.
|
||||
|
||||
- [x] Common Lisp: char type (`#\a`); string ports + `read`/`write` for reader/printer;
|
||||
gensym for macros; rational numbers for CL ratios; multiple values; sets for CL set ops;
|
||||
`modulo`/`remainder`/`quotient`; radix formatting; `format` for `cl:format`.
|
||||
lib/common-lisp/runtime.sx (103 forms) + test.sh (68/68 pass). 1ad8e74a.
|
||||
|
||||
- [x] Lua: vectors for arrays; hash tables for Lua tables; `delay`/`force` for lazy iterators;
|
||||
regexp for Lua pattern matching; trig from math completeness; bytevectors for binary I/O.
|
||||
math/string/table stdlib tables + lua-force. 185/185 pass. ec3512d6.
|
||||
|
||||
- [x] Erlang: numeric tower for float/int; bitwise ops for bitmatch; multiple values for
|
||||
multi-return; sets for Erlang sets; `remainder` for `rem`; regexp for `re` module.
|
||||
lib/erlang/runtime.sx (63 forms) + test.sh (55/55 pass). 3c0a9632.
|
||||
|
||||
- [x] Haskell: numeric tower for `Num`/`Integral`/`Fractional`; promises for lazy evaluation
|
||||
(critical); multiple values for tuples; rational numbers for `Rational`; char type for
|
||||
`Char`; `gcd`/`lcm`; sets for `Data.Set`; `read`/`write` for `Show`/`Read` instances.
|
||||
lib/haskell/runtime.sx (113 forms) + tests/runtime.sx (143/143 pass). c02ffcf3.
|
||||
|
||||
- [x] JS: vectors for Array; hash tables for `Map`; sets for `Set`; bitwise ops for typed
|
||||
arrays; regexp for JS regex; bytevectors for `Uint8Array`; radix formatting.
|
||||
lib/js/stdlib.sx (36 forms) + test.sh epochs 6000-6032 (25/25 pass). COMMIT.
|
||||
|
||||
- [x] Smalltalk: vectors for `Array new:`; hash tables for `Dictionary new`; sets for
|
||||
`Set new`; char type for `Character`; string ports + `read`/`write` for `printString`.
|
||||
lib/smalltalk/runtime.sx (72 forms) + tests/runtime.sx (86/86 pass). COMMIT.
|
||||
|
||||
- [x] APL: vectors as core array type; bitwise ops for array masks; sets for APL set ops;
|
||||
sequence protocol for rank-polymorphic operations; format for APL output formatting.
|
||||
lib/apl/runtime.sx (60 forms) + tests/runtime.sx (73/73 pass). COMMIT.
|
||||
|
||||
- [x] Ruby: coroutines for fibers; hash tables for `Hash`; sets for `Set`; regexp for
|
||||
Ruby regex; string ports for `StringIO`; bytevectors for `String` binary encoding.
|
||||
lib/ruby/runtime.sx (61 forms) + tests/runtime.sx (76/76 pass). COMMIT.
|
||||
Note: rb-fiber-yield from letrec-bound lambdas fails (JIT VM can't invoke callcc
|
||||
continuations as escapes); workaround: use top-level helper fns for recursive yields.
|
||||
|
||||
- [x] Tcl: string ports for Tcl channel abstraction; string-buffer for `append`; coroutines
|
||||
for Tcl coroutines; regexp for Tcl `regexp`; format for Tcl `format`.
|
||||
lib/tcl/runtime.sx (37 forms) + tests/runtime.sx (56/56 pass). COMMIT.
|
||||
|
||||
- [x] Forth: bitwise ops (core); string-buffer for word-definition accumulation; bytevectors
|
||||
for Forth's raw memory model.
|
||||
lib/forth/runtime.sx (36 forms) + tests/runtime.sx (64/64 pass). COMMIT.
|
||||
|
||||
---
|
||||
|
||||
## Ground rules
|
||||
|
||||
- Work on the `architecture` branch in `/root/rose-ash` (main worktree).
|
||||
- Use sx-tree MCP for all `.sx` file edits. Never use raw Edit/Write/Read on `.sx` files.
|
||||
- Commit after each concrete unit of work. Never leave the branch broken.
|
||||
- Never push to `main` — only push to `origin/architecture`.
|
||||
- Update this checklist every fire: tick `[x]` done, add inline notes on blockers.
|
||||
|
||||
---
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
- 2026-05-01: Phase 22 Forth done — runtime.sx (36 forms): bitwise (AND/OR/XOR/INVERT/LSHIFT/RSHIFT/2*/2//bit-count/integer-length/within + arithmetic helpers), string-buffer (emit!/type!/value/length/clear!/emit-int!), memory (cfetch/cstore/fetch/store/move!/fill!/erase!/mem->list). 64/64 tests. 8019e572.
|
||||
- 2026-05-01: Phase 22 Tcl done — runtime.sx (37 forms): string-buffer (append accumulator), channel (read/write ports with gets/read/puts), regexp (make-regexp wrappers), format (%s/%d/%f/%x/%o/%% manual char scan), coroutine (call/cc, top-level helper pattern). 56/56 tests. 3e07727d.
|
||||
- 2026-05-01: Phase 22 Ruby done — runtime.sx (61 forms): Hash (list-of-pairs dict-backed), Set (make-set, (set item) order), Regexp (make-regexp wrappers), StringIO (write buf + rewind/char read), Bytevectors (thin wrappers), Fiber (call/cc; letrec JIT workaround: use top-level helpers). 76/76 tests. 182e6f63.
|
||||
|
||||
- 2026-05-01: Phase 22 APL done — runtime.sx (60 forms): iota/rho/at, rank-polymorphic dyadic/monadic helpers, arithmetic/comparison/boolean/bitwise element-wise, reduce/scan, take/drop/rotate/compress/index, set ops (member/nub/union/intersect/without), format. 73/73 tests. COMMIT.
|
||||
- 2026-05-01: Phase 22 Smalltalk done — runtime.sx (72 forms): numeric helpers, Character (1-indexed Array backed by dict), Dictionary (list-of-pairs any-key map), Set (make-set), WriteStream/ReadStream/printString. set-member? (set item) order. 86/86 tests. COMMIT.
|
||||
- 2026-05-01: Phase 22 JS done — stdlib.sx (36 forms): bitwise (truncate not js-num-to-int; set-member? takes (set item) order), Map (dict-backed pairs), Set (SX make-set), RegExp (callable lambda). 25/25 new tests pass; total 492/585. COMMIT.
|
||||
- 2026-05-01: Phase 22 Haskell done — runtime.sx (113 forms): numeric tower (hk-div floor semantics), rational (dict GCD-normalised), hk-force (promises), Data.Char, Data.Set, Data.List, Maybe/Either, tuples, string helpers, hk-show. 148/148 tests. c02ffcf3.
|
||||
- 2026-05-01: Phase 22 Erlang done — runtime.sx (63 forms): numeric tower, bitwise (band/bor/bxor/bnot/bsl/bsr), sets, re module, list BIFs, type conversions, ok/error tuples. 55/55 tests. 3c0a9632.
|
||||
- 2026-05-01: Phase 22 Lua done — math/string/table stdlib tables + lua-force in lib/lua/runtime.sx. 185/185 tests (28 new). ec3512d6.
|
||||
- 2026-05-01: Phase 22 CL done — runtime.sx (103 forms): type preds, arithmetic, chars, format, gensym, values, sets, radix, list utils. cl-empty? guards nil/() split. 68/68 tests. 1ad8e74a.
|
||||
- 2026-05-01: Phase 22 step 1 — SX primitive baseline added to CL/APL/Ruby/Tcl plans. f43659ce.
|
||||
- 2026-05-01: Phase 21 complete — format (~a ~s ~d ~x ~o ~b ~f ~% ~& ~~ ~t) as pure SX in spec/stdlib.sx. Fixed lib/r7rs.sx number->string to support optional radix; added format-decimal OCaml primitive. 28/28 tests on both JS and OCaml. 4d7b3e29.
|
||||
- 2026-04-26: Phase 7 complete — bitwise-and/or/xor/not + arithmetic-shift + bit-count + integer-length. OCaml: land/lor/lxor/lnot/lsl/asr + Kernighan popcount + lsr loop for integer-length. JS: bitwise ops + Hamming weight + Math.clz32. 26 tests, 158 assertions, all pass. a8a79dc9.
|
||||
- 2026-04-26: Phase 6 complete — JS+Tests+Commit all ticked. JS needed no changes (spec-level forms). 40/40 ADT tests pass JS. 2032/2500 JS total (+67 vs phase-4). Phase 6 fully landed: 6c872107+0dc7e159+5d1913e7. Phase 7 (bitwise) next.
|
||||
- 2026-04-26: Phase 6 OCaml done — Dict-based ADT (no native SxAdt type needed); hand-written sf_define_type in bootstrap.py FIXUPS (skipped from transpile — &rest params + empty-dict {} literals); registered via register_special_form; step_limit/step_count added to PREAMBLE. 172 assertions pass (test-adt). Full suite 4280/1080 (was 4243/1117, +37). Committed 5d1913e7.
|
||||
- 2026-04-26: Phase 6 Spec match done — ADT case added to match-pattern in spec/evaluator.sx: checks (list? pattern)+(symbol? first)+(dict? value)+(get value :_adt), then matches :_ctor+arity and recursively binds field patterns. No-clause error now uses make-cek-value+raise-eval-frame so guard can catch it. 20 new match tests pass; 40/40 total ADT tests green. Zero regressions.
|
||||
- 2026-04-26: Phase 6 Spec define-type done — sf-define-type registered via register-special-form! in spec/evaluator.sx; AdtValue as {:_adt true :_type "..." :_ctor "..." :_fields (list ...)}; ctor fns + arity checking + Name?/Ctor? predicates + Ctor-field accessors; *adt-registry* dict populated per define-type call. 20/20 JS tests pass in spec/tests/test-adt.sx. OCaml define-type is next task.
|
||||
- 2026-04-26: Phase 6 Design done — plans/designs/sx-adt.md written. Covers define-type/match syntax, AdtValue CEK runtime, stepSfDefineType+MatchFrame dispatch, exhaustiveness warnings, recursive types, nested patterns, wildcard _. 3-phase impl plan. Next fire: Spec implement define-type.
|
||||
- 2026-04-26: Phase 5 complete — string buffer fully landed (d98b5fa2). 17 tests, 17/17 OCaml+JS. Phase 6 (ADTs) next.
|
||||
- 2026-04-26: Phase 5 Spec+OCaml+JS step done — StringBuffer of Buffer.t in sx_types.ml; make-string-buffer/append!/->string/length/string-buffer? in sx_primitives.ml; SxStringBuffer with _string_buffer marker + typeOf/dict? fixes in platform.py; JS rebuilt. 17/17 tests OCaml+JS.
|
||||
- 2026-04-26: Phase 4 complete — coroutine primitive fully landed (4 commits: spec library + OCaml verified + JS pre-load + 27 tests). Phase 5 (string buffer) next.
|
||||
- 2026-04-26: Phase 4 Tests step done — 27 tests total (10 new: state field inspection, yield-from-helper, initial-arg-ignored, mutable-closure, complex-values, round-robin, factory-no-state, non-coroutine-error). 27/27 OCaml+JS.
|
||||
- 2026-04-26: Phase 4 JS step done — all CEK primitives already in sx-browser.js; fix was pre-loading spec/coroutines.sx+spec/signals.sx in run_tests.js so (import (sx coroutines)) resolves synchronously. 17/17 coroutine tests pass JS. 1965/2500 total (+25), zero new failures.
|
||||
- 2026-04-26: Phase 4 OCaml step done — no native SxCoroutine type needed; existing cek-step-loop/cek-resume/perform/make-cek-state primitives in run_tests.ml fully support the spec/coroutines.sx library. 284/284 pass (coroutines+vectors+numeric-tower+dynamic-wind), zero regressions.
|
||||
- 2026-04-26: Phase 4 Spec step done — spec/coroutines.sx define-library with make-coroutine/coroutine-resume/coroutine-yield/coroutine?/coroutine-alive?; make-coroutine stub in evaluator.sx; 17/17 coroutine tests pass (OCaml). Key insight: coroutine body must use (define loop (fn...)) + (loop 0) not named let — named let uses cek_call→cek_run which errors on IO suspension.
|
||||
- 2026-05-01: Phase 10 complete — mutable hash tables. HashTable variant in OCaml; JS Map-based SxHashTable. 11 primitives: make-hash-table/hash-table?/set!/ref/delete!/size/keys/values/->alist/for-each/merge!. 28 tests, all pass OCaml+JS. 133bdf52.
|
||||
- 2026-05-01: Phase 9 complete — delay/force/delay-force/make-promise/promise?. Dict-based promise {:_promise :forced :thunk :value}; :_iterative flag for delay-force chain following. 25/25 tests OCaml (4357) and JS (2109). Committed e44cb89a.
|
||||
- 2026-05-01: Phase 8 complete — values/call-with-values/let-values/define-values. Dict marker {:_values true :_list [...]} (no new type). step-sf-define desugars shorthand (define (f x) body) on both hosts. 25/25 tests OCaml+JS. Committed 43cc1d90.
|
||||
- 2026-04-26: Phase 3 complete — OCaml+JS done. CallccContinuation gains winders-depth int; make_callcc_continuation/callcc_continuation_winders_len wired; wind-after/wind-return CekFrame fields fixed (cf_f=after-thunk, cf_extra=winders-len, cf_name=body-result); get_val + transpiler.sx updated. 8/8 dynamic-wind tests pass on OCaml; 235/235 (callcc+guard+do+r7rs) zero regressions. Committed 6602ec8c.
|
||||
- 2026-04-26: Phase 3 Spec+Tests done — dynamic-wind CEK implementation: wind-after/wind-return frames, *winders* stack, kont-unwind-to-handler, wind-escape-to. callcc frame stores winders-len in continuation; callcc-continuation? calls wind-escape-to before escape. 8/8 dynamic-wind tests pass (normal return, raise, call/cc, nested LIFO, guard ordering). 1948/2500 JS (+8). Zero regressions. Committed a9d5a108.
|
||||
- 2026-04-26: Phase 2 complete — Verify+Commit done. OCaml 4874/394, JS 1940/2500 (+60). No regressions. 6 JS-only failures are float≡int platform-inherent. Phase 2 fully landed across 4 commits.
|
||||
- 2026-04-26: Phase 2 JS bootstrapper done — integer?/float?/exact?/inexact? added (Number.isInteger); truncate/remainder/modulo/random-int/exact->inexact/inexact->exact/parse-number added. Fixed sx_server.ml epoch+blob+io-response protocol for Integer type. JS: 1940/2500 (+60). OCaml: 4874/394 baseline. 6 JS tests fail (JS float≡int platform limit). Committed b12a22e6.
|
||||
- 2026-04-26: Phase 2 Spec done — integer?/float? predicates added to spec/primitives.sx; floor/ceil/truncate :returns updated to "integer"; / to "float"; exact->inexact/inexact->exact docs and returns updated; float contagion documented on +/-/*; 4874/394 baseline. Committed 45ec5535.
|
||||
- 2026-04-26: Phase 2 OCaml+Tests done — `Integer of int` / `Number of float` in sx_types.ml; float contagion across all arithmetic; floor/truncate/round → Integer; integer?/float?/exact?/inexact?/exact->inexact/inexact->exact; 92/92 numeric tower tests pass; 4874 total (394 pre-existing unchanged). Committed c70bbdeb.
|
||||
- 2026-04-26: Phase 1 complete — JS step done. Fixed fundamental lambda binding bug (index-of on arrays returned -1 not NIL, making bind-lambda-params mis-fire &rest branch). Added _lastErrorKont_/hostError/try-catch stubs. 42/42 vector tests pass. 1847 std / 2362 full passing (up from 5). Committed.
|
||||
- 2026-04-25: Phase 1 spec step done — all 10 vector primitives in spec/primitives.sx have full :as type annotations, :returns, :doc; make-vector optional fill param added.
|
||||
- 2026-04-25: Phase 1 OCaml step done — bounds-checked vector-ref/set!, vector-copy now accepts optional start/end, spec/primitives.sx doc updated. 10/10 r7rs vector tests pass, 4747 total (394 pre-existing hs-upstream fails unchanged).
|
||||
- 2026-04-25: Phase 0 complete — stopped CL/APL/Ruby/Tcl loops (all 4 idle at shell); confirmed E38 (tokenizer :end/:line) and E39 (WebWorker stub) both have implementation commits.
|
||||
- 2026-05-01: Phase 20 complete — bytevectors. SxBytevector of bytes in OCaml using Bytes; Uint8Array-backed SxBytevector in JS. 12 primitives: make-bytevector, bytevector?, bytevector-length, bytevector-u8-ref, bytevector-u8-set!, bytevector-copy, bytevector-copy!, bytevector-append, utf8->string, string->utf8, bytevector->list, list->bytevector. 32 tests, all pass. JS 2535, OCaml 4725. a3811545.
|
||||
- 2026-05-01: Phase 19 complete — regular expressions. SxRegexp(src,flags,Re.re) in OCaml via Re.Pcre; SxRegexp wrapper around JS RegExp. 9 primitives: make-regexp, regexp?, regexp-source, regexp-flags, regexp-match, regexp-match-all, regexp-replace, regexp-replace-all, regexp-split. Match dicts with :match/:start/:end/:groups. 32 tests, all pass. JS 2503, OCaml 4693. d8d5588e.
|
||||
- 2026-05-01: Phase 18 complete — sets. SxSet as (string,value) Hashtbl keyed by inspect(val) in OCaml; Map keyed by write-to-string in JS. 13 primitives: make-set, set?, set-add!, set-member?, set-remove!, set-size, set->list, list->set, set-union, set-intersection, set-difference, set-for-each, set-map. 33 tests, all pass. JS 2469, OCaml 4659. 3b0ac67a.
|
||||
- 2026-05-01: Phase 17 complete — read/write/display. OCaml: sx_write_val/sx_display_val helpers; read via Sx_parser.read_value with #t/#f and N/D rational support added to parser; postprocess ()→Nil. JS: sxReadNormalize (#t/#f→true/false), sxReadConvert (()→NIL), sxEq list equality, sxWriteVal symbol/keyword name fix (v.name not v._sym), readerMacroGet registry. 42 tests (test-read-write.sx), all pass both hosts. JS 2436, OCaml 4626. 7d329f02.
|
||||
- 2026-05-01: Phase 16 complete — rational numbers. SxRational type in OCaml (Rational of int*int, reduced, denom>0) and JS (SxRational class, _rational marker). n/d reader in spec/parser.sx. Arithmetic contagion: int op rational → rational, rational op float → float. JS keeps int/int → float for CSS compat. OCaml as_number+safe_eq extended for cross-type rational equality. 62 tests in test-rationals.sx, all pass. JS 2232, OCaml 4532 (+11). 036022cc.
|
||||
- 2026-05-01: Phase 15 complete — math completeness. stdlib.math module: sin/cos/tan/asin/acos/atan(1-2 args)/exp/log/expt/quotient/gcd/lcm/number->string(radix)/string->number(radix). OCaml atan updated for optional 2nd arg. Strict radix parsing in JS string->number. 44 tests in test-math.sx, all pass. JS 2311/4801, OCaml 4547/5629. be2b11ac.
|
||||
- 2026-05-01: Phase 14 OCaml done — Eof + Port{PortInput/PortOutput} in sx_types.ml; 15 port primitives in sx_primitives.ml; raw_serialize updated; 4532/4532 (+39, zero regressions). 8ba0a33f.
|
||||
- 2026-05-01: Phase 14 Spec+JS+Tests+Commit done — port type {_port,_kind,_source/_buffer,_pos,_closed}; eof singleton; 15 primitives in spec/primitives.sx (stdlib.ports) + platform.py; 39/39 tests in test-ports.sx. Committed 3d8937d7. OCaml step next.
|
||||
- 2026-05-01: Phase 13 OCaml done — Char of int in sx_types.ml; #\ reader in sx_parser.ml; all char primitives in sx_primitives.ml; fixed get_val for Integer n list indexing (was Number-only); fixed raw_serialize for Integer/Char. 4493/4493 (+43, zero regressions). b939becd.
|
||||
- 2026-05-01: Phase 13 Spec+JS+Tests+Commit done — SxChar tagged {_char,codepoint}; char? char->integer integer->char char-upcase/downcase; 10 comparators (ordered+ci); 5 predicates; string->list/list->string as platform primitives; #\a #\space #\newline reader syntax in spec/parser.sx; js-char-renames dict in transpiler.sx; 43/43 tests pass JS (2254/4745). Committed 4b600f17. OCaml step next.
|
||||
- 2026-05-01: Phase 12 complete — gensym + symbol interning. gensym_counter/gensym/string->symbol/symbol->string/intern/symbol-interned? in spec + OCaml + JS. Fixed ListRef case in seq_to_list (both hosts). 19 tests, all pass. OCaml 4450/1080, JS 2205/2497. Commits: edf4e525 Spec, 0862a614 OCaml+Tests.
|
||||
- 2026-05-01: Phase 11 complete — sequence protocol done. Commits: da4b526a Spec, 7286629c OCaml, 06a3eee1 JS, 0fe00bf7 Tests. JS 2185/+48, OCaml 4424/+39.
|
||||
- 2026-05-01: Phase 11 Tests done — 45 tests in test-sequences.sx all passing (JS 2185/+48, OCaml 4424/+39). Fixed vector? rename, vectorLength/vectorRef/reverse aliases, in-range letrec→build-range, sequence-length nil, assert-equal for lists. Committed 0fe00bf7.
|
||||
- 2026-05-01: Phase 11 JS bootstrapper step done — confirmed sx-browser.js current (built in Spec step da4b526a); 19 sequence primitive refs in output; 2137/2500 JS tests passing.
|
||||
- 2026-05-01: Phase 11 OCaml step done — seq_to_list helper added before let-rec; ho_setup_dispatch wraps all 7 coll bindings with seq_to_list; seq-to-list/sequence-to-list/to-vector/length/ref/append + in-range primitives in sx_primitives.ml. 4385/4385 baseline unchanged, 0 regressions. Committed 7286629c.
|
||||
- 2026-05-01: Phase 11 Spec step done — seq-to-list coercion helper; ho-setup-dispatch extended with seqToList on all collection args; sequence-to-list/vector/length/ref/append + in-range added to evaluator.sx. Restored 3 accidentally-deleted make-cek-state/value/suspended definitions. Fixed 8 shorthand define forms + added vector->list/list->vector transpiler renames. JS: 2137 passing (+28 vs HEAD baseline of 2109).
|
||||
@@ -11,7 +11,7 @@ isolation: worktree
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/prolog-on-sx.md`. You run in an isolated git worktree. You work the plan's roadmap forever, one commit per feature. Push to `origin/loops/prolog` after every commit.
|
||||
You are the sole background agent working `/root/rose-ash/plans/prolog-on-sx.md`. You run in an isolated git worktree. You work the plan's roadmap forever, one commit per feature. You never push.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
@@ -39,13 +39,12 @@ Every iteration: implement → test → commit → tick `[ ]` in plan → append
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/prolog/**` and `plans/prolog-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Prolog primitives go in `lib/prolog/runtime.sx`. You may **read** `lib/hyperscript/runtime.sx` to understand the hook API but do not edit it — `hs-set-prolog-hook!` is already implemented there.
|
||||
- **Hyperscript bridge is NOT blocked:** `lib/prolog/hs-bridge.sx` already exists and `lib/hyperscript/runtime.sx` already exports `hs-set-prolog-hook!` / `hs-prolog-hook`. The Phase 5 DSL item just needs tests and wiring.
|
||||
- **Scope:** only `lib/prolog/**` and `plans/prolog-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Prolog primitives go in `lib/prolog/runtime.sx`.
|
||||
- **NEVER call `sx_build`.** 600s watchdog will kill you before OCaml finishes. If sx_server binary is broken, add Blockers entry and stop.
|
||||
- **Shared-file issues** → plan's Blockers section with a minimal repro. Don't fix them.
|
||||
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5 (IO suspension via `perform`/`cek-resume`). `sx_summarise` spec/evaluator.sx first — it's 2300+ lines.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits. Never `Edit`/`Read`/`Write` on `.sx`.
|
||||
- **Worktree:** commit, then push to `origin/loops/prolog`. Never touch `main`.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
- **If blocked** for two iterations on the same issue, add to Blockers and move on.
|
||||
|
||||
83
plans/agent-briefings/ruby-loop.md
Normal file
83
plans/agent-briefings/ruby-loop.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# ruby-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/ruby-on-sx.md` forever. Fibers via delcc is the headline showcase — `Fiber.new`/`Fiber.yield`/`Fiber.resume` are textbook delimited continuations with sugar, where MRI does it via C-stack swapping. Plus blocks/yield (lexical escape continuations, same shape as Smalltalk's non-local return), method_missing, and singleton classes.
|
||||
|
||||
```
|
||||
description: ruby-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/ruby-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/ruby-on-sx.md` — roadmap + Progress log.
|
||||
2. `ls lib/ruby/` — pick up from the most advanced file.
|
||||
3. If `lib/ruby/tests/*.sx` exist, run them. Green before new work.
|
||||
4. If `lib/ruby/scoreboard.md` exists, that's your baseline.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/ruby-on-sx.md`:
|
||||
|
||||
- **Phase 1** — tokenizer + parser. Keywords, identifier sigils (`@` ivar, `@@` cvar, `$` global), strings with interpolation, `%w[]`/`%i[]`, symbols, blocks `{|x| …}` and `do |x| … end`, splats, default args, method def
|
||||
- **Phase 2** — object model + sequential eval. Class table, ancestor-chain dispatch, `super`, singleton classes, `method_missing` fallback, dynamic constant lookup
|
||||
- **Phase 3** — blocks + procs + lambdas. Method captures escape continuation `^k`; `yield` / `return` / `break` / `next` / `redo` semantics; lambda strict arity vs proc lax
|
||||
- **Phase 4** — **THE SHOWCASE**: fibers via delcc. `Fiber.new`/`Fiber.resume`/`Fiber.yield`/`Fiber.transfer`. Classic programs (generator, producer-consumer, tree-walk) green
|
||||
- **Phase 5** — modules + mixins + metaprogramming. `include`/`prepend`/`extend`, `define_method`, `class_eval`/`instance_eval`, `respond_to?`/`respond_to_missing?`, hooks
|
||||
- **Phase 6** — stdlib drive. `Enumerable` mixin, `Comparable`, Array/Hash/Range/String/Integer methods, drive corpus to 200+
|
||||
|
||||
Within a phase, pick the checkbox that unlocks the most tests per effort.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/ruby/**` and `plans/ruby-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Ruby primitives go in `lib/ruby/runtime.sx`.
|
||||
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
|
||||
- **Shared-file issues** → plan's Blockers with minimal repro.
|
||||
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
|
||||
## Ruby-specific gotchas
|
||||
|
||||
- **Block `return` vs lambda `return`.** Inside a block `{ ... return v }`, `return` invokes the *enclosing method's* escape continuation (non-local return). Inside a lambda `->(){ ... return v }`, `return` returns from the *lambda*. Don't conflate. Implement: blocks bind their `^method-k`; lambdas bind their own `^lambda-k`.
|
||||
- **`break` from inside a block** invokes a different escape — the *iteration loop's* escape — and the loop returns the break-value. `next` is escape from current iteration, returns iteration value. `redo` re-enters current iteration without advancing.
|
||||
- **Proc arity is lax.** `proc { |a, b, c| … }.call(1, 2)` ↦ `c = nil`. Lambda is strict — same call raises ArgumentError. Check arity at call site for lambdas only.
|
||||
- **Block argument unpacking.** `[[1,2],[3,4]].each { |a, b| … }` — single Array arg auto-unpacks for blocks (not lambdas). One arg, one Array → unpack. Frequent footgun.
|
||||
- **Method dispatch chain order:** prepended modules → class methods → included modules → superclass → BasicObject → method_missing. `super` walks from the *defining* class's position, not the receiver class's.
|
||||
- **Singleton classes** are lazily allocated. Looking up the chain for an object passes through its singleton class first, then its actual class. `class << obj; …; end` opens the singleton.
|
||||
- **`method_missing`** — fallback when ancestor walk misses. Receives `(name_symbol, *args, &blk)`. Pair with `respond_to_missing?` for `respond_to?` to also report true. Do **not** swallow NoMethodError silently.
|
||||
- **Ivars are per-object dicts.** Reading an unset ivar yields `nil` and a warning (`-W`). Don't error.
|
||||
- **Constant lookup** is first lexical (Module.nesting), then inheritance (Module.ancestors of the innermost class). Different from method lookup.
|
||||
- **`Object#send`** invokes private and public methods alike; `Object#public_send` skips privates.
|
||||
- **Class reopening.** `class Foo; def bar; …; end; end` plus a later `class Foo; def baz; …; end; end` adds methods to the same class. Class table lookups must be by-name, mutable; methods dict is mutable.
|
||||
- **Fiber semantics.** `Fiber.new { |arg| … }` creates a fiber suspended at entry. First `Fiber.resume(v)` enters with `arg = v`. Inside, `Fiber.yield(w)` returns `w` to the resumer; the next `Fiber.resume(v')` returns `v'` to the yield site. End of block returns final value to last resumer; subsequent `Fiber.resume` raises FiberError.
|
||||
- **`Fiber.transfer`** is symmetric — either side can transfer to the other; no resume/yield asymmetry. Implement on top of the same continuation pair, just don't enforce direction.
|
||||
- **Symbols are interned.** `:foo == :foo` is identity. Use SX symbols.
|
||||
- **Strings are mutable.** `s = "abc"; s << "d"; s == "abcd"`. Hash keys can be strings; hash dups string keys at insertion to be safe (or freeze them).
|
||||
- **Truthiness:** only `false` and `nil` are falsy. `0`, `""`, `[]` are truthy.
|
||||
- **Test corpus:** custom + curated RubySpec slice. Place programs in `lib/ruby/tests/programs/` with `.rb` extension.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr.
|
||||
- `type-of` on user fn returns `"lambda"`.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/ruby-on-sx.md` inline.
|
||||
- Short, factual commit messages (`ruby: Fiber.yield + Fiber.resume (+8)`).
|
||||
- One feature per iteration. Commit. Log. Next.
|
||||
|
||||
Go. Read the plan; find first `[ ]`; implement.
|
||||
77
plans/agent-briefings/smalltalk-loop.md
Normal file
77
plans/agent-briefings/smalltalk-loop.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# smalltalk-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/smalltalk-on-sx.md` forever. Message-passing OO + **blocks with non-local return** on delimited continuations. Non-local return is the headline showcase — every other Smalltalk reinvents it on the host stack; on SX it falls out of the captured method-return continuation.
|
||||
|
||||
```
|
||||
description: smalltalk-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/smalltalk-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/smalltalk-on-sx.md` — roadmap + Progress log.
|
||||
2. `ls lib/smalltalk/` — pick up from the most advanced file.
|
||||
3. If `lib/smalltalk/tests/*.sx` exist, run them. Green before new work.
|
||||
4. If `lib/smalltalk/scoreboard.md` exists, that's your baseline.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/smalltalk-on-sx.md`:
|
||||
|
||||
- **Phase 1** — tokenizer + parser (chunk format, identifiers, keywords `foo:`, binary selectors, `#sym`, `#(…)`, `$c`, blocks `[:a | …]`, cascades, message precedence)
|
||||
- **Phase 2** — object model + sequential eval (class table bootstrap, message dispatch, `super`, `doesNotUnderstand:`, instance variables)
|
||||
- **Phase 3** — **THE SHOWCASE**: blocks with non-local return via captured method-return continuation. `whileTrue:` / `ifTrue:ifFalse:` as block sends. 5 classic programs (eight-queens, quicksort, mandelbrot, life, fibonacci) green.
|
||||
- **Phase 4** — reflection + MOP: `perform:`, `respondsTo:`, runtime method addition, `becomeForward:`, `Exception` / `on:do:` / `ensure:` on top of `handler-bind`/`raise`
|
||||
- **Phase 5** — collections + numeric tower + streams
|
||||
- **Phase 6** — port SUnit, vendor Pharo Kernel-Tests slice, drive corpus to 200+
|
||||
- **Phase 7** — speed (optional): inline caching, block intrinsification
|
||||
|
||||
Within a phase, pick the checkbox that unlocks the most tests per effort.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/smalltalk/**` and `plans/smalltalk-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Smalltalk primitives go in `lib/smalltalk/runtime.sx`.
|
||||
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
|
||||
- **Shared-file issues** → plan's Blockers with minimal repro.
|
||||
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
|
||||
## Smalltalk-specific gotchas
|
||||
|
||||
- **Method invocation captures `^k`** — the return continuation. Bind it as the block's escape token. `^expr` from inside any nested block invokes that captured `^k`. Escape past method return raises `BlockContext>>cannotReturn:`.
|
||||
- **Blocks are lambdas + escape token**, not bare lambdas. `value`/`value:`/… invoke the lambda; `^` invokes the escape.
|
||||
- **`ifTrue:` / `ifFalse:` / `whileTrue:` are ordinary block sends** — no special form. The runtime intrinsifies them in the JIT path (Tier 1 of bytecode expansion already covers this pattern).
|
||||
- **Cascade** `r m1; m2; m3` desugars to `(let ((tmp r)) (st-send tmp 'm1 ()) (st-send tmp 'm2 ()) (st-send tmp 'm3 ()))`. Result is the cascade's last send (or first, depending on parser variant — pick one and document).
|
||||
- **`super` send** looks up starting from the *defining* class's superclass, not the receiver class. Stash the defining class on the method record.
|
||||
- **Selectors are interned symbols.** Use SX symbols.
|
||||
- **Receiver dispatch:** tagged ints / floats / strings / symbols / `nil` / `true` / `false` aren't boxed. Their classes (`SmallInteger`, `Float`, `String`, `Symbol`, `UndefinedObject`, `True`, `False`) are looked up by SX type-of, not by an `:class` field.
|
||||
- **Method precedence:** unary > binary > keyword. `3 + 4 factorial` is `3 + (4 factorial)`. `a foo: b bar` is `a foo: (b bar)` (keyword absorbs trailing unary).
|
||||
- **Image / fileIn / become: between sessions** = out of scope. One-way `becomeForward:` only.
|
||||
- **Test corpus:** ~200 hand-written + a slice of Pharo Kernel-Tests. Place programs in `lib/smalltalk/tests/programs/`.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr.
|
||||
- `type-of` on user fn returns `"lambda"`.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/smalltalk-on-sx.md` inline.
|
||||
- Short, factual commit messages (`smalltalk: tokenizer + 56 tests`).
|
||||
- One feature per iteration. Commit. Log. Next.
|
||||
|
||||
Go. Read the plan; find first `[ ]`; implement.
|
||||
83
plans/agent-briefings/tcl-loop.md
Normal file
83
plans/agent-briefings/tcl-loop.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# tcl-on-sx loop agent (single agent, queue-driven)
|
||||
|
||||
Role: iterates `plans/tcl-on-sx.md` forever. `uplevel`/`upvar` is the headline showcase — Tcl's superpower for defining your own control structures, requiring deep VM cooperation in any normal host but falling out of SX's first-class env-chain. Plus the Dodekalogue (12 rules), command-substitution everywhere, and "everything is a string" homoiconicity.
|
||||
|
||||
```
|
||||
description: tcl-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/tcl-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
1. Read `plans/tcl-on-sx.md` — roadmap + Progress log.
|
||||
2. `ls lib/tcl/` — pick up from the most advanced file.
|
||||
3. If `lib/tcl/tests/*.sx` exist, run them. Green before new work.
|
||||
4. If `lib/tcl/scoreboard.md` exists, that's your baseline.
|
||||
|
||||
## The queue
|
||||
|
||||
Phase order per `plans/tcl-on-sx.md`:
|
||||
|
||||
- **Phase 1** — tokenizer + parser. The Dodekalogue (12 rules): word-splitting, command sub `[…]`, var sub `$name`/`${name}`/`$arr(idx)`, double-quote vs brace word, backslash, `;`, `#` comments only at command start, single-pass left-to-right substitution
|
||||
- **Phase 2** — sequential eval + core commands. `set`/`unset`/`incr`/`append`/`lappend`, `puts`/`gets`, `expr` (own mini-language), `if`/`while`/`for`/`foreach`/`switch`, string commands, list commands, dict commands
|
||||
- **Phase 3** — **THE SHOWCASE**: `proc` + `uplevel` + `upvar`. Frame stack with proc-call push/pop; `uplevel #N script` evaluates in caller's frame; `upvar` aliases names across frames. Classic programs (for-each-line, assert macro, with-temp-var) green
|
||||
- **Phase 4** — `return -code N`, `catch`, `try`/`trap`/`finally`, `throw`. Control flow as integer codes
|
||||
- **Phase 5** — namespaces + ensembles. `namespace eval`, qualified names `::ns::cmd`, ensembles, `namespace path`
|
||||
- **Phase 6** — coroutines (built on fibers, same delcc as Ruby fibers) + system commands + drive corpus to 150+
|
||||
|
||||
Within a phase, pick the checkbox that unlocks the most tests per effort.
|
||||
|
||||
Every iteration: implement → test → commit → tick `[ ]` → Progress log → next.
|
||||
|
||||
## Ground rules (hard)
|
||||
|
||||
- **Scope:** only `lib/tcl/**` and `plans/tcl-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, other `lib/<lang>/` dirs, `lib/stdlib.sx`, or `lib/` root. Tcl primitives go in `lib/tcl/runtime.sx`.
|
||||
- **NEVER call `sx_build`.** 600s watchdog. If sx_server binary broken → Blockers entry, stop.
|
||||
- **Shared-file issues** → plan's Blockers with minimal repro.
|
||||
- **Delimited continuations** are in `lib/callcc.sx` + `spec/evaluator.sx` Step 5. `sx_summarise` spec/evaluator.sx first — 2300+ lines.
|
||||
- **SX files:** `sx-tree` MCP tools ONLY. `sx_validate` after edits.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
|
||||
## Tcl-specific gotchas
|
||||
|
||||
- **Everything is a string.** Internally cache shimmer reps (list, dict, int, double) for performance, but every value must be re-stringifiable. Mutating one rep dirties the cached string and vice versa.
|
||||
- **The Dodekalogue is strict.** Substitution is **one-pass**, **left-to-right**. The result of a substitution is a value, not a script — it does NOT get re-parsed for further substitutions. This is what makes Tcl safe-by-default. Don't accidentally re-parse.
|
||||
- **Brace word `{…}`** is the only way to defer evaluation. No substitution inside, just balanced braces. Used for `if {expr}` body, `proc body`, `expr` arguments.
|
||||
- **Double-quote word `"…"`** is identical to a bare word for substitution purposes — it just allows whitespace in a single word. `\` escapes still apply.
|
||||
- **Comments are only at command position.** `# this is a comment` after a `;` or newline; *not* inside a command. `set x 1 # not a comment` is a 4-arg `set`.
|
||||
- **`expr` has its own grammar** — operator precedence, function calls — and does its own substitution. Brace `expr {$x + 1}` to avoid double-substitution and to enable bytecode caching.
|
||||
- **`if` and `while` re-parse** the condition only if not braced. Always use `if {…}`/`while {…}` form. The unbraced form re-substitutes per iteration.
|
||||
- **`return` from a `proc`** uses control code 2. `break` is 3, `continue` is 4. `error` is 1. `catch` traps any non-zero code; user can return non-zero with `return -code error -errorcode FOO message`.
|
||||
- **`uplevel #0 script`** is global frame. `uplevel 1 script` (or just `uplevel script`) is caller's frame. `uplevel #N` is absolute level N (0=global, 1=top-level proc, 2=proc-called-from-top, …). Negative levels are errors.
|
||||
- **`upvar #N otherVar localVar`** binds `localVar` in the current frame as an *alias* — both names refer to the same storage. Reads and writes go through the alias.
|
||||
- **`info level`** with no arg returns current level number. `info level N` (positive) returns the command list that invoked level N. `info level -N` returns the command list of the level N relative-up.
|
||||
- **Variable names with `(…)`** are array elements: `set arr(foo) 1`. Arrays are not first-class values — you can't `set x $arr`. `array get arr` gives a flat list `{key1 val1 key2 val2 …}`.
|
||||
- **List vs string.** `set l "a b c"` and `set l [list a b c]` look the same when printed but the second has a cached list rep. `lindex` works on both via shimmering. Most user code can't tell the difference.
|
||||
- **`incr x`** errors if x doesn't exist; pre-set with `set x 0` or use `incr x 0` first if you mean "create-or-increment". Or use `dict incr` for dicts.
|
||||
- **Coroutines are fibers.** `coroutine name body` starts a coroutine; calling `name` resumes it; `yield value` from inside suspends and returns `value` to the resumer. Same primitive as Ruby fibers — share the implementation under the hood.
|
||||
- **`switch`** matches first clause whose pattern matches. Default is `default`. Variant matches: glob (default), `-exact`, `-glob`, `-regexp`. Body `-` means "fall through to next clause's body".
|
||||
- **Test corpus:** custom + slice of Tcl's own tests. Place programs in `lib/tcl/tests/programs/` with `.tcl` extension.
|
||||
|
||||
## General gotchas (all loops)
|
||||
|
||||
- SX `do` = R7RS iteration. Use `begin` for multi-expr sequences.
|
||||
- `cond`/`when`/`let` clauses evaluate only the last expr.
|
||||
- `type-of` on user fn returns `"lambda"`.
|
||||
- Shell heredoc `||` gets eaten — escape or use `case`.
|
||||
|
||||
## Style
|
||||
|
||||
- No comments in `.sx` unless non-obvious.
|
||||
- No new planning docs — update `plans/tcl-on-sx.md` inline.
|
||||
- Short, factual commit messages (`tcl: uplevel + upvar (+11)`).
|
||||
- One feature per iteration. Commit. Log. Next.
|
||||
|
||||
Go. Read the plan; find first `[ ]`; implement.
|
||||
125
plans/apl-on-sx.md
Normal file
125
plans/apl-on-sx.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# APL-on-SX: rank-polymorphic primitives + glyph parser
|
||||
|
||||
The headline showcase is **rank polymorphism** — a single primitive (`+`, `⌈`, `⊂`, `⍳`) works uniformly on scalars, vectors, matrices, and higher-rank arrays. ~80 glyph primitives + 6 operators bind together with right-to-left evaluation; the entire language is a high-density combinator algebra. The JIT compiler + primitive table pay off massively here because almost every program is `array → array` pure pipelines.
|
||||
|
||||
End-state goal: Dyalog-flavoured APL subset, dfns + tradfns, classic programs (game-of-life, mandelbrot, prime-sieve, n-queens, conway), 100+ green tests.
|
||||
|
||||
## Scope decisions (defaults — override by editing before we spawn)
|
||||
|
||||
- **Syntax:** Dyalog APL surface, Unicode glyphs. `⎕`-quad system functions for I/O. `∇` tradfn header.
|
||||
- **Conformance:** "Reads like APL, runs like APL." Not byte-compat with Dyalog; we care about right-to-left semantics and rank polymorphism.
|
||||
- **Test corpus:** custom — APL idioms (Roger Hui style), classic programs, plus ~50 pattern tests for primitives.
|
||||
- **Out of scope:** ⎕-namespaces beyond a handful, complex numbers, full TAO ordering, `⎕FX` runtime function definition (use static `∇` only), nested-array-of-functions higher orders, the editor.
|
||||
- **Glyphs:** input via plain Unicode in `.apl` source files. Backtick-prefix shortcuts handled by the user's editor — we don't ship one.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/apl/**` and `plans/apl-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. APL primitives go in `lib/apl/runtime.sx`.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
APL source (Unicode glyphs)
|
||||
│
|
||||
▼
|
||||
lib/apl/tokenizer.sx — glyphs, identifiers, numbers (¯ for negative), strings, strands
|
||||
│
|
||||
▼
|
||||
lib/apl/parser.sx — right-to-left with valence resolution (mon vs dyadic by position)
|
||||
│
|
||||
▼
|
||||
lib/apl/transpile.sx — AST → SX AST (entry: apl-eval-ast)
|
||||
│
|
||||
▼
|
||||
lib/apl/runtime.sx — array model, ~80 primitives, 6 operators, dfns/tradfns
|
||||
```
|
||||
|
||||
Core mapping:
|
||||
- **Array** = SX dict `{:shape (d1 d2 …) :ravel #(v1 v2 …)}`. Scalar is rank-0 (empty shape), vector is rank-1, matrix rank-2, etc. Type uniformity not required (heterogeneous nested arrays via "boxed" elements `⊂x`).
|
||||
- **Rank polymorphism** — every scalar primitive is broadcast: `1 2 3 + 4 5 6` ↦ `5 7 9`; `(2 3⍴⍳6) + 1` ↦ broadcast scalar to matrix.
|
||||
- **Conformability** = matching shapes, or one-side scalar, or rank-1 cycling (deferred — keep strict in v1).
|
||||
- **Valence** = each glyph has a monadic and a dyadic meaning; resolution is purely positional (left-arg present → dyadic).
|
||||
- **Operator** = takes one or two function operands, returns a derived function (`f¨` = `each f`, `f/` = `reduce f`, `f∘g` = `compose`, `f⍨` = `commute`).
|
||||
- **Tradfn** `∇R←L F R; locals` = named function with explicit header.
|
||||
- **Dfn** `{⍺+⍵}` = anonymous, `⍺` = left arg, `⍵` = right arg, `∇` = recurse.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: Unicode glyphs (the full APL set: `+ - × ÷ * ⍟ ⌈ ⌊ | ! ? ○ ~ < ≤ = ≥ > ≠ ∊ ∧ ∨ ⍱ ⍲ , ⍪ ⍴ ⌽ ⊖ ⍉ ↑ ↓ ⊂ ⊃ ⊆ ∪ ∩ ⍳ ⍸ ⌷ ⍋ ⍒ ⊥ ⊤ ⊣ ⊢ ⍎ ⍕ ⍝`), operators (`/ \ ¨ ⍨ ∘ . ⍣ ⍤ ⍥ @`), numbers (`¯` for negative, `1E2`, `1J2` complex deferred), characters (`'a'`, `''` escape), strands (juxtaposition of literals: `1 2 3`), names, comments `⍝ …`
|
||||
- [ ] Parser: right-to-left; classify each token as function, operator, value, or name; resolve valence positionally; dfn `{…}` body, tradfn `∇` header, guards `:`, control words `:If :While :For …` (Dyalog-style)
|
||||
- [ ] Unit tests in `lib/apl/tests/parse.sx`
|
||||
|
||||
### Phase 2 — array model + scalar primitives
|
||||
- [ ] Array constructor: `make-array shape ravel`, `scalar v`, `vector v…`, `enclose`/`disclose`
|
||||
- [ ] Shape arithmetic: `⍴` (shape), `,` (ravel), `≢` (tally / first-axis-length), `≡` (depth)
|
||||
- [ ] Scalar arithmetic primitives broadcast: `+ - × ÷ ⌈ ⌊ * ⍟ | ! ○`
|
||||
- [ ] Scalar comparison primitives: `< ≤ = ≥ > ≠`
|
||||
- [ ] Scalar logical: `~ ∧ ∨ ⍱ ⍲`
|
||||
- [ ] Index generator: `⍳n` (vector 1..n or 0..n-1 depending on `⎕IO`)
|
||||
- [ ] `⎕IO` = 1 default (Dyalog convention)
|
||||
- [ ] 40+ tests in `lib/apl/tests/scalar.sx`
|
||||
|
||||
### Phase 3 — structural primitives + indexing
|
||||
- [ ] Reshape `⍴`, ravel `,`, transpose `⍉` (full + dyadic axis spec)
|
||||
- [ ] Take `↑`, drop `↓`, rotate `⌽` (last axis), `⊖` (first axis)
|
||||
- [ ] Catenate `,` (last axis) and `⍪` (first axis)
|
||||
- [ ] Index `⌷` (squad), bracket-indexing `A[I]` (sugar for `⌷`)
|
||||
- [ ] Grade-up `⍋`, grade-down `⍒`
|
||||
- [ ] Enclose `⊂`, disclose `⊃`, partition (subset deferred)
|
||||
- [ ] Membership `∊`, find `⍳` (dyadic), without `~` (dyadic), unique `∪` (deferred to phase 6)
|
||||
- [ ] 40+ tests in `lib/apl/tests/structural.sx`
|
||||
|
||||
### Phase 4 — operators (THE SHOWCASE)
|
||||
- [ ] Reduce `f/` (last axis), `f⌿` (first axis) — including `∧/`, `∨/`, `+/`, `×/`, `⌈/`, `⌊/`
|
||||
- [ ] Scan `f\`, `f⍀`
|
||||
- [ ] Each `f¨` — applies `f` to each scalar/element
|
||||
- [ ] Outer product `∘.f` — `1 2 3 ∘.× 1 2 3` ↦ multiplication table
|
||||
- [ ] Inner product `f.g` — `+.×` is matrix multiply
|
||||
- [ ] Commute `f⍨` — `f⍨ x` ↔ `x f x`, `x f⍨ y` ↔ `y f x`
|
||||
- [ ] Compose `f∘g` — applies `g` first then `f`
|
||||
- [ ] Power `f⍣n` — apply f n times; `f⍣≡` until fixed point
|
||||
- [ ] Rank `f⍤k` — apply f at sub-rank k
|
||||
- [ ] At `@` — selective replace
|
||||
- [ ] 40+ tests in `lib/apl/tests/operators.sx`
|
||||
|
||||
### Phase 5 — dfns + tradfns + control flow
|
||||
- [ ] Dfn `{…}` with `⍺` (left arg, may be absent → niladic/monadic), `⍵` (right arg), `∇` (recurse), guards `cond:expr`, default left arg `⍺←default`
|
||||
- [ ] Local assignment via `←` (lexical inside dfn)
|
||||
- [ ] Tradfn `∇` header: `R←L F R;l1;l2`, statement-by-statement, branch via `→linenum`
|
||||
- [ ] Dyalog control words: `:If/:Else/:EndIf`, `:While/:EndWhile`, `:For X :In V :EndFor`, `:Select/:Case/:EndSelect`, `:Trap`/`:EndTrap`
|
||||
- [ ] Niladic / monadic / dyadic dispatch (function valence at definition time)
|
||||
- [ ] `lib/apl/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
|
||||
|
||||
### Phase 6 — classic programs + drive corpus
|
||||
- [ ] Classic programs in `lib/apl/tests/programs/`:
|
||||
- [ ] `life.apl` — Conway's Game of Life as a one-liner using `⊂` `⊖` `⌽` `+/`
|
||||
- [ ] `mandelbrot.apl` — complex iteration with rank-polymorphic `+ × ⌊` (or real-axis subset)
|
||||
- [ ] `primes.apl` — `(2=+⌿0=A∘.|A)/A←⍳N` sieve
|
||||
- [ ] `n-queens.apl` — backtracking via reduce
|
||||
- [ ] `quicksort.apl` — the classic Roger Hui one-liner
|
||||
- [ ] System functions: `⎕FMT`, `⎕FR` (float repr), `⎕TS` (timestamp), `⎕IO`, `⎕ML` (migration level — fixed at 1), `⎕←` (print)
|
||||
- [ ] Drive corpus to 100+ green
|
||||
- [ ] Idiom corpus — `lib/apl/tests/idioms.sx` covering classic Roger Hui / Phil Last idioms
|
||||
|
||||
## SX primitive baseline
|
||||
|
||||
Use vectors for arrays; numeric tower + rationals for numbers; ADTs for tagged data;
|
||||
coroutines for fibers; string-buffer for mutable string building; bitwise ops for bit
|
||||
manipulation; multiple values for multi-return; promises for lazy evaluation; hash tables
|
||||
for mutable associative storage; sets for O(1) membership; sequence protocol for
|
||||
polymorphic iteration; gensym for unique symbols; char type for characters; string ports
|
||||
+ read/write for reader protocols; regexp for pattern matching; bytevectors for binary
|
||||
data; format for string templating.
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
- _(none yet)_
|
||||
|
||||
## Blockers
|
||||
|
||||
- _(none yet)_
|
||||
131
plans/common-lisp-on-sx.md
Normal file
131
plans/common-lisp-on-sx.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Common-Lisp-on-SX: conditions + restarts on delimited continuations
|
||||
|
||||
The headline showcase is the **condition system**. Restarts are *resumable* exceptions — every other Lisp implementation reinvents this on host-stack unwind tricks. On SX restarts are textbook delimited continuations: `signal` walks the handler chain; `invoke-restart` resumes the captured continuation at the restart point. Same delcc primitive that powers Erlang actors, expressed as a different surface.
|
||||
|
||||
End-state goal: ANSI Common Lisp subset with a working condition/restart system, CLOS multimethods (with `:before`/`:after`/`:around`), the LOOP macro, packages, and ~150 hand-written + classic programs.
|
||||
|
||||
## Scope decisions (defaults — override by editing before we spawn)
|
||||
|
||||
- **Syntax:** ANSI Common Lisp surface. Read tables, dispatch macros (`#'`, `#(`, `#\`, `#:`, `#x`, `#b`, `#o`, ratios `1/3`).
|
||||
- **Conformance:** ANSI X3.226 *as a target*, not bug-for-bug SBCL/CCL. "Reads like CL, runs like CL."
|
||||
- **Test corpus:** custom + a curated slice of `ansi-test`. Plus classic programs: condition-system demo, restart-driven debugger, multiple-dispatch geometry, LOOP corpus.
|
||||
- **Out of scope:** compilation to native, FFI, sockets, threads, MOP class redefinition, full pathname/logical-pathname machinery, structures with `:include` deep customization.
|
||||
- **Packages:** simple — `defpackage`/`in-package`/`export`/`use-package`/`:cl`/`:cl-user`. No nicknames, no shadowing-import edge cases.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/common-lisp/**` and `plans/common-lisp-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. CL primitives go in `lib/common-lisp/runtime.sx`.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
Common Lisp source
|
||||
│
|
||||
▼
|
||||
lib/common-lisp/reader.sx — tokenizer + reader (read macros, dispatch chars)
|
||||
│
|
||||
▼
|
||||
lib/common-lisp/parser.sx — AST: forms, declarations, lambda lists
|
||||
│
|
||||
▼
|
||||
lib/common-lisp/transpile.sx — AST → SX AST (entry: cl-eval-ast)
|
||||
│
|
||||
▼
|
||||
lib/common-lisp/runtime.sx — special forms, condition system, CLOS, packages, BIFs
|
||||
```
|
||||
|
||||
Core mapping:
|
||||
- **Symbol** = SX symbol with package prefix; package table is a flat dict.
|
||||
- **Cons cell** = SX pair via `cons`/`car`/`cdr`; lists native.
|
||||
- **Multiple values** = thread through `values`/`multiple-value-bind`; primary-value default for one-context callers.
|
||||
- **Block / return-from** = captured continuation; `return-from name v` invokes the block-named `^k`.
|
||||
- **Tagbody / go** = each tag is a continuation; `go tag` invokes it.
|
||||
- **Unwind-protect** = scope frame with a cleanup thunk fired on any non-local exit.
|
||||
- **Conditions / restarts** = layered handler chain on top of `handler-bind` + delcc. `signal` walks handlers; `invoke-restart` resumes a captured continuation.
|
||||
- **CLOS** = generic functions are dispatch tables on argument-class lists; method combination computed lazily; `call-next-method` is a continuation.
|
||||
- **Macros** = SX macros (sentinel-body) — defmacro lowers directly.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — reader + parser
|
||||
- [ ] Tokenizer: symbols (with package qualification `pkg:sym` / `pkg::sym`), numbers (int, float, ratio `1/3`, `#xFF`, `#b1010`, `#o17`), strings `"…"` with `\` escapes, characters `#\Space` `#\Newline` `#\a`, comments `;`, block comments `#| … |#`
|
||||
- [ ] Reader: list, dotted pair, quote `'`, function `#'`, quasiquote `` ` ``, unquote `,`, splice `,@`, vector `#(…)`, uninterned `#:foo`, nil/t literals
|
||||
- [ ] Parser: lambda lists with `&optional` `&rest` `&key` `&aux` `&allow-other-keys`, defaults, supplied-p variables
|
||||
- [ ] Unit tests in `lib/common-lisp/tests/read.sx`
|
||||
|
||||
### Phase 2 — sequential eval + special forms
|
||||
- [ ] `cl-eval-ast`: `quote`, `if`, `progn`, `let`, `let*`, `flet`, `labels`, `setq`, `setf` (subset), `function`, `lambda`, `the`, `locally`, `eval-when`
|
||||
- [ ] `block` + `return-from` via captured continuation
|
||||
- [ ] `tagbody` + `go` via per-tag continuations
|
||||
- [ ] `unwind-protect` cleanup frame
|
||||
- [ ] `multiple-value-bind`, `multiple-value-call`, `multiple-value-prog1`, `values`, `nth-value`
|
||||
- [ ] `defun`, `defparameter`, `defvar`, `defconstant`, `declaim`, `proclaim` (no-op)
|
||||
- [ ] Dynamic variables — `defvar`/`defparameter` produce specials; `let` rebinds via parameterize-style scope
|
||||
- [ ] 60+ tests in `lib/common-lisp/tests/eval.sx`
|
||||
|
||||
### Phase 3 — conditions + restarts (THE SHOWCASE)
|
||||
- [ ] `define-condition` — class hierarchy rooted at `condition`/`error`/`warning`/`simple-error`/`simple-warning`/`type-error`/`arithmetic-error`/`division-by-zero`
|
||||
- [ ] `signal`, `error`, `cerror`, `warn` — all walk the handler chain
|
||||
- [ ] `handler-bind` — non-unwinding handlers, may decline by returning normally
|
||||
- [ ] `handler-case` — unwinding handlers (delcc abort)
|
||||
- [ ] `restart-case`, `with-simple-restart`, `restart-bind`
|
||||
- [ ] `find-restart`, `invoke-restart`, `invoke-restart-interactively`, `compute-restarts`
|
||||
- [ ] `with-condition-restarts` — associate restarts with a specific condition
|
||||
- [ ] `*break-on-signals*`, `*debugger-hook*` (basic)
|
||||
- [ ] Classic programs in `lib/common-lisp/tests/programs/`:
|
||||
- [ ] `restart-demo.lisp` — division with `:use-zero` and `:retry` restarts
|
||||
- [ ] `parse-recover.lisp` — parser with skipped-token restart
|
||||
- [ ] `interactive-debugger.lisp` — ASCII REPL using `:debugger-hook`
|
||||
- [ ] `lib/common-lisp/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
|
||||
|
||||
### Phase 4 — CLOS
|
||||
- [ ] `defclass` with `:initarg`/`:initform`/`:accessor`/`:reader`/`:writer`/`:allocation`
|
||||
- [ ] `make-instance`, `slot-value`, `(setf slot-value)`, `with-slots`, `with-accessors`
|
||||
- [ ] `defgeneric` with `:method-combination` (standard, plus `+`, `and`, `or`)
|
||||
- [ ] `defmethod` with `:before` / `:after` / `:around` qualifiers
|
||||
- [ ] `call-next-method` (continuation), `next-method-p`
|
||||
- [ ] `class-of`, `find-class`, `slot-boundp`, `change-class` (basic)
|
||||
- [ ] Multiple dispatch — method specificity by argument-class precedence list
|
||||
- [ ] Built-in classes registered for tagged values (`integer`, `float`, `string`, `symbol`, `cons`, `null`, `t`)
|
||||
- [ ] Classic programs:
|
||||
- [ ] `geometry.lisp` — `intersect` generic dispatching on (point line), (line line), (line plane)…
|
||||
- [ ] `mop-trace.lisp` — `:before` + `:after` printing call trace
|
||||
|
||||
### Phase 5 — macros + LOOP + reader macros
|
||||
- [ ] `defmacro`, `macrolet`, `symbol-macrolet`, `macroexpand-1`, `macroexpand`
|
||||
- [ ] `gensym`, `gentemp`
|
||||
- [ ] `set-macro-character`, `set-dispatch-macro-character`, `get-macro-character`
|
||||
- [ ] **The LOOP macro** — iteration drivers (`for … in/across/from/upto/downto/by`, `while`, `until`, `repeat`), accumulators (`collect`, `append`, `nconc`, `count`, `sum`, `maximize`, `minimize`), conditional clauses (`if`/`when`/`unless`/`else`), termination (`finally`/`thereis`/`always`/`never`), `named` blocks
|
||||
- [ ] LOOP test corpus: 30+ tests covering all clause types
|
||||
|
||||
### Phase 6 — packages + stdlib drive
|
||||
- [ ] `defpackage`, `in-package`, `export`, `use-package`, `import`, `find-package`
|
||||
- [ ] Package qualification at the reader level — `cl:car`, `mypkg::internal`
|
||||
- [ ] `:common-lisp` (`:cl`) and `:common-lisp-user` (`:cl-user`) packages
|
||||
- [ ] Sequence functions — `mapcar`, `mapc`, `mapcan`, `reduce`, `find`, `find-if`, `position`, `count`, `every`, `some`, `notany`, `notevery`, `remove`, `remove-if`, `subst`
|
||||
- [ ] List ops — `assoc`, `getf`, `nth`, `last`, `butlast`, `nthcdr`, `tailp`, `ldiff`
|
||||
- [ ] String ops — `string=`, `string-upcase`, `string-downcase`, `subseq`, `concatenate`
|
||||
- [ ] FORMAT — basic directives `~A`, `~S`, `~D`, `~F`, `~%`, `~&`, `~T`, `~{...~}` (iteration), `~[...~]` (conditional), `~^` (escape), `~P` (plural)
|
||||
- [ ] Drive corpus to 200+ green
|
||||
|
||||
## SX primitive baseline
|
||||
|
||||
Use vectors for arrays; numeric tower + rationals for numbers; ADTs for tagged data;
|
||||
coroutines for fibers; string-buffer for mutable string building; bitwise ops for bit
|
||||
manipulation; multiple values for multi-return; promises for lazy evaluation; hash tables
|
||||
for mutable associative storage; sets for O(1) membership; sequence protocol for
|
||||
polymorphic iteration; gensym for unique symbols; char type for characters; string ports
|
||||
+ read/write for reader protocols; regexp for pattern matching; bytevectors for binary
|
||||
data; format for string templating.
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
- _(none yet)_
|
||||
|
||||
## Blockers
|
||||
|
||||
- _(none yet)_
|
||||
257
plans/designs/sx-adt.md
Normal file
257
plans/designs/sx-adt.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# SX Algebraic Data Types — Design
|
||||
|
||||
## Motivation
|
||||
|
||||
Every language implementation currently uses `{:tag "..." :field ...}` tagged dicts to
|
||||
simulate sum types. This is verbose, error-prone (typos in tag strings go undetected), and
|
||||
produces no exhaustiveness warnings. Native ADTs eliminate the pattern everywhere.
|
||||
|
||||
Examples of current workarounds:
|
||||
- Haskell `Maybe a` → `{:tag "Just" :value x}` / `{:tag "Nothing"}`
|
||||
- Prolog terms → `{:tag "functor" :name "foo" :args (list x y)}`
|
||||
- Lua result type → `{:tag "ok" :value v}` / `{:tag "err" :msg s}`
|
||||
- Common Lisp `cons` pairs → `{:tag "cons" :car a :cdr b}`
|
||||
|
||||
---
|
||||
|
||||
## Syntax
|
||||
|
||||
### `define-type`
|
||||
|
||||
```lisp
|
||||
(define-type Name
|
||||
(Ctor1 field1 field2 ...)
|
||||
(Ctor2 field1 ...)
|
||||
...)
|
||||
```
|
||||
|
||||
Creates:
|
||||
- Constructor functions: `Ctor1`, `Ctor2`, … (callable like normal functions)
|
||||
- Type predicate: `Name?` — returns true for any value of type `Name`
|
||||
- Constructor predicates: `Ctor1?`, `Ctor2?`, … (optional, auto-generated)
|
||||
- Field accessors: `Ctor1-field1`, `Ctor1-field2`, … (optional, auto-generated)
|
||||
|
||||
Examples:
|
||||
|
||||
```lisp
|
||||
(define-type Maybe
|
||||
(Just value)
|
||||
(Nothing))
|
||||
|
||||
(define-type Result
|
||||
(Ok value)
|
||||
(Err message))
|
||||
|
||||
(define-type Tree
|
||||
(Leaf)
|
||||
(Node left value right))
|
||||
|
||||
(define-type List-of
|
||||
(Nil-of)
|
||||
(Cons-of head tail))
|
||||
```
|
||||
|
||||
Constructors with no fields are zero-argument constructors (singletons by value):
|
||||
|
||||
```lisp
|
||||
(Nothing) ; => #<Nothing>
|
||||
(Leaf) ; => #<Leaf>
|
||||
```
|
||||
|
||||
### `match`
|
||||
|
||||
```lisp
|
||||
(match expr
|
||||
((Ctor1 a b) body)
|
||||
((Ctor2 x) body)
|
||||
((Ctor3) body)
|
||||
(else body))
|
||||
```
|
||||
|
||||
- Clauses are tried in order; first match wins.
|
||||
- `else` clause is optional but suppresses exhaustiveness warnings.
|
||||
- Pattern variables (`a`, `b`, `x`) are bound in the body scope.
|
||||
- Wildcard `_` discards the matched value.
|
||||
- Literal patterns: `42`, `"str"`, `true`, `nil` — match by value equality.
|
||||
- Nested patterns: `((Node left (Leaf) right) body)` — nested constructor patterns.
|
||||
|
||||
Examples:
|
||||
|
||||
```lisp
|
||||
(match result
|
||||
((Ok v) (str "got: " v))
|
||||
((Err m) (str "error: " m)))
|
||||
|
||||
(match tree
|
||||
((Leaf) 0)
|
||||
((Node l v r) (+ 1 (tree-depth l) (tree-depth r))))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CEK Dispatch
|
||||
|
||||
### Runtime representation
|
||||
|
||||
ADT values are OCaml records (not dicts) — opaque, non-inspectable via `get`:
|
||||
|
||||
```ocaml
|
||||
type adt_value = {
|
||||
av_type : string; (* type name, e.g. "Maybe" *)
|
||||
av_ctor : string; (* constructor name, e.g. "Just" *)
|
||||
av_fields: value array; (* positional fields *)
|
||||
}
|
||||
```
|
||||
|
||||
In JS: `{ _adt: true, _type: "Maybe", _ctor: "Just", _fields: [v] }`.
|
||||
|
||||
`typeOf` returns the ADT type name (e.g. `"Maybe"`).
|
||||
|
||||
### `define-type` — special form
|
||||
|
||||
`stepSfDefineType(args, env, kont)`:
|
||||
|
||||
1. Parse `Name` and list of `(CtorN field...)` clauses.
|
||||
2. For each constructor `CtorK` with fields `[f1, f2, …]`:
|
||||
- Register `CtorK` as a `NativeFn` that takes `|fields|` args and returns an `AdtValue`.
|
||||
- Register `CtorK?` as a predicate (`AdtValue` with matching ctor name → `true`).
|
||||
- Register `CtorK-fN` as field accessor (returns `av_fields[N]`).
|
||||
3. Register `Name?` as a predicate (`AdtValue` with matching type name → `true`).
|
||||
4. All bindings go into the current environment via `env-bind!`.
|
||||
5. Returns `Nil`.
|
||||
|
||||
This is an environment mutation — no new frame needed. Evaluates in one step.
|
||||
|
||||
### `match` — special form
|
||||
|
||||
`stepSfMatch(args, env, kont)`:
|
||||
|
||||
1. Push `MatchFrame` with `clauses` and `env` onto kont.
|
||||
2. Return state evaluating the scrutinee `expr`.
|
||||
3. `MatchFrame` continue: receive scrutinee value, walk clauses:
|
||||
- For each `((CtorN vars...) body)`:
|
||||
- If scrutinee is an `AdtValue` with `av_ctor = "CtorN"` and `av_fields.length = |vars|`:
|
||||
- Bind `vars[i]` → `av_fields[i]` in fresh child env.
|
||||
- Return state evaluating `body` in that env.
|
||||
- `(else body)` — always matches, body evaluated in current env.
|
||||
- Literal `42`/`"str"` patterns: match by value equality.
|
||||
- Wildcard `_`: always matches, binds nothing.
|
||||
4. If no clause matched and no `else`: raise `"match: no clause matched <value>"`.
|
||||
|
||||
Frame type: `"match"` — stores `cf_remaining` (clauses), `cf_env` (enclosing env).
|
||||
|
||||
---
|
||||
|
||||
## Interaction with `cond` / `case`
|
||||
|
||||
`match` is the primary dispatch form for ADTs. `cond` / `case` remain unchanged:
|
||||
|
||||
- `cond` tests arbitrary boolean expressions — still useful for non-ADT dispatch.
|
||||
- `case` matches on equality to literal values — unchanged.
|
||||
- `match` is the new form: structural pattern matching on ADT constructors.
|
||||
|
||||
They are orthogonal. A `match` clause can contain a `cond`; a `cond` clause can contain a `match`.
|
||||
|
||||
---
|
||||
|
||||
## Exhaustiveness checking
|
||||
|
||||
Emit a **warning** (not an error) when:
|
||||
- A `match` has no `else` clause, AND
|
||||
- Not all constructors of the scrutinee's type are covered.
|
||||
|
||||
Detection: when `define-type` runs, it registers the constructor set in a global table
|
||||
`_adt_registry: type_name → [ctor_names]`. At `match` compile/evaluation time:
|
||||
- If the scrutinee's type is in `_adt_registry` and not all ctors appear as patterns:
|
||||
- `console.warn("[sx] match: non-exhaustive — missing: Ctor3, Ctor4 for type Maybe")`
|
||||
- Execution continues (warning, not error).
|
||||
|
||||
This is best-effort: the scrutinee type is only known at runtime. The warning fires on
|
||||
first non-exhaustive match evaluation, not at definition time.
|
||||
|
||||
---
|
||||
|
||||
## Recursive types
|
||||
|
||||
Recursive types work because constructors are registered as functions, and function bodies
|
||||
are evaluated lazily:
|
||||
|
||||
```lisp
|
||||
(define-type Tree
|
||||
(Leaf)
|
||||
(Node left value right))
|
||||
|
||||
; Recursive function over a recursive type:
|
||||
(define (depth tree)
|
||||
(match tree
|
||||
((Leaf) 0)
|
||||
((Node l v r) (+ 1 (max (depth l) (depth r))))))
|
||||
```
|
||||
|
||||
No special treatment needed — the type definition doesn't need to know about recursion.
|
||||
The constructor `Node` accepts any values, including other `Node` or `Leaf` values.
|
||||
|
||||
---
|
||||
|
||||
## Pattern variables
|
||||
|
||||
In `match` clauses, identifiers in constructor position that are NOT constructor names are
|
||||
treated as pattern variables (bound to matched field values):
|
||||
|
||||
```lisp
|
||||
(match x
|
||||
((Just v) v) ; v bound to the wrapped value
|
||||
((Nothing) nil))
|
||||
|
||||
(match pair
|
||||
((Cons-of h t) (list h t))) ; h, t bound to head and tail
|
||||
```
|
||||
|
||||
**Wildcard**: `_` is always a wildcard — matches anything, binds nothing.
|
||||
|
||||
```lisp
|
||||
(match x
|
||||
((Just _) "has value")
|
||||
((Nothing) "empty"))
|
||||
```
|
||||
|
||||
**Nested patterns**:
|
||||
|
||||
```lisp
|
||||
(match tree
|
||||
((Node (Leaf) v (Leaf)) (str "leaf node: " v))
|
||||
((Node l v r) (str "inner node: " v)))
|
||||
```
|
||||
|
||||
Nested patterns are matched recursively: the inner `(Leaf)` pattern checks that the
|
||||
`left` field is itself a `Leaf` ADT value.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 6a — `define-type` + basic `match` (no nested patterns, no exhaustiveness)
|
||||
|
||||
1. OCaml: add `AdtValue of adt_value` to `sx_types.ml`.
|
||||
2. Evaluator: add `step-sf-define-type` — parse clauses, register ctor fns + predicates + accessors.
|
||||
3. Evaluator: add `step-sf-match` + `MatchFrame` — linear scan of clauses, flat patterns only.
|
||||
4. JS: same (AdtValue as plain object with `_adt`/`_type`/`_ctor`/`_fields` props).
|
||||
|
||||
### Phase 6b — nested patterns (separate fire)
|
||||
|
||||
Recursive `matchPattern(pattern, value, env)` helper that:
|
||||
- Returns `{matched: bool, bindings: map}`
|
||||
- Recursively matches sub-patterns against ADT fields.
|
||||
|
||||
### Phase 6c — exhaustiveness warnings (separate fire)
|
||||
|
||||
`_adt_registry` global + warning emission on first non-exhaustive match.
|
||||
|
||||
---
|
||||
|
||||
## Open questions (deferred to review)
|
||||
|
||||
1. **Accessor auto-generation**: should `Ctor-field` accessors be generated always, or only on demand? Risk: name collisions if two types have constructors with same field names.
|
||||
2. **Singleton constructors**: `(Nothing)` — zero-arg ctor — should these be interned (same object every call) or fresh each time? Interning enables `eq?` checks but requires a global table.
|
||||
3. **Printing/inspect**: `inspect` on an AdtValue should show `(Just 42)` not `#<adt:Just>`. Implement in `inspect` function or via `display`/`write` (Phase 17 ports).
|
||||
4. **Pattern-matching on non-ADT values**: should `match` handle list patterns `(a . b)` and literal patterns in clause heads? Deferred — add only if needed by a language implementation.
|
||||
96
plans/hs-blockers-drain.md
Normal file
96
plans/hs-blockers-drain.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# HS conformance — blockers drain
|
||||
|
||||
Goal: take hyperscript conformance from **1277/1496 (85.4%)** to **1496/1496 (100%)** by clearing the blocked clusters and the design-done Bucket E subsystems.
|
||||
|
||||
This plan exists because the per-iteration `loops/hs` agent can't fit these into its 30-min budget — they need dedicated multi-commit sit-downs. Track progress here; refer to `plans/hs-conformance-to-100.md` for the canonical cluster ledger.
|
||||
|
||||
## Current state (2026-04-25)
|
||||
|
||||
- Loop running in `/root/rose-ash-loops/hs` (branch `loops/hs`)
|
||||
- sx-tree MCP **fixed** (was a session-stale binary issue — restart of claude in the tmux window picked it up). Loop hinted to retry **#32**, **#29** first.
|
||||
- Recent loop progress: ~1 commit/6h — easy wins drained, what's left needs focused attention.
|
||||
|
||||
## Remaining work
|
||||
|
||||
### Bucket-A/B/C blockers (small, in-place fixes)
|
||||
|
||||
| # | Cluster | Tests | Effort | Blocker | Fix sketch |
|
||||
|---|---------|------:|--------|---------|------------|
|
||||
| **17** | `tell` semantics | +3 | ~1h | Implicit-default-target ambiguity. `bare add .bar` inside `tell X` should target `X` but explicit `to me` must reach the original element. | Add `beingTold` symbol distinct from `me`; bare commands compile to `beingTold-or-me`; explicit `me` always the original. |
|
||||
| **22** | window global fn fallback | +2-4 | ~1h | `foo()` where `foo` isn't SX-defined needs to fall back to `(host-global "foo")`. Three attempts failed: guard (host-level error not catchable), `env-has?` (not in HS kernel), `hs-win-call` (NativeFn not callable from CALL). | Add `symbol-bound?` predicate to HS kernel **OR** a host-call-fn primitive with arity-agnostic dispatch. |
|
||||
| **29** | `hyperscript:before:init` / `:after:init` / `:parse-error` events | +4-6 | ~30m (post sx-tree fix) | Was sx-tree MCP outage. Now unblocked — loop should retry. 4 of 6 tests need stricter parser error-rejection (out of scope; mark partial). | Edit `integration.sx` to fire DOM events at activation boundaries. |
|
||||
|
||||
### Bucket D — medium features
|
||||
|
||||
| # | Cluster | Tests | Effort | Status |
|
||||
|---|---------|------:|--------|--------|
|
||||
| **31** | runtime null-safety error reporting | **+15-18** | **2-4h** | **THIS SESSION'S TARGET.** Plan node fully spec'd: 5 pieces of work. |
|
||||
| **32** | MutationObserver mock + `on mutation` | +10-15 | ~2h | Was sx-tree-blocked. Now unblocked — loop hinted to retry. Multi-file: parser, compiler, runtime, runner mock, generator skip-list. |
|
||||
| **33** | cookie API | +2 (remaining) | ~30m | Partial done (+3). Remaining 2 need `hs-method-call` runtime fallback for unknown methods + `hs-for-each` recognising host-array/proxy collections. |
|
||||
| 34 | event modifier DSL | +6-8 | ~1-2h | `elsewhere`, `every`, count filters (`once`/`twice`/`3 times`/ranges), `from elsewhere`. Pending. |
|
||||
| 35 | namespaced `def` | +3 | ~30m | Pending. |
|
||||
|
||||
### Bucket E — subsystems (design docs landed, multi-commit each)
|
||||
|
||||
Each has a design doc with a step-by-step checklist. These are 1-2 days of focused work each, not loop-fits.
|
||||
|
||||
| # | Subsystem | Tests | Design doc | Branch |
|
||||
|---|-----------|------:|------------|--------|
|
||||
| 36 | WebSocket + `socket` + RPC Proxy | +12-16 | `plans/designs/e36-websocket.md` | `worktree-agent-a9daf73703f520257` |
|
||||
| 37 | Tokenizer-as-API | +16-17 | `plans/designs/e37-tokenizer-api.md` | `worktree-agent-a6bb61d59cc0be8b4` |
|
||||
| 38 | SourceInfo API | +4 | `plans/designs/e38-sourceinfo.md` | `agent-e38-sourceinfo` |
|
||||
| 39 | WebWorker plugin (parser-only stub) | +1 | `plans/designs/e39-webworker.md` | `hs-design-e39-webworker` |
|
||||
| 40 | Real Fetch / non-2xx / before-fetch | +7 | `plans/designs/e40-real-fetch.md` | `worktree-agent-a94612a4283eaa5e0` |
|
||||
|
||||
### Bucket F — generator translation gaps
|
||||
|
||||
~25 tests SKIP'd because `tests/playwright/generate-sx-tests.py` bails with `return None`. Single dedicated generator-repair sit-down once Bucket D is drained. ~half-day.
|
||||
|
||||
## Order of attack
|
||||
|
||||
In approximate cost-per-test order:
|
||||
|
||||
1. **Loop self-heal** (no human work) — wait for #29, #32 to land via the running loop ⏱️ ~next 1-2 hours
|
||||
2. **#31 null-safety** — biggest scoped single win, dedicated worktree agent (this session)
|
||||
3. **#33 cookie API remainder** — quick partial completion
|
||||
4. **#17 / #22 / #34 / #35** — small fiddly fixes, one sit-down each
|
||||
5. **Bucket E** — pick one subsystem at a time. **#39 (WebWorker stub) first** — single commit, smallest. Then **#38 (SourceInfo)** — 4 commits. Then the bigger three (#36, #37, #40).
|
||||
6. **Bucket F** — generator repair sweep at the end.
|
||||
|
||||
Estimated total to 100%: ~10-15 days of focused work, parallelisable across branches.
|
||||
|
||||
## Cluster #31 spec (full detail)
|
||||
|
||||
The plan note from `hs-conformance-to-100.md`:
|
||||
|
||||
> 18 tests in `runtimeErrors`. When accessing `.foo` on nil, emit a structured error with position info. One coordinated fix in the compiler emit paths for property access, function calls, set/put.
|
||||
|
||||
**Required pieces:**
|
||||
|
||||
1. **Generator-side `eval-hs-error` helper + recognizer** for `expect(await error("HS")).toBe("MSG")` blocks. In `tests/playwright/generate-sx-tests.py`.
|
||||
2. **Runtime helpers** in `lib/hyperscript/runtime.sx`:
|
||||
- `hs-null-error!` raising `'<sel>' is null`
|
||||
- `hs-named-target` — wraps a query result with the original selector source
|
||||
- `hs-named-target-list` — same for list results
|
||||
3. **Compiler patches at every target-position `(query SEL)` emit** — wrap in named-target carrying the original selector source. ~17 command emit paths in `lib/hyperscript/compiler.sx`:
|
||||
add, remove, hide, show, measure, settle, trigger, send, set, default, increment, decrement, put, toggle, transition, append, take.
|
||||
4. **Function-call null-check** at bare `(name)`, `hs-method-call`, and `host-get` chains, deriving the leftmost-uncalled-name (`'x'` / `'x.y'`) from the parse tree.
|
||||
5. **Possessive-base null-check** (`set x's y to true` → `'x' is null`).
|
||||
|
||||
**Files in scope:**
|
||||
- `lib/hyperscript/runtime.sx` (new helpers)
|
||||
- `lib/hyperscript/compiler.sx` (~17 emit-path edits)
|
||||
- `tests/playwright/generate-sx-tests.py` (test recognizer)
|
||||
- `tests/hs-run-filtered.js` (if mock helpers needed)
|
||||
- `shared/static/wasm/sx/hs-runtime.sx` + `hs-compiler.sx` (WASM staging copies)
|
||||
|
||||
**Approach:** target-named pieces incrementally — runtime helpers first (no compiler change), then compiler emit paths in batches (group similar commands), then function-call/possessive at the end. Each batch is one commit if it lands +N tests; mark partial if it only unlocks part.
|
||||
|
||||
**Watch for:** smoke-range regressions (tests flipping pass→fail). Each commit: rerun smoke 0-195 and the `runtimeErrors` suite.
|
||||
|
||||
## Notes for future sessions
|
||||
|
||||
- `plans/hs-conformance-to-100.md` is the canonical cluster ledger — update it on every commit.
|
||||
- `plans/hs-conformance-scoreboard.md` is the live tally — bump `Merged:` and the bucket roll-up.
|
||||
- Loop has scope rule "never edit `spec/evaluator.sx` or broader SX kernel" — most fixes here stay in `lib/hyperscript/**`, `tests/`, generator. If a fix needs kernel work, surface to the user; don't merge silently.
|
||||
- Cluster #22's `symbol-bound?` predicate would be a kernel addition — that's a real cross-boundary scope expansion.
|
||||
@@ -125,7 +125,7 @@ Each item: implement → tests → update progress. Mark `[x]` when tests green.
|
||||
- [x] Rest params (`...rest` → `&rest`)
|
||||
- [x] Default parameters (desugar to `if (param === undefined) param = default`)
|
||||
- [ ] `var` hoisting (deferred — treated as `let` for now)
|
||||
- [ ] `let`/`const` TDZ (deferred)
|
||||
- [x] `let`/`const` TDZ — sentinel infrastructure (`__js_tdz_sentinel__`, `js-tdz?`, `js-tdz-check` in runtime.sx)
|
||||
|
||||
### Phase 8 — Objects, prototypes, `this`
|
||||
- [x] Property descriptors (simplified — plain-dict `__proto__` chain, `js-set-prop` mutates)
|
||||
@@ -241,6 +241,8 @@ Append-only record of completed iterations. Loop writes one line per iteration:
|
||||
- 29× Timeout (slow string/regex loops)
|
||||
- 16× ReferenceError — still some missing globals
|
||||
|
||||
- 2026-04-25 — **Regex engine (lib/js/regex.sx) + let/const TDZ infrastructure.** New file `lib/js/regex.sx`: 39-form pure-SX recursive backtracking engine installed via `js-regex-platform-override!`. Covers literals, `.`, `\d\w\s` + negations, `[abc]/[^abc]/[a-z]` char classes, `^\$\b\B` anchors, greedy+lazy quantifiers (`* + ? {n,m} *? +? ??`), capturing groups, non-capturing `(?:...)`, alternation `a|b`, flags `i`/`g`/`m`. Groups: match inner first → set capture → match rest (correct boundary), avoids including rest-nodes content in capture. Greedy: expand-first then backtrack (correct longest-match semantics). `js-regex-match-all` for String.matchAll. Fixed `String.prototype.match` to use platform engine (was calling stub). TDZ infrastructure added to `runtime.sx`: `__js_tdz_sentinel__` (unique sentinel dict), `js-tdz?`, `js-tdz-check`. `transpile.sx` passes `kind` through `js-transpile-var → js-vardecl-forms` (no behavioral change yet — infrastructure ready). `test262-runner.py` and `conformance.sh` updated to load `regex.sx` as epoch 6/50. Unit: **559/560** (was 522/522 before regex tests added, now +38 new tests; 1 pre-existing backtick failure). Conformance: **148/148** (unchanged). Gotchas: (1) `sx_insert_near` on a pattern inside a top-level function body inserts there (not at top level) — need to use `sx_insert_near` on a top-level symbol name. (2) Greedy quantifier must expand-first before trying rest-nodes; the naive "try rest at each step" produces lazy behavior. (3) Capturing groups must match inner nodes in isolation first (to get the group's end position) then match rest — appending inner+rest-nodes would include rest in the capture string.
|
||||
|
||||
## Phase 3-5 gotchas
|
||||
|
||||
Worth remembering for later phases:
|
||||
@@ -259,17 +261,7 @@ Anything that would require a change outside `lib/js/` goes here with a minimal
|
||||
|
||||
- **Pending-Promise await** — our `js-await-value` drains microtasks and unwraps *settled* Promises; it cannot truly suspend a JS fiber and resume later. Every Promise that settles eventually through the synchronous `resolve`/`reject` + microtask path works. A Promise that never settles without external input (e.g. a real `setTimeout` waiting on the event loop) would hit the `"await on pending Promise (no scheduler)"` error. Proper async suspension would need the JS eval path to run under `cek-step-loop` (not `eval-expr` → `cek-run`) and treat `await pending-Promise` as a `perform` that registers a resume thunk on the Promise's callback list. Non-trivial plumbing; out of scope for this phase. Consider it a Phase 9.5 item.
|
||||
|
||||
- **Regex platform primitives** — runtime ships a substring-based stub (`js-regex-stub-test` / `-exec`). Overridable via `js-regex-platform-override!` so a real engine can be dropped in. Required platform-primitive surface:
|
||||
- `regex-compile pattern flags` — build an opaque compiled handle
|
||||
- `regex-test compiled s` → bool
|
||||
- `regex-exec compiled s` → match dict `{match index input groups}` or nil
|
||||
- `regex-match-all compiled s` → list of match dicts (or empty list)
|
||||
- `regex-replace compiled s replacement` → string
|
||||
- `regex-replace-fn compiled s fn` → string (fn receives match+groups, returns string)
|
||||
- `regex-split compiled s` → list of strings
|
||||
- `regex-source compiled` → string
|
||||
- `regex-flags compiled` → string
|
||||
Ideally a single `(js-regex-platform-install-all! platform)` entry point the host calls once at boot. OCaml would wrap `Str` / `Re` or a dedicated regex lib; JS host can just delegate to the native `RegExp`.
|
||||
- ~~**Regex platform primitives**~~ **RESOLVED** — `lib/js/regex.sx` ships a pure-SX recursive backtracking engine. Installs via `js-regex-platform-override!` at load. Covers: literals, `.`, `\d\w\s` and negations, `[abc]` / `[^abc]` / ranges, `^` `$` `\b \B`, `* + ? {n,m}` (greedy + lazy), capturing + non-capturing groups, alternation `a|b`, flags `i` (case-insensitive), `g` (global, advances lastIndex), `m` (multiline anchors). `js-regex-match-all` for String.matchAll. String.prototype.match regex path updated to use platform engine (was calling stub). 34 new unit tests added (5000–5033). Conformance: 148/148 (unchanged — slice had no regex fixtures).
|
||||
|
||||
- **Math trig + transcendental primitives missing.** The scoreboard shows 34× "TypeError: not a function" across the Math category — every one a test calling `Math.sin/cos/tan/log/…` on our runtime. We shim `Math` via `js-global`; the SX runtime supplies `sqrt`, `pow`, `abs`, `floor`, `ceil`, `round` and a hand-rolled `trunc`/`sign`/`cbrt`/`hypot`. Nothing else. Missing platform primitives (each is a one-line OCaml/JS binding, but a primitive all the same — we can't land approximation polynomials from inside the JS shim, they'd blow `Math.sin(1e308)` precision):
|
||||
- Trig: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`
|
||||
|
||||
@@ -39,93 +39,59 @@ Representation choices (finalise in phase 1, document here):
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + term parser (no operator table)
|
||||
- [x] Tokenizer: atoms (lowercase/quoted), variables (uppercase/`_`), numbers, strings, punct `( ) , . [ ] | ! :-`, comments (`%`, `/* */`)
|
||||
- [x] Parser: clauses `head :- body.` and facts `head.`; terms `atom | Var | number | compound(args) | [list,sugar]`
|
||||
- [x] **Skip for phase 1:** operator table. `X is Y + 1` must be written `is(X, '+'(Y, 1))`; `=` written `=(X, Y)`. Operators land in phase 4.
|
||||
- [x] Unit tests in `lib/prolog/tests/parse.sx` — 25 pass
|
||||
- [ ] Tokenizer: atoms (lowercase/quoted), variables (uppercase/`_`), numbers, strings, punct `( ) , . [ ] | ! :-`, comments (`%`, `/* */`)
|
||||
- [ ] Parser: clauses `head :- body.` and facts `head.`; terms `atom | Var | number | compound(args) | [list,sugar]`
|
||||
- [ ] **Skip for phase 1:** operator table. `X is Y + 1` must be written `is(X, '+'(Y, 1))`; `=` written `=(X, Y)`. Operators land in phase 4.
|
||||
- [ ] Unit tests in `lib/prolog/tests/parse.sx`
|
||||
|
||||
### Phase 2 — unification + trail
|
||||
- [x] `make-var`, `walk` (follow binding chain), `prolog-unify!` (terms + trail → bool), `trail-undo-to!`
|
||||
- [x] Occurs-check off by default, exposed as flag
|
||||
- [x] 30+ unification tests in `lib/prolog/tests/unify.sx`: atoms, vars, compounds, lists, cyclic (no-occurs-check), mutual occurs — 47 pass
|
||||
- [ ] `make-var`, `walk` (follow binding chain), `prolog-unify!` (terms + trail → bool), `trail-undo-to!`
|
||||
- [ ] Occurs-check off by default, exposed as flag
|
||||
- [ ] 30+ unification tests in `lib/prolog/tests/unify.sx`: atoms, vars, compounds, lists, cyclic (no-occurs-check), mutual occurs
|
||||
|
||||
### Phase 3 — clause DB + DFS solver + cut + first classic programs
|
||||
- [x] Clause DB: `"functor/arity" → list-of-clauses`, loader inserts — `pl-mk-db` / `pl-db-add!` / `pl-db-load!` / `pl-db-lookup` / `pl-db-lookup-goal`, 14 tests in `tests/clausedb.sx`
|
||||
- [x] Solver: DFS with choice points backed by delimited continuations (`lib/callcc.sx`). On goal entry, capture; per matching clause, unify head + recurse body; on failure, undo trail, try next — first cut: trail-based undo + CPS k (no shift/reset yet, per briefing gotcha). Built-ins so far: `true/0`, `fail/0`, `=/2`, `,/2`. Refactor to delimited conts later.
|
||||
- [x] Cut (`!`): cut barrier at current choice-point frame; collapse all up to barrier — two-cut-box scheme: each `pl-solve-user!` creates a fresh inner-cut-box (set by `!` in this predicate's body) AND snapshots the outer-cut-box state on entry. After body fails, abandon clause alternatives if (a) inner was set or (b) outer transitioned false→true during this call. Lets post-cut goals backtrack normally while blocking pre-cut alternatives. 6 cut tests cover bare cut, clause-commit, choice-commit, cut+fail, post-cut backtracking, nested-cut isolation.
|
||||
- [x] Built-ins: `=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2` inside `;`, `call/1`, `write/1`, `nl/0` — all 11 done. `write/1` and `nl/0` use a global `pl-output-buffer` string + `pl-output-clear!` for testability; `pl-format-term` walks deep then renders atoms/nums/strs/compounds/vars (var → `_<id>`). Note: cut-transparency via `;` not testable yet without operator support — `;(,(a,!), b)` parser-rejects because `,` is body-operator-only; revisit in phase 4.
|
||||
- [x] Arithmetic `is/2` with `+ - * / mod abs` — `pl-eval-arith` walks deep, recurses on compounds, dispatches on functor; binary `+ - * / mod`, binary AND unary `-`, unary `abs`. `is/2` evaluates RHS, wraps as `("num" v)`, unifies via `pl-solve-eq!`. 11 tests cover each op + nested + ground LHS match/mismatch + bound-var-on-RHS chain.
|
||||
- [x] Classic programs in `lib/prolog/tests/programs/`:
|
||||
- [x] `append.pl` — list append (with backtracking) — `lib/prolog/tests/programs/append.{pl,sx}`. 6 tests cover: build (`append([], L, X)`, `append([1,2], [3,4], X)`), check ground match/mismatch, full split-backtracking (`append(X, Y, [1,2,3])` → 4 solutions), single-deduce (`append(X, [3], [1,2,3])` → X=[1,2]).
|
||||
- [x] `reverse.pl` — naive reverse — `lib/prolog/tests/programs/reverse.{pl,sx}`. Naive reverse via append: `reverse([H|T], R) :- reverse(T, RT), append(RT, [H], R)`. 6 tests cover empty, singleton, 3-list, 4-atom-list, ground match, ground mismatch.
|
||||
- [x] `member.pl` — generate all solutions via backtracking — `lib/prolog/tests/programs/member.{pl,sx}`. Classic 2-clause `member(X, [X|_])` + `member(X, [_|T]) :- member(X, T)`. 7 tests cover bound-element hit/miss, empty list, generator (count = list length), first-solution binding, duplicate matches counted twice, anonymous head-cell unification.
|
||||
- [x] `nqueens.pl` — 8-queens — `lib/prolog/tests/programs/nqueens.{pl,sx}`. Permute-and-test formulation: `queens(L, Qs) :- permute(L, Qs), safe(Qs)` + `select` + `safe` + `no_attack`. Tested at N=1 (1), N=2 (0), N=3 (0), N=4 (2), N=5 (10) plus first-solution check at N=4 = `[2, 4, 1, 3]`. N=8 omitted — interpreter is too slow (40320 perms); add once compiled clauses or constraint-style placement land. `range/3` skipped pending arithmetic-comparison built-ins (`>/2` etc.).
|
||||
- [x] `family.pl` — facts + rules (parent/ancestor) — `lib/prolog/tests/programs/family.{pl,sx}`. 5 parent facts + male/female + derived `father`/`mother`/`ancestor`/`sibling`. 10 tests cover direct facts, fact count, transitive ancestor through 3 generations, descendant counting, gender-restricted father/mother, sibling via shared parent + `\=`.
|
||||
- [x] `lib/prolog/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md` — bash script feeds load + eval epoch script to sx_server, parses each suite's `{:failed N :passed N :total N :failures (...)}` line, writes JSON (machine) + MD (human) scoreboards. Exit non-zero on any failure. `SX_SERVER` env var overrides binary path. First scoreboard: 183 / 183.
|
||||
- [x] Target: all 5 classic programs passing — append (6) + reverse (6) + member (7) + nqueens (6) + family (10) = 35 program tests, all green. Phase 3 architecturally complete bar the conformance harness/scoreboard.
|
||||
- [ ] Clause DB: `"functor/arity" → list-of-clauses`, loader inserts
|
||||
- [ ] Solver: DFS with choice points backed by delimited continuations (`lib/callcc.sx`). On goal entry, capture; per matching clause, unify head + recurse body; on failure, undo trail, try next
|
||||
- [ ] Cut (`!`): cut barrier at current choice-point frame; collapse all up to barrier
|
||||
- [ ] Built-ins: `=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2` inside `;`, `call/1`, `write/1`, `nl/0`
|
||||
- [ ] Arithmetic `is/2` with `+ - * / mod abs`
|
||||
- [ ] Classic programs in `lib/prolog/tests/programs/`:
|
||||
- [ ] `append.pl` — list append (with backtracking)
|
||||
- [ ] `reverse.pl` — naive reverse
|
||||
- [ ] `member.pl` — generate all solutions via backtracking
|
||||
- [ ] `nqueens.pl` — 8-queens
|
||||
- [ ] `family.pl` — facts + rules (parent/ancestor)
|
||||
- [ ] `lib/prolog/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
|
||||
- [ ] Target: all 5 classic programs passing
|
||||
|
||||
### Phase 4 — operator table + more built-ins (next run)
|
||||
- [x] Operator table parsing (prefix/infix/postfix, precedence, assoc) — `pl-op-table` (15 entries: `, ; -> = \= is < > =< >= + - * / mod`); precedence-climbing parser via `pp-parse-primary` + `pp-parse-term-prec` + `pp-parse-op-rhs`. Parens override precedence. Args inside compounds parsed at 999 so `,` stays as separator. xfx/xfy/yfx supported; prefix/postfix deferred (so `-5` still tokenises as bare atom + num as before). Comparison built-ins `</2 >/2 =</2 >=/2` added. New `tests/operators.sx` 19 tests cover assoc/precedence/parens + solver via infix.
|
||||
- [x] `assert/1`, `asserta/1`, `assertz/1`, `retract/1` — `assert` aliases `assertz`. Helpers `pl-rt-to-ast` (deep-walk + replace runtime vars with `_G<id>` parse markers) + `pl-build-clause` (detect `:-` head). `assertz` uses `pl-db-add!`; `asserta` uses new `pl-db-prepend!`. `retract` walks goal, looks up by functor/arity, tries each clause via unification, removes first match by index (`pl-list-without`). 11 tests in `tests/dynamic.sx`. Rule-asserts now work — `:-` added to op table (prec 1200 xfx) with fix to `pl-token-op` accepting `"op"` token type. 15 tests in `tests/assert_rules.sx`.
|
||||
- [x] `findall/3`, `bagof/3`, `setof/3` — shared `pl-collect-solutions` runs the goal in a fresh cut-box, deep-copies the template (via `pl-deep-copy` with var-map for shared-var preservation) on each success, returns false to backtrack, then restores trail. `findall` always succeeds with a (possibly empty) list. `bagof` fails on empty. `setof` builds a string-keyed dict via `pl-format-term` for sort+dedupe (via `keys` + `sort`), fails on empty. Existential `^` deferred (operator). 11 tests in `tests/findall.sx`.
|
||||
- [x] `copy_term/2`, `functor/3`, `arg/3`, `=../2` — `copy_term/2` reuses `pl-deep-copy` with a fresh var-map (preserves source aliasing). `functor/3` handles 4 modes: compound→{name, arity}, atom→{atom, 0}, num→{num, 0}, var with ground name+arity→constructed term (`pl-make-fresh-args` for compound case). `arg/3` extracts 1-indexed arg from compound. **`=../2` deferred** — the tokenizer treats `.` as the clause terminator unconditionally, so `=..` lexes as `=` + `.` + `.`; needs special-case lex (or surface syntax via a different name). 14 tests in `tests/term_inspect.sx`.
|
||||
- [x] String/atom predicates
|
||||
- [ ] Operator table parsing (prefix/infix/postfix, precedence, assoc)
|
||||
- [ ] `assert/1`, `asserta/1`, `assertz/1`, `retract/1`
|
||||
- [ ] `findall/3`, `bagof/3`, `setof/3`
|
||||
- [ ] `copy_term/2`, `functor/3`, `arg/3`, `=../2`
|
||||
- [ ] String/atom predicates
|
||||
|
||||
### Phase 5 — Hyperscript integration
|
||||
- [x] `prolog-query` primitive callable from SX/Hyperscript
|
||||
- [x] Hyperscript DSL: `when allowed(user, action) then …` — `lib/prolog/hs-bridge.sx`: `pl-hs-query` (bool goal test) + `pl-hs-predicate/1,2,3` factories + `pl-hs-install`. No parser/compiler changes needed: Hyperscript already compiles `allowed(user, action)` to `(allowed user action)` — a plain SX call backed by the Prolog DB.
|
||||
- [x] Integration suite
|
||||
- [ ] `prolog-query` primitive callable from SX/Hyperscript
|
||||
- [ ] Hyperscript DSL: `when allowed(user, :edit) then …`
|
||||
- [ ] Integration suite
|
||||
|
||||
### Phase 6 — ISO conformance
|
||||
- [x] Vendor Hirst's conformance tests
|
||||
- [x] Drive scoreboard to 200+
|
||||
- [ ] Vendor Hirst's conformance tests
|
||||
- [ ] Drive scoreboard to 200+
|
||||
|
||||
### Phase 7 — compiler (later, optional)
|
||||
- [x] Compile clauses to SX continuations for speed
|
||||
- [x] Keep interpreter as the reference
|
||||
- [ ] Compile clauses to SX continuations for speed
|
||||
- [ ] Keep interpreter as the reference
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first. Agent appends on every commit._
|
||||
|
||||
- 2026-05-06 — Hyperscript bridge (`lib/prolog/hs-bridge.sx`): `pl-hs-query`, `pl-hs-predicate/1,2,3`, `pl-hs-install`. No parser/compiler changes needed — Hyperscript already compiles `when allowed(user, action)` to `(allowed user action)`, a plain SX call; bridge factories wire a Prolog DB as the backing implementation. 19 tests in `tests/hs_bridge.sx`. Total **590** (+19).
|
||||
- 2026-05-05 — Integration test suite (`tests/integration.sx`): 20 end-to-end tests via `pl-query-*` API covering permission system (6), graph reachability (4), quicksort (4), fibonacci (3), dynamic KB (3). Suite added to conformance harness. Total **571** (+20).
|
||||
- 2026-04-25 — `pl-compiled-matches-interp?` cross-validator in `compiler.sx`: loads source into both a plain and a compiled DB, runs the same goal, returns true iff solution counts match. `tests/cross_validate.sx` applies this to 17 goals across append/member/ancestor/cut/arithmetic/if-then-else, locking the interpreter as the reference against which any future compiler change must agree. Total **551** (+17).
|
||||
- 2026-04-25 — Clause compiler (`lib/prolog/compiler.sx`): `pl-compile-clause` converts parse-AST clauses to SX closures `(fn (goal trail db cut-box k) bool)`. Pre-collects var names at compile time; `pl-cmp-build-term` reconstructs fresh runtime terms per call. `pl-compile-db!` compiles all clauses in a DB and stores them in `:compiled` table. `pl-solve-user!` in runtime.sx auto-dispatches to compiled lambdas when present, falls back to interpreted. `pl-try-compiled-clauses!` mirrors `pl-try-clauses!` cut semantics. 17 tests in `tests/compiler.sx`. Total **534** (+17).
|
||||
- 2026-04-25 — `predsort/3` (insertion-sort with 3-arg comparator predicate, deduplicates `=` pairs), `term_variables/2` (collect unbound vars left-to-right, dedup by id), arithmetic extensions (`floor/1`, `ceiling/1`, `truncate/1`, `round/1`, `sign/1`, `sqrt/1`, `pow/2`, `**/2`, `^/2`, `integer/1`, `float/1`, `float_integer_part/1`, `float_fractional_part/1`). 21 tests in `tests/advanced.sx`. Total **517** (+21).
|
||||
- 2026-04-25 — `sub_atom/5` (non-deterministic substring enumeration; CPS loop over all (start,sublen) pairs; trail-undo only on backtrack) + `aggregate_all/3` (6 templates: count/bag/sum/max/min/set; uses `pl-collect-solutions`). 25 tests in `tests/string_agg.sx`. Total **496** (+25).
|
||||
- 2026-04-25 — `:-` operator + assert with rules: added `(list ":-" 1200 "xfx")` to `pl-op-table`; fixed `pl-token-op` to accept `"op"` token type (tokenizer emits `:-` as `"op"`, not `"atom"`). `pl-build-clause` already handled `("compound" ":-" ...)`. `assert((head :- body))` now works for facts+rules. 15 tests in `tests/assert_rules.sx`. Total **471** (+15).
|
||||
- 2026-04-25 — IO/term predicates: `term_to_atom/2` (bidirectional: format term or parse atom), `term_string/2` (alias), `with_output_to/2` (atom/string sinks — saves/restores `pl-output-buffer`), `writeln/1`, `format/1` (~n/~t/~~), `format/2` (~w/~a/~d pull from arg list). 24 tests in `tests/io_predicates.sx`. Total **456** (+24).
|
||||
- 2026-04-25 — Char predicates: `char_type/2` (9 modes: alpha/alnum/digit/digit(N)/space/white/upper(L)/lower(U)/ascii(C)/punct), `upcase_atom/2`, `downcase_atom/2`, `string_upper/2`, `string_lower/2`. 10 helpers using `char-code`/`char-from-code` SX primitives. 27 tests in `tests/char_predicates.sx`. Total **432** (+27).
|
||||
- 2026-04-25 — Set/fold predicates: `foldl/4` (CPS fold-left, threads accumulator via `pl-apply-goal`), `list_to_set/2` (dedup preserving first-occurrence), `intersection/3`, `subtract/3`, `union/3` (all via `pl-struct-eq?`). 3 new helpers, 15 tests in `tests/set_predicates.sx`. Total **405** (+15).
|
||||
- 2026-04-25 — Meta-call predicates: `forall/2` (negation-of-counterexample), `maplist/2` (goal over list), `maplist/3` (map goal building output list), `include/3` (filter by goal success), `exclude/3` (filter by goal failure). New `pl-apply-goal` helper extends a goal with extra args. 15 tests in `tests/meta_call.sx`. Total **390** (+15).
|
||||
- 2026-04-25 — List/utility predicates: `==/2`, `\==/2` (structural equality/inequality via `pl-struct-eq?`), `flatten/2` (deep Prolog-list flatten), `numlist/3` (integer range list), `atomic_list_concat/2` (join with no sep), `atomic_list_concat/3` (join with separator), `sum_list/2`, `max_list/2`, `min_list/2` (arithmetic folds), `delete/3` (remove all struct-equal elements). 7 new helpers, 33 tests in `tests/list_predicates.sx`. Total **375** (+33).
|
||||
- 2026-04-25 — Meta/logic predicates: `\+/1` (negation-as-failure, trail-undo on success), `not/1` (alias), `once/1` (commit to first solution via if-then-else), `ignore/1` (always succeed), `ground/1` (all vars bound), `sort/2` (sort + dedup by formatted key), `msort/2` (sort, keep dups), `atom_number/2` (bidirectional), `number_string/2` (bidirectional). 2 helpers (`pl-ground?`, `pl-sort-pairs-dedup`). 25 tests in `tests/meta_predicates.sx`. Total **342** (+25).
|
||||
- 2026-04-25 — ISO utility predicates batch: `succ/2` (bidirectional), `plus/3` (3-mode bidirectional), `between/3` (backtracking range generator), `length/2` (bidirectional list length + var-list constructor), `last/2`, `nth0/3`, `nth1/3`, `max/2` + `min/2` in arithmetic eval. 6 new helper functions (`pl-list-length`, `pl-make-list-of-vars`, `pl-between-loop!`, `pl-solve-between!`, `pl-solve-last!`, `pl-solve-nth0!`). 29 tests in `tests/iso_predicates.sx`. Phase 6 complete: scoreboard already at 317, far above 200+ target. Hyperscript DSL blocked (needs `lib/hyperscript/**`). Total **317** (+29).
|
||||
- 2026-04-25 — `prolog-query` SX API (`lib/prolog/query.sx`). New public API layer: `pl-load source-str → db`, `pl-query-all db query-str → list of solution dicts`, `pl-query-one db query-str → dict or nil`, `pl-query src query → list` (convenience). Each solution dict maps variable name strings to their formatted term strings. Var names extracted from pre-instantiation parse AST. Trail is marked before solve and reset after to ensure clean state. 16 tests in `tests/query_api.sx` cover fact lookup, no-solution, boolean queries, multi-var, recursive rules, is/2 built-in, query-one, convenience form. Total **288** (+16).
|
||||
- 2026-04-25 — String/atom predicates. Type-test predicates: `var/1`, `nonvar/1`, `atom/1`, `number/1`, `integer/1`, `float/1` (always-fail), `compound/1`, `callable/1`, `atomic/1`, `is_list/1`. String/atom operations: `atom_length/2`, `atom_concat/3` (3 modes: both-ground, result+first, result+second), `atom_chars/2` (bidirectional), `atom_codes/2` (bidirectional), `char_code/2` (bidirectional), `number_codes/2`, `number_chars/2`. 7 helper functions in runtime.sx (`pl-list-to-prolog`, `pl-proper-list?`, `pl-prolog-list-to-sx`, `pl-solve-atom-concat!`, `pl-solve-atom-chars!`, `pl-solve-atom-codes!`, `pl-solve-char-code!`). 34 tests in `tests/atoms.sx`. Total **272** (+34).
|
||||
- 2026-04-25 — `copy_term/2` + `functor/3` + `arg/3` (term inspection). `copy_term` is a one-line dispatch to existing `pl-deep-copy`. `functor/3` is bidirectional — decomposes a bound compound/atom/num into name+arity OR constructs from ground name+arity (atom+positive-arity → compound with N anonymous fresh args via `pl-make-fresh-args`; arity 0 → atom/num). `arg/3` extracts 1-indexed arg with bounds-fail. New helper `pl-solve-eq2!` for paired-unification with shared trail-undo. 14 tests in `tests/term_inspect.sx`. Total **238** (+14). `=..` deferred — `.` always tokenizes as clause terminator; needs special lexer case.
|
||||
- 2026-04-25 — `findall/3` + `bagof/3` + `setof/3`. Shared collector `pl-collect-solutions` runs the goal in a fresh cut-box, deep-copies the template per success (`pl-deep-copy` walks term, allocates fresh runtime vars via shared var-map so co-occurrences keep aliasing), returns false to keep backtracking, then `pl-trail-undo-to!` to clean up. `findall` always builds a list. `bagof` fails on empty. `setof` uses a `pl-format-term`-keyed dict + SX `sort` for dedupe + ordering. New `tests/findall.sx` 11 tests. Total **224** (+11). Existential `^` deferred — needs operator.
|
||||
- 2026-04-25 — Dynamic clauses: `assert/1`, `assertz/1`, `asserta/1`, `retract/1`. New helpers `pl-rt-to-ast` (deep-walk runtime term → parse-AST, mapping unbound runtime vars to `_G<id>` markers so `pl-instantiate-fresh` produces fresh vars per call) + `pl-build-clause` + `pl-db-prepend!` + `pl-list-without`. `retract` keeps runtime vars (so the caller's vars get bound), walks head for the functor/arity key, tries each stored clause via `pl-unify!`, removes the first match by index. 11 tests in `tests/dynamic.sx`; conformance script gained dynamic row. Total **213** (+11). Rule-form asserts (`(H :- B)`) deferred until `:-` is in the op table.
|
||||
- 2026-04-25 — Phase 4 starts: operator-table parsing. Parser rewrite uses precedence climbing (xfx/xfy/yfx); 15-op table covers control (`, ; ->`), comparison (`= \\= is < > =< >=`), arithmetic (`+ - * / mod`). Parens override. Backwards-compatible: prefix-syntax compounds (`=(X, Y)`, `+(2, 3)`) still parse as before; existing 183 tests untouched. Added comparison built-ins `</2 >/2 =</2 >=/2` to runtime (eval both sides, compare). New `tests/operators.sx` 19 tests; conformance script gained an operators row. Total **202** (+19). Prefix/postfix deferred — `-5` keeps old bare-atom semantics.
|
||||
- 2026-04-25 — Conformance harness landed. `lib/prolog/conformance.sh` runs all 9 suites in one sx_server epoch, parses the `{:failed/:passed/:total/:failures}` summary lines, and writes `scoreboard.json` + `scoreboard.md`. `SX_SERVER` env var overrides the binary path; default points at the main-repo build. Phase 3 fully complete: 183 / 183 passing across parse/unify/clausedb/solve/append/reverse/member/nqueens/family.
|
||||
- 2026-04-25 — `family.pl` fifth classic program — completes the 5-program target. 5-fact pedigree + male/female + derived father/mother/ancestor/sibling. 10 tests cover fact lookup + count, transitive ancestor through 3 generations, descendant counting (5), gender-restricted derivations, sibling via shared parent guarded by `\=`. Total 183 (+10). All 5 classic programs ticked; Phase 3 needs only conformance harness + scoreboard left.
|
||||
- 2026-04-25 — `nqueens.pl` fourth classic program. Permute-and-test variant exercises every Phase-3 feature: lists with `[H|T]` cons sugar, multi-clause backtracking, recursive `permute`/`select`/`safe`/`no_attack`, `is/2` arithmetic on diagonals, `\=/2` for diagonal-conflict check. 6 tests at N ∈ {1,2,3,4,5} with expected counts {1,0,0,2,10} + first-solution `[2,4,1,3]`. N=5 takes ~30s (120 perms × safe-check); N=8 omitted as it would be ~thousands of seconds. Total 173 (+6).
|
||||
- 2026-04-25 — `member.pl` third classic program. Standard 2-clause definition; 7 tests cover bound-element hit/miss, empty-list fail, generator-count = list length, first-solution binding (X=11), duplicate elements matched twice on backtrack, anonymous-head unification (`member(a, [X, b, c])` binds X=a). Total 167 (+7).
|
||||
- 2026-04-25 — `reverse.pl` second classic program. Naive reverse defined via append. 6 tests (empty/singleton/3-list/4-atom-list/ground match/ground mismatch). Confirms the solver handles non-trivial recursive composition: `reverse([1,2,3], R)` recurses to depth 3 then unwinds via 3 nested `append`s. Total 160 (+6).
|
||||
- 2026-04-25 — `append.pl` first classic program. `lib/prolog/tests/programs/append.pl` is the canonical 2-clause source; `append.sx` embeds the source as a string (no file-read primitive in SX yet) and runs 6 tests covering build, check, full split-backtrack (4 solutions), and deduction modes. Helpers `pl-ap-list-to-sx` / `pl-ap-term-to-sx` convert deep-walked Prolog lists (`("compound" "." (h t))` / `("atom" "[]")`) to SX lists for structural assertion. Total 154 (+6).
|
||||
- 2026-04-25 — `is/2` arithmetic landed. `pl-eval-arith` recursively evaluates ground RHS expressions (binary `+ - * /`, `mod`; binary+unary `-`; unary `abs`); `is/2` wraps the value as `("num" v)` and unifies via `pl-solve-eq!`, so it works in all three modes — bind unbound LHS, check ground LHS for equality, propagate from earlier var bindings on RHS. 11 tests, total 148 (+11). Without operator support, expressions must be written prefix: `is(X, +(2, *(3, 4)))`.
|
||||
- 2026-04-25 — `write/1` + `nl/0` landed using global string buffer (`pl-output-buffer` + `pl-output-clear!` + `pl-output-write!`). `pl-format-term` walks deep + dispatches on atom/num/str/compound/var; `pl-format-args` recursively comma-joins. 7 new tests cover atom/num/compound formatting, conjunction order, var-walk, and `nl`. Built-ins box (`=/2`, `\\=/2`, `true/0`, `fail/0`, `!/0`, `,/2`, `;/2`, `->/2`, `call/1`, `write/1`, `nl/0`) now ticked. Total 137 (+7).
|
||||
- 2026-04-25 — `->/2` if-then-else landed (both `;(->(C,T), E)` and standalone `->(C, T)` ≡ `(C -> T ; fail)`). `pl-solve-or!` now special-cases `->` in left arg → `pl-solve-if-then-else!`. Cond runs in a fresh local cut-box (ISO opacity for cut inside cond). Then-branch can backtrack, else-branch can backtrack, but cond commits to first solution. 9 new tests covering both forms, both branches, binding visibility, cond-commit, then-backtrack, else-backtrack. Total 130 (+9).
|
||||
- 2026-04-25 — Built-ins `\=/2`, `;/2`, `call/1` landed. `pl-solve-not-eq!` (try unify, always undo, succeed iff unify failed). `pl-solve-or!` (try left, on failure check cut and only try right if not cut). `call/1` opens a fresh inner cut-box (ISO opacity: cut inside `call(G)` commits G, not caller). 11 new tests in `tests/solve.sx` cover atoms+vars for `\=`, both branches + count for `;`, and `call/1` against atoms / compounds / bound goal vars. Total 121 (+11). Box not yet ticked — `->/2`, `write/1`, `nl/0` still pending.
|
||||
- 2026-04-25 — Cut (`!/0`) landed. `pl-cut?` predicate; solver functions all take a `cut-box`; `pl-solve-user!` creates a fresh inner-cut-box and snapshots `outer-was-cut`; `pl-try-clauses!` abandons alternatives when inner.cut OR (outer.cut transitioned false→true during this call). 6 new cut tests in `tests/solve.sx` covering bare cut, clause-commit, choice-commit, cut+fail blocks alt clauses, post-cut goal backtracks freely, inner cut isolation. Total 110 (+6).
|
||||
- 2026-04-25 — Phase 3 DFS solver landed (CPS, trail-based backtracking; delimited conts deferred). `pl-solve!` + `pl-solve-eq!` + `pl-solve-user!` + `pl-try-clauses!` + `pl-solve-once!` + `pl-solve-count!` in runtime.sx. Built-ins: `true/0`, `fail/0`, `=/2`, `,/2`. New `tests/solve.sx` 18/18 green covers atomic goals, =, conjunction, fact lookup, multi-solution count, recursive ancestor rule, trail-undo verification. Bug fix: `pl-instantiate` had no `("clause" h b)` case → vars in rule head/body were never instantiated, so rule resolution silently failed against runtime-var goals. Added clause case to recurse with shared var-env. Total 104 (+18).
|
||||
- 2026-04-24 — Phase 3 clause DB landed: `pl-mk-db` + `pl-head-key` / `pl-clause-key` / `pl-goal-key` + `pl-db-add!` / `pl-db-load!` / `pl-db-lookup` / `pl-db-lookup-goal` in runtime.sx. New `tests/clausedb.sx` 14/14 green. Total 86 (+14). Loader preserves declaration order (append!).
|
||||
- 2026-04-24 — Verified phase 1+2 already implemented on loops/prolog: `pl-parse-tests-run!` 25/25, `pl-unify-tests-run!` 47/47 (72 total). Ticked phase 1+2 boxes.
|
||||
- _(awaiting phase 1)_
|
||||
|
||||
## Blockers
|
||||
|
||||
_Shared-file issues that need someone else to fix. Minimal repro only._
|
||||
|
||||
- **Phase 5 Hyperscript DSL** — `lib/hyperscript/**` is out of scope for this loop. Needs `lib/hyperscript/parser.sx` + evaluator to add `when allowed(user, :edit) then …` syntax. Skipping; Phase 5 item 1 (`prolog-query` SX API) is done.
|
||||
- _(none yet)_
|
||||
|
||||
134
plans/ruby-on-sx.md
Normal file
134
plans/ruby-on-sx.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Ruby-on-SX: fibers + blocks + open classes on delimited continuations
|
||||
|
||||
The headline showcase is **fibers** — Ruby's `Fiber.new { … Fiber.yield v … }` / `Fiber.resume` are textbook delimited continuations with sugar. MRI implements them by swapping C stacks; on SX they fall out of the existing `perform`/`cek-resume` machinery for free. Plus blocks/yield (lexical escape continuations, same shape as Smalltalk's non-local return), method_missing, and singleton classes.
|
||||
|
||||
End-state goal: Ruby 2.7-flavoured subset, Enumerable mixin, fibers + threads-via-fibers (no real OS threads), method_missing-driven DSLs, ~150 hand-written + classic programs.
|
||||
|
||||
## Scope decisions (defaults — override by editing before we spawn)
|
||||
|
||||
- **Syntax:** Ruby 2.7. No 3.x pattern matching, no rightward assignment, no endless methods. We pick 2.7 because it's the biggest semantic surface that still parses cleanly.
|
||||
- **Conformance:** "Reads like Ruby, runs like Ruby." Slice of RubySpec (Core + Library subset), not full RubySpec.
|
||||
- **Test corpus:** custom + curated RubySpec slice. Plus classic programs: fiber-based generator, internal DSL with method_missing, mixin-based Enumerable on a custom class.
|
||||
- **Out of scope:** real threads, GIL, refinements, `binding_of_caller` from non-Ruby contexts, Encoding object beyond UTF-8/ASCII-8BIT, RubyVM::* introspection beyond bytecode-disassembly placeholder, IO subsystem beyond `puts`/`gets`/`File.read`.
|
||||
- **Symbols:** SX symbols. Strings are mutable copies; symbols are interned.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/ruby/**` and `plans/ruby-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Ruby primitives go in `lib/ruby/runtime.sx`.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
Ruby source
|
||||
│
|
||||
▼
|
||||
lib/ruby/tokenizer.sx — keywords, ops, %w[], %i[], heredocs (deferred), regex (deferred)
|
||||
│
|
||||
▼
|
||||
lib/ruby/parser.sx — AST: classes, modules, methods, blocks, calls
|
||||
│
|
||||
▼
|
||||
lib/ruby/transpile.sx — AST → SX AST (entry: rb-eval-ast)
|
||||
│
|
||||
▼
|
||||
lib/ruby/runtime.sx — class table, MOP, dispatch, fibers, primitives
|
||||
```
|
||||
|
||||
Core mapping:
|
||||
- **Object** = SX dict `{:class :ivars :singleton-class?}`. Instance variables live in `ivars` keyed by symbol.
|
||||
- **Class** = SX dict `{:name :superclass :methods :class-methods :metaclass :includes :prepends}`. Class table is flat.
|
||||
- **Method dispatch** = lookup walks ancestor chain (prepended → class → included modules → superclass → …). Falls back to `method_missing` with a `Symbol`+args.
|
||||
- **Block** = lambda + escape continuation. `yield` invokes the block in current context. `return` from within a block invokes the enclosing-method's escape continuation.
|
||||
- **Proc** = lambda without strict arity. `Proc.new` + `proc {}`.
|
||||
- **Lambda** = lambda with strict arity + `return`-returns-from-lambda semantics.
|
||||
- **Fiber** = pair of continuations (resume-k, yield-k) wrapped in a record. `Fiber.new { … }` builds it; `Fiber.resume` invokes the resume-k; `Fiber.yield` invokes the yield-k. Built directly on `perform`/`cek-resume`.
|
||||
- **Module** = class without instance allocation. `include` puts it in the chain; `prepend` puts it earlier; `extend` puts it on the singleton.
|
||||
- **Singleton class** = lazily allocated per-object class for `def obj.foo` definitions.
|
||||
- **Symbol** = interned SX symbol. `:foo` reads as `(quote foo)` flavour.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: keywords (`def end class module if unless while until do return yield begin rescue ensure case when then else elsif`), identifiers (lowercase = local/method, `@` = ivar, `@@` = cvar, `$` = global, uppercase = constant), numbers (int, float, `0x` `0o` `0b`, `_` separators), strings (`"…"` interpolation, `'…'` literal, `%w[a b c]`, `%i[a b c]`), symbols `:foo` `:"…"`, operators (`+ - * / % ** == != < > <= >= <=> === =~ !~ << >> & | ^ ~ ! && || and or not`), `:: . , ; ( ) [ ] { } -> => |`, comments `#`
|
||||
- [ ] Parser: program is sequence of statements separated by newlines or `;`; method def `def name(args) … end`; class `class Foo < Bar … end`; module `module M … end`; block `do |a, b| … end` and `{ |a, b| … }`; call sugar (no parens), `obj.method`, `Mod::Const`; arg shapes (positional, default, splat `*args`, double-splat `**opts`, block `&blk`)
|
||||
- [ ] If/while/case expressions (return values), `unless`/`until`, postfix modifiers
|
||||
- [ ] Begin/rescue/ensure/retry, raise, raise with class+message
|
||||
- [ ] Unit tests in `lib/ruby/tests/parse.sx`
|
||||
|
||||
### Phase 2 — object model + sequential eval
|
||||
- [ ] Class table bootstrap: `BasicObject`, `Object`, `Kernel`, `Module`, `Class`, `Numeric`, `Integer`, `Float`, `String`, `Symbol`, `Array`, `Hash`, `Range`, `NilClass`, `TrueClass`, `FalseClass`, `Proc`, `Method`
|
||||
- [ ] `rb-eval-ast`: literals, variables (local, ivar, cvar, gvar, constant), assignment (single and parallel `a, b = 1, 2`, splat receive), method call, message dispatch
|
||||
- [ ] Method lookup walks ancestor chain; cache hit-class per `(class, selector)`
|
||||
- [ ] `method_missing` fallback constructing args list
|
||||
- [ ] `super` and `super(args)` — lookup in defining class's superclass
|
||||
- [ ] Singleton class allocation on first `def obj.foo` or `class << obj`
|
||||
- [ ] `nil`, `true`, `false` are singletons of their classes; tagged values aren't boxed
|
||||
- [ ] Constant lookup (lexical-then-inheritance) with `Module.nesting`
|
||||
- [ ] 60+ tests in `lib/ruby/tests/eval.sx`
|
||||
|
||||
### Phase 3 — blocks + procs + lambdas
|
||||
- [ ] Method invocation captures escape continuation `^k` for `return`; binds it as block's escape
|
||||
- [ ] `yield` invokes implicit block
|
||||
- [ ] `block_given?`, `&blk` parameter, `&proc` arg unpacking
|
||||
- [ ] `Proc.new`, `proc { }`, `lambda { }` (or `->(x) { x }`)
|
||||
- [ ] Lambda strict arity + lambda-local `return` semantics
|
||||
- [ ] Proc lax arity (`a, b, c` unpacks Array; missing args nil)
|
||||
- [ ] `break`, `next`, `redo` — `break` is escape-from-loop-or-block; `next` is escape-from-block-iteration; `redo` re-runs current iteration
|
||||
- [ ] 30+ tests in `lib/ruby/tests/blocks.sx`
|
||||
|
||||
### Phase 4 — fibers (THE SHOWCASE)
|
||||
- [ ] `Fiber.new { |arg| … Fiber.yield v … }` allocates a fiber record with paired continuations
|
||||
- [ ] `Fiber.resume(args…)` resumes the fiber, returning the value passed to `Fiber.yield`
|
||||
- [ ] `Fiber.yield(v)` from inside the fiber suspends and returns control to the resumer
|
||||
- [ ] `Fiber.current` from inside the fiber
|
||||
- [ ] `Fiber#alive?`, `Fiber#raise` (deferred)
|
||||
- [ ] `Fiber.transfer` — symmetric coroutines (resume from any side)
|
||||
- [ ] Classic programs in `lib/ruby/tests/programs/`:
|
||||
- [ ] `generator.rb` — pull-style infinite enumerator built on fibers
|
||||
- [ ] `producer-consumer.rb` — bounded buffer with `Fiber.transfer`
|
||||
- [ ] `tree-walk.rb` — recursive tree walker that yields each node, driven by `Fiber.resume`
|
||||
- [ ] `lib/ruby/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
|
||||
|
||||
### Phase 5 — modules + mixins + metaprogramming
|
||||
- [ ] `include M` — appends M's methods after class methods in chain
|
||||
- [ ] `prepend M` — prepends M before class methods
|
||||
- [ ] `extend M` — adds M to singleton class
|
||||
- [ ] `Module#ancestors`, `Module#included_modules`
|
||||
- [ ] `define_method`, `class_eval`, `instance_eval`, `module_eval`
|
||||
- [ ] `respond_to?`, `respond_to_missing?`, `method_missing`
|
||||
- [ ] `Object#send`, `Object#public_send`, `Object#__send__`
|
||||
- [ ] `Module#method_added`, `singleton_method_added` hooks
|
||||
- [ ] Hooks: `included`, `extended`, `inherited`, `prepended`
|
||||
- [ ] Internal-DSL classic program: `lib/ruby/tests/programs/dsl.rb`
|
||||
|
||||
### Phase 6 — stdlib drive
|
||||
- [ ] `Enumerable` mixin: `each` (abstract), `map`, `select`/`filter`, `reject`, `reduce`/`inject`, `each_with_index`, `each_with_object`, `take`, `drop`, `take_while`, `drop_while`, `find`/`detect`, `find_index`, `any?`, `all?`, `none?`, `one?`, `count`, `min`, `max`, `min_by`, `max_by`, `sort`, `sort_by`, `group_by`, `partition`, `chunk`, `each_cons`, `each_slice`, `flat_map`, `lazy`
|
||||
- [ ] `Comparable` mixin: `<=>`, `<`, `<=`, `>`, `>=`, `==`, `between?`, `clamp`
|
||||
- [ ] `Array`: indexing, slicing, `push`/`pop`/`shift`/`unshift`, `concat`, `flatten`, `compact`, `uniq`, `sort`, `reverse`, `zip`, `dig`, `pack`/`unpack` (deferred)
|
||||
- [ ] `Hash`: `[]`, `[]=`, `delete`, `merge`, `each_pair`, `keys`, `values`, `to_a`, `dig`, `fetch`, default values, default proc
|
||||
- [ ] `Range`: `each`, `step`, `cover?`, `include?`, `size`, `min`, `max`
|
||||
- [ ] `String`: indexing, slicing, `split`, `gsub` (string-arg version, regex deferred), `sub`, `upcase`, `downcase`, `strip`, `chomp`, `chars`, `bytes`, `to_i`, `to_f`, `to_sym`, `*`, `+`, `<<`, format with `%`
|
||||
- [ ] `Integer`: `times`, `upto`, `downto`, `step`, `digits`, `gcd`, `lcm`
|
||||
- [ ] Drive corpus to 200+ green
|
||||
|
||||
## SX primitive baseline
|
||||
|
||||
Use vectors for arrays; numeric tower + rationals for numbers; ADTs for tagged data;
|
||||
coroutines for fibers; string-buffer for mutable string building; bitwise ops for bit
|
||||
manipulation; multiple values for multi-return; promises for lazy evaluation; hash tables
|
||||
for mutable associative storage; sets for O(1) membership; sequence protocol for
|
||||
polymorphic iteration; gensym for unique symbols; char type for characters; string ports
|
||||
+ read/write for reader protocols; regexp for pattern matching; bytevectors for binary
|
||||
data; format for string templating.
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
- _(none yet)_
|
||||
|
||||
## Blockers
|
||||
|
||||
- _(none yet)_
|
||||
116
plans/smalltalk-on-sx.md
Normal file
116
plans/smalltalk-on-sx.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Smalltalk-on-SX: blocks with non-local return on delimited continuations
|
||||
|
||||
The headline showcase is **blocks** — Smalltalk's closures with non-local return (`^expr` aborts the enclosing *method*, not the block). Every other Smalltalk on top of a host VM (RSqueak on PyPy, GemStone on C, Maxine on Java) reinvents non-local return on whatever stack discipline the host gives them. On SX it's a one-liner: a block holds a captured continuation; `^` just invokes it. Message-passing OO falls out cheaply on top of the existing component / dispatch machinery.
|
||||
|
||||
End-state goal: ANSI-ish Smalltalk-80 subset, SUnit working, ~200 hand-written tests + a vendored slice of the Pharo kernel tests, classic corpus (eight queens, quicksort, mandelbrot, Conway's Life).
|
||||
|
||||
## Scope decisions (defaults — override by editing before we spawn)
|
||||
|
||||
- **Syntax:** Pharo / Squeak chunk format (`!` separators, `Object subclass: #Foo …`). No fileIn/fileOut images — text source only.
|
||||
- **Conformance:** ANSI X3J20 *as a target*, not bug-for-bug Squeak. "Reads like Smalltalk, runs like Smalltalk."
|
||||
- **Test corpus:** SUnit ported to SX-Smalltalk + custom programs + a curated slice of Pharo `Kernel-Tests` / `Collections-Tests`.
|
||||
- **Image:** out of scope. Source-only. No `become:` between sessions, no snapshotting.
|
||||
- **Reflection:** `class`, `respondsTo:`, `perform:`, `doesNotUnderstand:` in. `become:` (object-identity swap) **in** — it's a good CEK exercise. Method modification at runtime in.
|
||||
- **GUI / Morphic / threads:** out entirely.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/smalltalk/**` and `plans/smalltalk-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Smalltalk primitives go in `lib/smalltalk/runtime.sx`.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick roadmap boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
Smalltalk source
|
||||
│
|
||||
▼
|
||||
lib/smalltalk/tokenizer.sx — selectors, keywords, literals, $c, #sym, #(…), $'…'
|
||||
│
|
||||
▼
|
||||
lib/smalltalk/parser.sx — AST: classes, methods, blocks, cascades, sends
|
||||
│
|
||||
▼
|
||||
lib/smalltalk/transpile.sx — AST → SX AST (entry: smalltalk-eval-ast)
|
||||
│
|
||||
▼
|
||||
lib/smalltalk/runtime.sx — class table, MOP, dispatch, primitives
|
||||
```
|
||||
|
||||
Core mapping:
|
||||
- **Class** = SX dict `{:name :superclass :ivars :methods :class-methods :metaclass}`. Class table is a flat dict keyed by class name.
|
||||
- **Object** = SX dict `{:class :ivars}` — `ivars` keyed by symbol. Tagged ints / floats / strings / symbols are not boxed; their class is looked up by SX type.
|
||||
- **Method** = SX lambda closing over a `self` binding + temps. Body wrapped in a delimited continuation so `^` can escape.
|
||||
- **Message send** = `(st-send receiver selector args)` — does class-table lookup, walks superclass chain, falls back to `doesNotUnderstand:` with a `Message` object.
|
||||
- **Block** `[:x | … ^v … ]` = lambda + captured `^k` (the method-return continuation). Invoking `^` calls `k`; outer block invocation past method return raises `BlockContext>>cannotReturn:`.
|
||||
- **Cascade** `r m1; m2; m3` = `(let ((tmp r)) (st-send tmp 'm1 ()) (st-send tmp 'm2 ()) (st-send tmp 'm3 ()))`.
|
||||
- **`ifTrue:ifFalse:` / `whileTrue:`** = ordinary block sends; the runtime intrinsifies them in the JIT path so they compile to native branches (Tier 1 of bytecode expansion already covers this pattern).
|
||||
- **`become:`** = swap two object identities everywhere — in SX this is a heap walk, but we restrict to `oneWayBecome:` (cheap: rewrite class field) by default.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: identifiers, keywords (`foo:`), binary selectors (`+`, `==`, `,`, `->`, `~=` etc.), numbers (radix `16r1F`, scaled `1.5s2`), strings `'…''…'`, characters `$c`, symbols `#foo` `#'foo bar'` `#+`, byte arrays `#[1 2 3]`, literal arrays `#(1 #foo 'x')`, comments `"…"`
|
||||
- [ ] Parser: chunk format (`! !` separators), class definitions (`Object subclass: #X instanceVariableNames: '…' classVariableNames: '…' …`), method definitions (`extend: #Foo with: 'bar ^self'`), pragmas `<primitive: 1>`, blocks `[:a :b | | t1 t2 | …]`, cascades, message precedence (unary > binary > keyword)
|
||||
- [ ] Unit tests in `lib/smalltalk/tests/parse.sx`
|
||||
|
||||
### Phase 2 — object model + sequential eval
|
||||
- [ ] Class table + bootstrap: `Object`, `Behavior`, `Class`, `Metaclass`, `UndefinedObject`, `Boolean`/`True`/`False`, `Number`/`Integer`/`Float`, `String`, `Symbol`, `Array`, `Block`
|
||||
- [ ] `smalltalk-eval-ast`: literals, variable reference, assignment, message send, cascade, sequence, return
|
||||
- [ ] Method lookup: walk class → superclass; cache hit-class on `(class, selector)`
|
||||
- [ ] `doesNotUnderstand:` fallback constructing `Message` object
|
||||
- [ ] `super` send (lookup starts at superclass of *defining* class, not receiver class)
|
||||
- [ ] 30+ tests in `lib/smalltalk/tests/eval.sx`
|
||||
|
||||
### Phase 3 — blocks + non-local return (THE SHOWCASE)
|
||||
- [ ] Method invocation captures a `^k` (the return continuation) and binds it as the block's escape
|
||||
- [ ] `^expr` from inside a block invokes that captured `^k`
|
||||
- [ ] `BlockContext>>value`, `value:`, `value:value:`, …, `valueWithArguments:`
|
||||
- [ ] `whileTrue:` / `whileTrue` / `whileFalse:` / `whileFalse` as ordinary block sends — runtime intrinsifies the loop in the bytecode JIT
|
||||
- [ ] `ifTrue:` / `ifFalse:` / `ifTrue:ifFalse:` as block sends, similarly intrinsified
|
||||
- [ ] Escape past returned-from method raises `BlockContext>>cannotReturn:`
|
||||
- [ ] Classic programs in `lib/smalltalk/tests/programs/`:
|
||||
- [ ] `eight-queens.st`
|
||||
- [ ] `quicksort.st`
|
||||
- [ ] `mandelbrot.st`
|
||||
- [ ] `life.st` (Conway's Life, glider gun)
|
||||
- [ ] `fibonacci.st` (recursive + memoised)
|
||||
- [ ] `lib/smalltalk/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
|
||||
|
||||
### Phase 4 — reflection + MOP
|
||||
- [ ] `Object>>class`, `class>>name`, `class>>superclass`, `class>>methodDict`, `class>>selectors`
|
||||
- [ ] `Object>>perform:` / `perform:with:` / `perform:withArguments:`
|
||||
- [ ] `Object>>respondsTo:`, `Object>>isKindOf:`, `Object>>isMemberOf:`
|
||||
- [ ] `Behavior>>compile:` — runtime method addition
|
||||
- [ ] `Object>>becomeForward:` (one-way become; rewrites the class field of `aReceiver`)
|
||||
- [ ] Exceptions: `Exception`, `Error`, `signal`, `signal:`, `on:do:`, `ensure:`, `ifCurtailed:` — built on top of SX `handler-bind`/`raise`
|
||||
|
||||
### Phase 5 — collections + numeric tower
|
||||
- [ ] `SequenceableCollection`/`OrderedCollection`/`Array`/`String`/`Symbol`
|
||||
- [ ] `HashedCollection`/`Set`/`Dictionary`/`IdentityDictionary`
|
||||
- [ ] `Stream` hierarchy: `ReadStream`/`WriteStream`/`ReadWriteStream`
|
||||
- [ ] `Number` tower: `SmallInteger`/`LargePositiveInteger`/`Float`/`Fraction`
|
||||
- [ ] `String>>format:`, `printOn:` for everything
|
||||
|
||||
### Phase 6 — SUnit + corpus to 200+
|
||||
- [ ] Port SUnit (TestCase, TestSuite, TestResult) — written in SX-Smalltalk, runs in itself
|
||||
- [ ] Vendor a slice of Pharo `Kernel-Tests` and `Collections-Tests`
|
||||
- [ ] Drive the scoreboard up: aim for 200+ green tests
|
||||
- [ ] Stretch: ANSI Smalltalk validator subset
|
||||
|
||||
### Phase 7 — speed (optional)
|
||||
- [ ] Method-dictionary inline caching (already in CEK as a primitive; just wire selector cache)
|
||||
- [ ] Block intrinsification beyond `whileTrue:` / `ifTrue:`
|
||||
- [ ] Compare against GNU Smalltalk on the corpus
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first. Agent appends on every commit._
|
||||
|
||||
- _(none yet)_
|
||||
|
||||
## Blockers
|
||||
|
||||
_Shared-file issues that need someone else to fix. Minimal repro only._
|
||||
|
||||
- _(none yet)_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user