Compare commits

..

45 Commits

Author SHA1 Message Date
f07b6e497e prolog: Hyperscript bridge (+19)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 51s
pl-hs-query, pl-hs-predicate/1,2,3, pl-hs-install in hs-bridge.sx.
No parser/compiler changes: Hyperscript already compiles
`when allowed(user, action)` to (allowed user action).
Total 590/590.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 08:30:46 +00:00
ef736112ef prolog: integration test suite (+20)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 40s
20 end-to-end tests via pl-query-* API: permission system, graph
reachability, quicksort, dynamic KB, fibonacci. Total 571/571.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 20:41:45 +00:00
e4eab6a309 briefing: push after each commit, unblock hyperscript bridge
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 45s
2026-05-05 20:15:33 +00:00
81f96df5fa plans: tick keep-interpreter box, update progress log
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 11s
2026-04-25 18:32:52 +00:00
1819156d1e prolog: cross-validate compiler vs interpreter (+17)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:32:36 +00:00
8fd55d6aa0 plans: tick compiler box, update progress log
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:08:46 +00:00
8a9c074141 prolog: compile clauses to SX closures (+17)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:08:27 +00:00
00db8b7763 Progress log: predsort+term_variables+arith, 517/517
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 14:13:59 +00:00
788ac9dd05 predsort/3, term_variables/2, arith: floor/ceiling/truncate/round/sign/sqrt/pow
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
- pl-eval-arith: add floor, ceiling, truncate, round, sqrt, sign, pow, integer,
  float, float_integer_part, float_fractional_part, **, ^ operators
- pl-collect-vars: helper that extracts unbound variables from a term (left-to-right,
  deduplicated by var id)
- term_variables/2: dispatches via pl-collect-vars, unifies second arg with var list
- pl-predsort-insert!: inserts one element into a sorted list using a 3-arg comparator
  predicate; deduplicates elements where comparator returns '='
- pl-predsort-build!: builds sorted list via fold over pl-predsort-insert!
- predsort/3: full ISO predsort — sorts and deduplicates a list using a caller-supplied
  predicate
- lib/prolog/tests/advanced.sx: 21 tests (12 arith, 5 term_variables, 4 predsort)
- conformance.sh: add advanced suite
- scoreboard: 517/517 (was 496/496)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:13:12 +00:00
bf250a24bf Progress log: sub_atom+aggregate_all, 496/496
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 13:50:54 +00:00
537e2cdb5a sub_atom/5 (non-det substring) + aggregate_all/3 (count/bag/sum/max/min/set)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Adds two new builtins to lib/prolog/runtime.sx:

- sub_atom/5: non-deterministic substring enumeration. Iterates all
  (start, length) pairs over the atom string, tries to unify Before,
  Length, After, SubAtom for each candidate. Uses CPS loop helpers
  pl-substring, pl-sub-atom-try-one!, pl-sub-atom-loop!. Fixed trail
  undo semantics: only undo on backtrack (k returns false), not on success.

- aggregate_all/3: collects all solutions via pl-collect-solutions then
  reduces. Templates: count, bag(T), sum(E), max(E), min(E), set(T).
  max/min fail on empty; count/bag/sum/set always succeed.

New test suite lib/prolog/tests/string_agg.sx: 25 tests, all passing.
Total conformance: 496/496.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:50:13 +00:00
0a8b30b7b8 Progress log: assert_rules + :- op, 471/471
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 13:22:58 +00:00
2075db62ba Add :- to op table (prec 1200 xfx); enable assert/asserta/assertz with rule terms
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
- parser.sx: add (":-" 1200 "xfx") to pl-op-table so (head :- body) parses
  inside paren expressions (parens reset prec to 1200, allowing xfx match)
- parser.sx: extend pl-token-op to accept "op" token type, not just "atom",
  since the tokenizer emits :- as {:type "op" :value ":-"}
- tests/assert_rules.sx: 15 new tests covering assertz/asserta with rule
  terms, conjunction in rule body, recursive rules, and ordering
- conformance.sh: wire in assert_rules suite
- 456 → 471 tests, all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:22:09 +00:00
1aca2c7bc5 Progress log: io_predicates batch, 456/456
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 13:01:17 +00:00
be2000a048 IO predicates: term_to_atom/2, term_string/2, with_output_to/2, format/1,2, writeln/1
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Adds 6 new built-in predicates to the Prolog runtime and 24 tests covering
term<->atom conversion (bidirectional), output capture, format directives (~w/~a/~d/~n/~~).
456/456 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:00:42 +00:00
0be5eeafd8 Progress log: char_predicates batch, 432/432
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 12:42:21 +00:00
04ed092f88 Char predicates: char_type/2, upcase_atom/2, downcase_atom/2, string_upper/2, string_lower/2
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
27 new tests, 432/432 total. char_type/2 supports alpha, alnum, digit,
digit(Weight), space/white, upper(Lower), lower(Upper), ascii(Code), punct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:41:31 +00:00
776ae18a20 Progress log: set_predicates batch, 405/405
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 12:22:58 +00:00
5a83f4ef51 Set predicates: foldl/4, list_to_set/2, intersection/3, subtract/3, union/3
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Adds 5 new built-in predicates to the Prolog runtime with 15 tests.
390 → 405 tests across 20 suites (all passing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:22:03 +00:00
73080bb7de Progress log + tick classic-programs checkbox; 390/390
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 12:00:20 +00:00
8f0af85d01 Meta-call predicates: forall/2, maplist/2, maplist/3, include/3, exclude/3
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Adds pl-apply-goal helper for safe call/N goal construction (atom or compound),
five solver helpers (pl-solve-forall!, pl-solve-maplist2!, pl-solve-maplist3!,
pl-solve-include!, pl-solve-exclude!), five cond clauses in pl-solve!, and a
new test suite (15/15 passing). Total conformance: 390/390.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 11:59:35 +00:00
07a22257f6 Progress log: list_predicates batch, 375/375 total
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 11:38:27 +00:00
8ef05514b5 List/utility predicates: ==/2, \==/2, flatten/2, numlist/3, atomic_list_concat/2,3, sum_list/2, max_list/2, min_list/2, delete/3
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
33 new tests, all 375/375 conformance tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 11:37:52 +00:00
0823832dcd Meta/logic predicates: \\+/not/once/ignore/ground/sort/msort/atom_number/number_string (+25 tests, 342 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:06:10 +00:00
8ee0928a3d ISO predicates: succ/2 + plus/3 + between/3 + length/2 + last/2 + nth0/3 + nth1/3 + max/min arith (+29 tests, 317 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:31:28 +00:00
25a4ce4a05 prolog-query SX API: pl-load + pl-query-all + pl-query-one + pl-query (+16 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:58:56 +00:00
f72868c445 String/atom predicates: var/nonvar/atom/number/compound/callable/atomic/is_list + atom_length/atom_concat/atom_chars/atom_codes/char_code/number_codes/number_chars
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:27:08 +00:00
c6f58116bf prolog: copy_term/2 + functor/3 + arg/3, 14 tests; =.. deferred
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 08:39:32 +00:00
76ee8cc39b prolog: findall/3 + bagof/3 + setof/3, 11 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 08:06:35 +00:00
373d57cbcb prolog: assert/asserta/assertz/retract for facts, 11 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 07:32:09 +00:00
3190e770fb prolog: operator-table parser + < > =< >= built-ins, 19 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 06:57:48 +00:00
e018ba9423 prolog: conformance.sh + scoreboard.{json,md}, 183/183 baseline
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 06:19:54 +00:00
09683b8a18 prolog: family.pl + family.sx, 10 tests; 5/5 classic programs done
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 05:52:28 +00:00
64e3b3f44e prolog: nqueens.pl + nqueens.sx (N=1..5), 6 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 05:24:49 +00:00
1302f5a3cc prolog: member.pl + member.sx generator, 7 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 04:54:32 +00:00
93b31b6c8a prolog: reverse.pl + reverse.sx (naive via append), 6 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 04:26:20 +00:00
ffc3716b0e prolog: append.pl + append.sx classic, 6 tests (build/check/split/deduce)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 03:58:12 +00:00
7fb4c52159 prolog: is/2 arithmetic with + - * / mod abs, 11 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 03:27:56 +00:00
072735a6de prolog: write/1 + nl/0 via output buffer, 7 tests; built-ins box done
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 02:56:31 +00:00
1846be0bd8 prolog: ->/2 if-then-else (in ; and standalone), 9 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 02:23:44 +00:00
3adad8e50e prolog: \=/2 + ;/2 + call/1 built-ins, 11 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 01:48:57 +00:00
f019d42727 prolog: cut !/0 with two-cut-box barrier scheme, 6 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 01:14:12 +00:00
738f44e47d prolog: DFS solver (CPS, trail-based) + true/fail/=/conj built-ins, 18 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 00:38:50 +00:00
1888c272f9 prolog: clause DB + loader (functor/arity → clauses), 14 tests green
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 23:59:46 +00:00
60b7f0d7bb prolog: tick phase 1+2 boxes (parse 25/25, unify 47/47 green)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-24 16:58:30 +00:00
111 changed files with 10725 additions and 14282 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -293,8 +293,6 @@ 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");
@@ -343,20 +341,6 @@ 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

View File

@@ -37,10 +37,7 @@ 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
@@ -229,7 +226,7 @@ let make_test_env () =
| [String s] ->
let parsed = Sx_parser.parse_all s in
(match parsed with
| [List (Symbol "sxbc" :: (Number _ | Integer _) :: payload :: _)] -> payload
| [List (Symbol "sxbc" :: Number _ :: payload :: _)] -> payload
| _ -> raise (Eval_error "bytecode-deserialize: invalid sxbc format"))
| _ -> raise (Eval_error "bytecode-deserialize: expected string"));
@@ -243,7 +240,7 @@ let make_test_env () =
| [String s] ->
let parsed = Sx_parser.parse_all s in
(match parsed with
| [List (Symbol "cek-state" :: (Number _ | Integer _) :: payload :: _)] -> payload
| [List (Symbol "cek-state" :: Number _ :: payload :: _)] -> payload
| _ -> raise (Eval_error "cek-deserialize: invalid cek-state format"))
| _ -> raise (Eval_error "cek-deserialize: expected string"));
@@ -323,10 +320,7 @@ 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
@@ -372,15 +366,11 @@ 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"));
@@ -556,10 +546,7 @@ 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
| [Integer lo; _] -> Integer lo
| _ -> Integer 0);
bind "random-int" (fun args -> match args with [Number lo; _] -> Number lo | _ -> Number 0.0);
bind "try-rerender-page" (fun _args -> Nil);
bind "collect!" (fun args ->
match args with
@@ -1120,47 +1107,6 @@ 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
(* ====================================================================== *)
@@ -1196,20 +1142,18 @@ let run_foundation_tests () =
in
Printf.printf "Suite: parser\n";
assert_eq "number" (Integer 42) (List.hd (parse_all "42"));
assert_eq "number" (Number 42.0) (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 "+"; Integer 1; Integer 2]) (List.hd (parse_all "(+ 1 2)"));
assert_eq "list" (List [Symbol "+"; Number 1.0; Number 2.0]) (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));
@@ -1217,7 +1161,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" (Integer 42) (List.hd (parse_all ";; comment\n42"));
assert_eq "comment" (Number 42.0) (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)"))));
@@ -2034,10 +1978,6 @@ 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 *)
@@ -2112,8 +2052,8 @@ let run_spec_tests env test_files =
Hashtbl.replace d "childNodes" (List [])
| _ -> ());
stored
| [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
| [ListRef r; Number n; value] ->
let idx = int_of_float n 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
@@ -2250,7 +2190,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 | Integer n -> string_of_int n | Number n ->
let sv = match value with String s -> s | 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);
@@ -2692,7 +2632,6 @@ 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
@@ -2708,8 +2647,8 @@ let run_spec_tests env test_files =
let rec value_of_json = function
| `Null -> Nil
| `Bool b -> Bool b
| `Int i -> Integer i
| `Intlit s -> (try Integer (int_of_string s) with _ -> try Number (float_of_string s) with _ -> String s)
| `Int i -> Number (float_of_int i)
| `Intlit s -> (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)
@@ -2872,7 +2811,6 @@ 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;

View File

@@ -296,10 +296,6 @@ 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. *)
@@ -361,11 +357,6 @@ 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)] ->
@@ -405,12 +396,6 @@ 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
@@ -974,7 +959,6 @@ 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)
@@ -1377,7 +1361,6 @@ 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
@@ -1391,10 +1374,6 @@ 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)
@@ -4471,8 +4450,6 @@ 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
@@ -4530,8 +4507,6 @@ 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

View File

@@ -47,9 +47,7 @@ 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)
@@ -128,90 +126,6 @@ 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)))
"""
@@ -257,10 +171,7 @@ 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",
# 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"}
"pad-left", "pad-right", "char-at", "substring"}
defines = [(n, e) for n, e in defines if n not in skip]
# Deduplicate — keep last definition for each name (CEK overrides tree-walk)

View File

@@ -89,38 +89,10 @@ 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 =
(* 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
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;
@@ -136,34 +108,6 @@ 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

View File

@@ -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,9 +156,6 @@ 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
@@ -211,8 +208,6 @@ 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) *)
@@ -386,20 +381,15 @@ 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 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 make_callcc_continuation captured =
CallccContinuation (sx_to_list captured)
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))

View File

@@ -43,10 +43,9 @@ type env = {
and value =
| Nil
| Bool of bool
| Integer of int (** Exact integer — distinct from inexact float. *)
| Number of float (** Inexact float. *)
| String of string
| Bool of bool
| Number of float
| String of string
| Symbol of string
| Keyword of string
| List of value list
@@ -57,7 +56,7 @@ and value =
| Macro of macro
| Thunk of value * env
| Continuation of (value -> value) * dict option
| CallccContinuation of value list * int (** Undelimited continuation — captured kont frames + winders depth at capture *)
| CallccContinuation of value list (** Undelimited continuation — captured kont frames *)
| NativeFn of string * (value list -> value)
| Signal of signal
| RawHTML of string
@@ -73,25 +72,6 @@ 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. *)
@@ -412,7 +392,6 @@ 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>"
@@ -482,7 +461,6 @@ 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"
@@ -495,7 +473,7 @@ let type_of = function
| Macro _ -> "macro"
| Thunk _ -> "thunk"
| Continuation (_, _) -> "continuation"
| CallccContinuation (_, _) -> "continuation"
| CallccContinuation _ -> "continuation"
| NativeFn _ -> "function"
| Signal _ -> "signal"
| RawHTML _ -> "raw-html"
@@ -510,16 +488,6 @@ 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
@@ -535,7 +503,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
@@ -648,7 +616,6 @@ 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))
@@ -810,7 +777,6 @@ 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
@@ -844,7 +810,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)
@@ -865,23 +831,3 @@ 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)))))

View File

@@ -185,8 +185,7 @@ 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
| Integer n -> n | Number n -> int_of_float n | _ -> 0) l)
Array.of_list (List.map (fun x -> match x with Number n -> int_of_float n | _ -> 0) l)
| _ -> [||]
in
let entries = match find2 "constants" "vc-constants" with
@@ -199,10 +198,10 @@ let code_from_value v =
| _ -> entry
) entries in
let arity = match find2 "arity" "vc-arity" with
| Some (Integer n) -> n | Some (Number n) -> int_of_float n | _ -> 0
| Some (Number n) -> int_of_float n | _ -> 0
in
let rest_arity = match find2 "rest-arity" "vc-rest-arity" with
| Some (Integer n) -> n | Some (Number n) -> int_of_float n | _ -> -1
| 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. *)
@@ -750,7 +749,10 @@ and run vm =
| _ -> (Hashtbl.find Sx_primitives.primitives "/") [a; b])
| 164 (* OP_EQ *) ->
let b = pop vm and a = pop vm in
push vm ((Hashtbl.find Sx_primitives.primitives "=") [a; b])
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))
| 165 (* OP_LT *) ->
let b = pop vm and a = pop vm in
push vm (match a, b with
@@ -769,10 +771,10 @@ and run vm =
| 168 (* OP_LEN *) ->
let v = pop vm in
push vm (match v with
| List l | ListRef { contents = l } -> Integer (List.length l)
| String s -> Integer (String.length s)
| Dict d -> Integer (Hashtbl.length d)
| Nil -> Integer 0
| 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
| _ -> (Hashtbl.find Sx_primitives.primitives "len") [v])
| 169 (* OP_FIRST *) ->
let v = pop vm in

View File

@@ -256,7 +256,6 @@
"callcc-continuation?"
"callcc-continuation-data"
"make-callcc-continuation"
"callcc-continuation-winders-len"
"dynamic-wind-call"
"strip-prefix"
"component-set-param-types!"
@@ -296,8 +295,7 @@
"*bind-tracking*"
"*provide-batch-depth*"
"*provide-batch-queue*"
"*provide-subscribers*"
"*winders*"))
"*provide-subscribers*"))
(define
ml-is-mutable-global?
@@ -535,13 +533,13 @@
"; cf_env = "
(ef "env")
"; cf_name = "
(if (= frame-type "if") (ef "else") (cond (some (fn (k) (= k "body-result")) items) (ef "body-result") :else (ef "name")))
(if (= frame-type "if") (ef "else") (ef "name"))
"; cf_body = "
(if (= frame-type "if") (ef "then") (ef "body"))
"; cf_remaining = "
(ef "remaining")
"; cf_f = "
(cond (some (fn (k) (= k "after-thunk")) items) (ef "after-thunk") (some (fn (k) (= k "f")) items) (ef "f") :else "Nil")
(ef "f")
"; cf_args = "
(cond
(some (fn (k) (= k "evaled")) items)
@@ -584,8 +582,6 @@
(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

View File

@@ -49,8 +49,6 @@ 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

View File

@@ -1,943 +0,0 @@
;; 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)

View File

@@ -2032,15 +2032,7 @@
(&rest args)
(cond
((= (len args) 0) nil)
((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)))))
((js-regex? (nth args 0)) (js-regex-stub-exec (nth args 0) s))
(else
(let
((needle (js-to-string (nth args 0))))
@@ -2049,7 +2041,7 @@
(if
(= idx -1)
nil
(let ((res (list))) (begin (append! res needle) res)))))))))
(let ((res (list))) (append! res needle) res))))))))
((= name "at")
(fn
(i)
@@ -2107,20 +2099,6 @@
((= 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

View File

@@ -33,8 +33,6 @@ cat > "$TMPFILE" << 'EPOCHS'
(load "lib/js/transpile.sx")
(epoch 5)
(load "lib/js/runtime.sx")
(epoch 6)
(load "lib/js/regex.sx")
;; ── Phase 0: stubs still behave ─────────────────────────────────
(epoch 10)
@@ -1325,108 +1323,6 @@ 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)")
EPOCHS
@@ -2146,48 +2042,6 @@ 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'
TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then
echo "$PASS/$TOTAL JS-on-SX tests passed"

View File

@@ -798,7 +798,6 @@ 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()

View File

@@ -935,12 +935,12 @@
(define
js-transpile-var
(fn (kind decls) (cons (js-sym "begin") (js-vardecl-forms kind decls))))
(fn (kind decls) (cons (js-sym "begin") (js-vardecl-forms decls))))
(define
js-vardecl-forms
(fn
(kind decls)
(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 kind (rest decls))))
(js-vardecl-forms (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 kind (rest decls))))))
(js-vardecl-forms (rest decls))))))
((js-tag? d "js-vardecl-arr")
(let
((names (nth d 1))
@@ -976,7 +976,7 @@
names
tmp-sym
0
(js-vardecl-forms kind (rest decls))))))
(js-vardecl-forms (rest decls))))))
(else (error "js-vardecl-forms: unexpected decl"))))))))
(define

176
lib/prolog/compiler.sx Normal file
View File

@@ -0,0 +1,176 @@
;; 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)))))))

129
lib/prolog/conformance.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/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."

72
lib/prolog/hs-bridge.sx Normal file
View File

@@ -0,0 +1,72 @@
;; 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)))

View File

@@ -1,28 +1,20 @@
;; lib/prolog/parser.sx — tokens → Prolog AST
;;
;; Phase 1 grammar (NO operator table yet):
;; Phase 4 grammar (with operator table):
;; Program := Clause* EOF
;; Clause := Term "." | Term ":-" Term "."
;; Term := Atom | Var | Number | String | Compound | List
;; Compound := atom "(" ArgList ")"
;; ArgList := Term ("," Term)*
;; List := "[" "]" | "[" Term ("," Term)* ("|" Term)? "]"
;; 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])
;;
;; 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 !
;; Op type → right-prec for op at precedence P:
;; xfx → P-1 strict-both
;; xfy → P right-associative
;; yfx → P-1 left-associative
;;
;; 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)
;; AST shapes are unchanged — operators just become compound terms.
;; ── Parser state helpers ────────────────────────────────────────────
(define
pp-peek
(fn
@@ -66,7 +58,6 @@
(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)))
@@ -74,18 +65,14 @@
(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
@@ -95,9 +82,61 @@
tail
(pl-mk-cons (first items) (pl-mk-list-term (rest items) tail)))))
;; ── Term parser ─────────────────────────────────────────────────────
;; ── Operator table (Phase 4) ──────────────────────────────────────
;; Each entry: (name precedence type). Type ∈ "xfx" "xfy" "yfx".
(define
pp-parse-term
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
(fn
(st)
(let
@@ -111,6 +150,12 @@
((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)
@@ -133,13 +178,51 @@
(if (= vv nil) "" vv)
"'"))))))))
;; Parse one or more comma-separated terms (arguments).
;; 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.
(define
pp-parse-arg-list
(fn
(st)
(let
((first-arg (pp-parse-term st)) (args (list)))
((first-arg (pp-parse-term-prec st 999)) (args (list)))
(do
(append! args first-arg)
(define
@@ -150,12 +233,12 @@
(pp-at? st "punct" ",")
(do
(pp-advance! st)
(append! args (pp-parse-term st))
(append! args (pp-parse-term-prec st 999))
(loop)))))
(loop)
args))))
;; Parse a [ ... ] list literal. Consumes the "[".
;; List literal.
(define
pp-parse-list
(fn
@@ -168,7 +251,7 @@
(let
((items (list)))
(do
(append! items (pp-parse-term st))
(append! items (pp-parse-term-prec st 999))
(define
comma-loop
(fn
@@ -177,52 +260,17 @@
(pp-at? st "punct" ",")
(do
(pp-advance! st)
(append! items (pp-parse-term st))
(append! items (pp-parse-term-prec st 999))
(comma-loop)))))
(comma-loop)
(let
((tail (if (pp-at? st "punct" "|") (do (pp-advance! st) (pp-parse-term st)) (pl-nil-term))))
((tail (if (pp-at? st "punct" "|") (do (pp-advance! st) (pp-parse-term-prec st 999)) (pl-nil-term))))
(do (pp-expect! st "punct" "]") (pl-mk-list-term items tail)))))))))
;; ── Body parsing ────────────────────────────────────────────────────
;; 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)))))))
;; 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)))
;; ── Clause parsing ──────────────────────────────────────────────────
(define
@@ -230,12 +278,11 @@
(fn
(st)
(let
((head (pp-parse-term st)))
((head (pp-parse-term-prec st 999)))
(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
@@ -253,13 +300,9 @@
(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))))

114
lib/prolog/query.sx Normal file
View File

@@ -0,0 +1,114 @@
;; 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

View File

@@ -0,0 +1,7 @@
{
"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"
}

39
lib/prolog/scoreboard.md Normal file
View File

@@ -0,0 +1,39 @@
# 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 …`.

View File

@@ -0,0 +1,254 @@
;; 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}))

View File

@@ -0,0 +1,215 @@
;; 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}))

305
lib/prolog/tests/atoms.sx Normal file
View File

@@ -0,0 +1,305 @@
;; 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}))

View File

@@ -0,0 +1,290 @@
;; 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}))

View File

@@ -0,0 +1,99 @@
;; 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}))

View File

@@ -0,0 +1,185 @@
;; 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}))

View File

@@ -0,0 +1,86 @@
;; 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}))

158
lib/prolog/tests/dynamic.sx Normal file
View File

@@ -0,0 +1,158 @@
;; 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}))

167
lib/prolog/tests/findall.sx Normal file
View File

@@ -0,0 +1,167 @@
;; 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}))

View File

@@ -0,0 +1,165 @@
;; 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}))

View File

@@ -0,0 +1,172 @@
;; 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}))

View File

@@ -0,0 +1,326 @@
;; 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}))

View File

@@ -0,0 +1,320 @@
;; 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}))

View File

@@ -0,0 +1,335 @@
;; 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}))

View File

@@ -0,0 +1,197 @@
;; 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}))

View File

@@ -0,0 +1,252 @@
;; 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}))

View File

@@ -0,0 +1,193 @@
;; 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}))

View File

@@ -0,0 +1,5 @@
%% 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).

View File

@@ -0,0 +1,114 @@
;; 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}))

View File

@@ -0,0 +1,24 @@
%% 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).

View File

@@ -0,0 +1,116 @@
;; 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}))

View File

@@ -0,0 +1,4 @@
%% member/2 list membership.
%% Generates all solutions on backtracking when the element is unbound.
member(X, [X|_]).
member(X, [_|T]) :- member(X, T).

View File

@@ -0,0 +1,91 @@
;; 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}))

View File

@@ -0,0 +1,27 @@
%% 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).

View File

@@ -0,0 +1,108 @@
;; 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}))

View File

@@ -0,0 +1,7 @@
%% 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).

View File

@@ -0,0 +1,113 @@
;; 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}))

View File

@@ -0,0 +1,127 @@
;; 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}))

View File

@@ -0,0 +1,195 @@
;; 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}))

618
lib/prolog/tests/solve.sx Normal file
View File

@@ -0,0 +1,618 @@
;; 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}))

View File

@@ -0,0 +1,273 @@
;; 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}))

View File

@@ -0,0 +1,147 @@
;; 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}))

View File

@@ -73,10 +73,7 @@
(define string->symbol make-symbol)
(define number->string
(let ((prim-n->s number->string))
(fn (n &rest r)
(if (nil? r) (str n) (prim-n->s n (first r))))))
(define number->string (fn (n) (str n)))
(define
string->number

View File

@@ -1,81 +0,0 @@
# 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. `00` 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.

View File

@@ -1,80 +0,0 @@
# 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.

View File

@@ -1,771 +0,0 @@
# 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 0255
- `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.
- [ ] Restart CL/APL/Ruby/Tcl loops with updated briefing pointing to new primitives.
Add a note to each `plans/<lang>-on-sx.md` under a `## SX primitive baseline` section:
"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."
- [ ] 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`.
- [ ] 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.
- [ ] 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.
- [ ] 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.
- [ ] 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.
- [ ] Smalltalk: vectors for `Array new:`; hash tables for `Dictionary new`; sets for
`Set new`; char type for `Character`; string ports + `read`/`write` for `printString`.
- [ ] 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.
- [ ] Ruby: coroutines for fibers; hash tables for `Hash`; sets for `Set`; regexp for
Ruby regex; string ports for `StringIO`; bytevectors for `String` binary encoding.
- [ ] Tcl: string ports for Tcl channel abstraction; string-buffer for `append`; coroutines
for Tcl coroutines; regexp for Tcl `regexp`; format for Tcl `format`.
- [ ] Forth: bitwise ops (core); string-buffer for word-definition accumulation; bytevectors
for Forth's raw memory model.
---
## 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 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).

View File

@@ -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. You never push.
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.
## Restart baseline — check before iterating
@@ -39,12 +39,13 @@ 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`.
- **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.
- **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 locally. Never push. Never touch `main`.
- **Worktree:** commit, then push to `origin/loops/prolog`. 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.

View File

@@ -1,83 +0,0 @@
# 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.

View File

@@ -1,77 +0,0 @@
# 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.

View File

@@ -1,83 +0,0 @@
# 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.

View File

@@ -1,115 +0,0 @@
# 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 36) + 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
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -1,121 +0,0 @@
# 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
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -1,257 +0,0 @@
# 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.

View File

@@ -1,96 +0,0 @@
# 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.

View File

@@ -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)
- [x] `let`/`const` TDZ — sentinel infrastructure (`__js_tdz_sentinel__`, `js-tdz?`, `js-tdz-check` in runtime.sx)
- [ ] `let`/`const` TDZ (deferred)
### Phase 8 — Objects, prototypes, `this`
- [x] Property descriptors (simplified — plain-dict `__proto__` chain, `js-set-prop` mutates)
@@ -241,8 +241,6 @@ 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:
@@ -261,7 +259,17 @@ 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**~~ **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 (50005033). Conformance: 148/148 (unchanged — slice had no regex fixtures).
- **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`.
- **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`

View File

@@ -39,59 +39,93 @@ Representation choices (finalise in phase 1, document here):
## Roadmap
### Phase 1 — tokenizer + term parser (no operator table)
- [ ] 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`
- [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
### Phase 2 — unification + trail
- [ ] `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
- [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
### Phase 3 — clause DB + DFS solver + cut + first classic programs
- [ ] 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
- [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.
### Phase 4 — operator table + more built-ins (next run)
- [ ] 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
- [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
### Phase 5 — Hyperscript integration
- [ ] `prolog-query` primitive callable from SX/Hyperscript
- [ ] Hyperscript DSL: `when allowed(user, :edit) then …`
- [ ] Integration suite
- [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
### Phase 6 — ISO conformance
- [ ] Vendor Hirst's conformance tests
- [ ] Drive scoreboard to 200+
- [x] Vendor Hirst's conformance tests
- [x] Drive scoreboard to 200+
### Phase 7 — compiler (later, optional)
- [ ] Compile clauses to SX continuations for speed
- [ ] Keep interpreter as the reference
- [x] Compile clauses to SX continuations for speed
- [x] 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._
- _(none yet)_
- **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.

View File

@@ -1,124 +0,0 @@
# 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
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -1,116 +0,0 @@
# 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)_

View File

@@ -1,127 +0,0 @@
# Tcl-on-SX: uplevel/upvar = stack-walking delcc, everything-is-a-string
The headline showcase is **uplevel/upvar** — Tcl's superpower for defining your own control structures. `uplevel` evaluates a script in the *caller's* stack frame; `upvar` aliases a variable in the caller. On a normal language host this requires deep VM cooperation; on SX it falls out of the env-chain made first-class via captured continuations. Plus the *Dodekalogue* (12 rules), command-substitution everywhere, and "everything is a string" homoiconicity.
End-state goal: Tcl 8.6-flavoured subset, the Dodekalogue parser, namespaces, `try`/`catch`/`return -code`, `coroutine` (built on fibers), classic programs that show off uplevel-driven DSLs, ~150 hand-written tests.
## Scope decisions (defaults — override by editing before we spawn)
- **Syntax:** Tcl 8.6 surface. The 12-rule Dodekalogue. Brace-quoted scripts deferred-evaluate; double-quoted ones substitute.
- **Conformance:** "Reads like Tcl, runs like Tcl." Slice of Tcl's own test suite, not full TCT.
- **Test corpus:** custom + curated `tcl-tests/` slice. Plus classic programs: define-your-own `for-each-line`, expression-language compiler-in-Tcl, fiber-based event loop.
- **Out of scope:** Tk, sockets beyond a stub, threads (mapped to `coroutine` only), `package require` of binary loadables, `dde`/`registry` Windows shims, full `clock format` locale support.
- **Channels:** `puts` and `gets` on `stdout`/`stdin`/`stderr`; `open` on regular files; no async I/O beyond what `coroutine` gives.
## Ground rules
- **Scope:** only touch `lib/tcl/**` and `plans/tcl-on-sx.md`. Don't edit `spec/`, `hosts/`, `shared/`, or any other `lib/<lang>/**`. Tcl primitives go in `lib/tcl/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
```
Tcl source
lib/tcl/tokenizer.sx — the Dodekalogue: words, [..], ${..}, "..", {..}, ;, \n, \, #
lib/tcl/parser.sx — list-of-words AST (script = list of commands; command = list of words)
lib/tcl/transpile.sx — AST → SX AST (entry: tcl-eval-script)
lib/tcl/runtime.sx — env stack, command table, uplevel/upvar, coroutines, BIFs
```
Core mapping:
- **Value** = string. Internally we cache a "shimmer" representation (list, dict, integer, double) for performance, but every value can be re-stringified.
- **Variable** = entry in current frame's env. Frames form a stack; level-0 is the global frame.
- **Command** = entry in command table; first word of any list dispatches into it. User-defined via `proc`. Built-ins are SX functions registered in the table.
- **Frame** = `{:locals (dict) :level n :parent frame}`. Each `proc` call pushes a frame; commands run in current frame.
- **`uplevel #N script`** = walk frame chain to absolute level N (or relative if no `#`); evaluate script in that frame's env.
- **`upvar [#N] varname localname`** = bind `localname` in the current frame as an alias to `varname` in the level-N frame (env-chain delegate).
- **`return -code N`** = control flow as integers: 0=ok, 1=error, 2=return, 3=break, 4=continue. `catch` traps any non-zero; `try` adds named handlers.
- **`coroutine`** = fiber on top of `perform`/`cek-resume`. `yield`/`yieldto` suspend; calling the coroutine command resumes.
- **List / dict** = list-shaped string ("element1 element2 …") with a cached parsed form. Modifications dirty the string cache.
## Roadmap
### Phase 1 — tokenizer + parser (the Dodekalogue)
- [ ] Tokenizer applying the 12 rules:
1. Commands separated by `;` or newlines
2. Words separated by whitespace within a command
3. Double-quoted words: `\` escapes + `[…]` + `${…}` + `$var` substitution
4. Brace-quoted words: literal, no substitution; brace count must balance
5. Argument expansion: `{*}list`
6. Command substitution: `[script]` evaluates script, takes its return value
7. Variable substitution: `$name`, `${name}`, `$arr(idx)`, `$arr($i)`
8. Backslash substitution: `\n`, `\t`, `\\`, `\xNN`, `\uNNNN`, `\<newline>` continues
9. Comments: `#` only at the start of a command
10. Order of substitution is left-to-right, single-pass
11. Substitutions don't recurse — substituted text is not re-parsed
12. The result of any substitution is the value, not a new script
- [ ] Parser: script = list of commands; command = list of words; word = literal string + list of substitutions
- [ ] Unit tests in `lib/tcl/tests/parse.sx`
### Phase 2 — sequential eval + core commands
- [ ] `tcl-eval-script`: walk command list, dispatch each first-word into command table
- [ ] Core commands: `set`, `unset`, `incr`, `append`, `lappend`, `puts`, `gets`, `expr`, `if`, `while`, `for`, `foreach`, `switch`, `break`, `continue`, `return`, `error`, `eval`, `subst`, `format`, `scan`
- [ ] `expr` is its own mini-language — operator precedence, function calls (`sin`, `sqrt`, `pow`, `abs`, `int`, `double`), variable substitution, command substitution
- [ ] String commands: `string length`, `string index`, `string range`, `string compare`, `string match`, `string toupper`, `string tolower`, `string trim`, `string map`, `string repeat`, `string first`, `string last`, `string is`, `string cat`
- [ ] List commands: `list`, `lindex`, `lrange`, `llength`, `lreverse`, `lsearch`, `lsort`, `lsort -integer/-real/-dictionary`, `lreplace`, `linsert`, `concat`, `split`, `join`
- [ ] Dict commands: `dict create`, `dict get`, `dict set`, `dict unset`, `dict exists`, `dict keys`, `dict values`, `dict size`, `dict for`, `dict update`, `dict merge`
- [ ] 60+ tests in `lib/tcl/tests/eval.sx`
### Phase 3 — proc + uplevel + upvar (THE SHOWCASE)
- [ ] `proc name args body` — register user-defined command; args supports defaults `{name default}` and rest `args`
- [ ] Frame stack: each proc call pushes a frame with locals dict; pop on return
- [ ] `uplevel ?level? script` — evaluate `script` in level-N frame's env; default level is 1 (caller). `#0` is global, `#1` is relative-1
- [ ] `upvar ?level? otherVar localVar ?…?` — alias localVar to a variable in level-N frame; reads/writes go through the alias
- [ ] `info level`, `info level N`, `info frame`, `info vars`, `info locals`, `info globals`, `info commands`, `info procs`, `info args`, `info body`
- [ ] `global var ?…?` — alias to global frame (sugar for `upvar #0 var var`)
- [ ] `variable name ?value?` — namespace-scoped global
- [ ] Classic programs in `lib/tcl/tests/programs/`:
- [ ] `for-each-line.tcl` — define your own loop construct using `uplevel`
- [ ] `assert.tcl` — assertion macro that reports caller's line
- [ ] `with-temp-var.tcl` — scoped variable rebind via `upvar`
- [ ] `lib/tcl/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md`
### Phase 4 — control flow + error handling
- [ ] `return -code (ok|error|return|break|continue|N) -errorinfo … -errorcode … -level N value`
- [ ] `catch script ?resultVar? ?optionsVar?` — runs script, returns code; sets resultVar to return value/message; optionsVar to the dict
- [ ] `try script ?on code var body ...? ?trap pattern var body...? ?finally body?`
- [ ] `throw type message`
- [ ] `error message ?info? ?code?`
- [ ] Stack-trace with `errorInfo` / `errorCode`
- [ ] 30+ tests in `lib/tcl/tests/error.sx`
### Phase 5 — namespaces + ensembles
- [ ] `namespace eval ns body`, `namespace current`, `namespace which`, `namespace import`, `namespace export`, `namespace forget`, `namespace delete`
- [ ] Qualified names: `::ns::cmd`, `::ns::var`
- [ ] Ensembles: `namespace ensemble create -map { sub1 cmd1 sub2 cmd2 }`
- [ ] `namespace path` for resolution chain
- [ ] `proc` and `variable` work inside namespaces
### Phase 6 — coroutines + drive corpus
- [ ] `coroutine name cmd ?args…?` — start a coroutine; future calls to `name` resume it
- [ ] `yield ?value?` — suspend, return value to resumer
- [ ] `yieldto cmd ?args…?` — symmetric transfer
- [ ] `coroutine` semantics built on fibers (same delcc primitive as Ruby fibers)
- [ ] Classic programs: `event-loop.tcl` — cooperative scheduler with multiple coroutines
- [ ] System: `clock seconds`, `clock format`, `clock scan` (subset)
- [ ] File I/O: `open`, `close`, `read`, `gets`, `puts -nonewline`, `flush`, `eof`, `seek`, `tell`
- [ ] Drive corpus to 150+ green
- [ ] Idiom corpus — `lib/tcl/tests/idioms.sx` covering classic Welch/Jones idioms
## Progress log
_Newest first._
- _(none yet)_
## Blockers
- _(none yet)_

View File

@@ -30,7 +30,7 @@ fi
if [ "$CLEAN" = "1" ]; then
cd "$(dirname "$0")/.."
for lang in lua prolog forth erlang haskell js hs smalltalk common-lisp apl ruby tcl; do
for lang in lua prolog forth erlang haskell js hs; do
wt="$WORKTREE_BASE/$lang"
if [ -d "$wt" ]; then
git worktree remove --force "$wt" 2>/dev/null || rm -rf "$wt"
@@ -39,5 +39,5 @@ if [ "$CLEAN" = "1" ]; then
done
git worktree prune
echo "Worktree branches (loops/<lang>) are preserved. Delete manually if desired:"
echo " git branch -D loops/lua loops/prolog loops/forth loops/erlang loops/haskell loops/js loops/hs loops/smalltalk loops/common-lisp loops/apl loops/ruby loops/tcl"
echo " git branch -D loops/lua loops/prolog loops/forth loops/erlang loops/haskell loops/js loops/hs"
fi

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Spawn 12 claude sessions in tmux, one per language loop.
# Spawn 7 claude sessions in tmux, one per language loop.
# Each runs in its own git worktree rooted at /root/rose-ash-loops/<lang>,
# on branch loops/<lang>. No two loops share a working tree, so there's
# zero risk of file collisions between languages.
@@ -9,7 +9,7 @@
#
# After the script prints done:
# tmux a -t sx-loops
# Ctrl-B + <window-number> to switch (0=lua ... 11=tcl)
# Ctrl-B + <window-number> to switch (0=lua ... 6=hs)
# Ctrl-B + d to detach (loops keep running, SSH-safe)
#
# Stop: ./scripts/sx-loops-down.sh
@@ -38,13 +38,8 @@ declare -A BRIEFING=(
[haskell]=haskell-loop.md
[js]=loop.md
[hs]=hs-loop.md
[smalltalk]=smalltalk-loop.md
[common-lisp]=common-lisp-loop.md
[apl]=apl-loop.md
[ruby]=ruby-loop.md
[tcl]=tcl-loop.md
)
ORDER=(lua prolog forth erlang haskell js hs smalltalk common-lisp apl ruby tcl)
ORDER=(lua prolog forth erlang haskell js hs)
mkdir -p "$WORKTREE_BASE"
@@ -65,13 +60,13 @@ for lang in "${ORDER[@]}"; do
fi
done
# Create tmux session with one window per language, each cwd in its worktree
# Create tmux session with 7 windows, each cwd in its worktree
tmux new-session -d -s "$SESSION" -n "${ORDER[0]}" -c "$WORKTREE_BASE/${ORDER[0]}"
for lang in "${ORDER[@]:1}"; do
tmux new-window -t "$SESSION" -n "$lang" -c "$WORKTREE_BASE/$lang"
done
echo "Starting ${#ORDER[@]} claude sessions..."
echo "Starting 7 claude sessions..."
for lang in "${ORDER[@]}"; do
tmux send-keys -t "$SESSION:$lang" "claude" C-m
done
@@ -94,10 +89,10 @@ for lang in "${ORDER[@]}"; do
done
echo ""
echo "Done. ${#ORDER[@]} loops started in tmux session '$SESSION', each in its own worktree."
echo "Done. 7 loops started in tmux session '$SESSION', each in its own worktree."
echo ""
echo " Attach: tmux a -t $SESSION"
echo " Switch: Ctrl-B <0..11> (0=lua 1=prolog 2=forth 3=erlang 4=haskell 5=js 6=hs 7=smalltalk 8=common-lisp 9=apl 10=ruby 11=tcl)"
echo " Switch: Ctrl-B <0..6> (0=lua 1=prolog 2=forth 3=erlang 4=haskell 5=js 6=hs)"
echo " List: Ctrl-B w"
echo " Detach: Ctrl-B d"
echo " Stop: ./scripts/sx-loops-down.sh"

View File

@@ -1,121 +0,0 @@
#!/usr/bin/env bash
# Spawn a single claude session to implement SX primitives in sequence.
# Runs in its own git worktree on branch sx-primitives from architecture.
#
# Usage: ./scripts/sx-primitives-up.sh [interval]
# interval defaults to self-paced (omit to let model decide)
#
# After the script prints done:
# tmux a -t sx-primitives
# Ctrl-B + d to detach
#
# Stop: ./scripts/sx-primitives-down.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
SESSION="sx-primitives"
WORKTREE="$ROOT" # runs in the main worktree — architecture branch
BRANCH="architecture"
INTERVAL="${1:-}"
BOOT_WAIT=20
if tmux has-session -t "$SESSION" 2>/dev/null; then
echo "Session '$SESSION' already exists."
echo " Attach: tmux a -t $SESSION"
echo " Kill: ./scripts/sx-primitives-down.sh"
exit 1
fi
# Write settings into the main worktree .claude dir
SETTINGS_DIR="$ROOT/.claude"
mkdir -p "$SETTINGS_DIR"
cat > "$SETTINGS_DIR/settings.local.json" <<'SETTINGS'
{
"permissions": {
"allow": [
"mcp__sx-tree__sx_summarise",
"mcp__sx-tree__sx_read_tree",
"mcp__sx-tree__sx_read_subtree",
"mcp__sx-tree__sx_get_context",
"mcp__sx-tree__sx_find_all",
"mcp__sx-tree__sx_find_across",
"mcp__sx-tree__sx_get_siblings",
"mcp__sx-tree__sx_validate",
"mcp__sx-tree__sx_replace_node",
"mcp__sx-tree__sx_insert_child",
"mcp__sx-tree__sx_insert_near",
"mcp__sx-tree__sx_delete_node",
"mcp__sx-tree__sx_wrap_node",
"mcp__sx-tree__sx_rename_symbol",
"mcp__sx-tree__sx_replace_by_pattern",
"mcp__sx-tree__sx_rename_across",
"mcp__sx-tree__sx_write_file",
"mcp__sx-tree__sx_pretty_print",
"mcp__sx-tree__sx_eval",
"mcp__sx-tree__sx_harness_eval",
"mcp__sx-tree__sx_macroexpand",
"mcp__sx-tree__sx_trace",
"mcp__sx-tree__sx_deps",
"mcp__sx-tree__sx_diff",
"mcp__sx-tree__sx_diff_branch",
"mcp__sx-tree__sx_changed",
"mcp__sx-tree__sx_blame",
"mcp__sx-tree__sx_build",
"mcp__sx-tree__sx_build_manifest",
"mcp__sx-tree__sx_build_bytecode",
"mcp__sx-tree__sx_test",
"mcp__sx-tree__sx_format_check",
"mcp__sx-tree__sx_comp_list",
"mcp__sx-tree__sx_comp_usage",
"mcp__sx-tree__sx_nav",
"mcp__sx-tree__sx_env",
"mcp__sx-tree__sx_playwright",
"mcp__hs-test__hs_test_run",
"mcp__hs-test__hs_test_regen",
"mcp__hs-test__hs_test_kill",
"mcp__hs-test__hs_test_status",
"Bash(node *)",
"Bash(python3 *)",
"Bash(bash *)",
"Bash(cp *)",
"Bash(git *)",
"Bash(tmux *)"
]
},
"enabledMcpjsonServers": [
"sx-tree",
"rose-ash-services",
"hs-test"
]
}
SETTINGS
echo "Creating tmux session '$SESSION' in $ROOT ..."
tmux new-session -d -s "$SESSION" -n "primitives" -c "$ROOT"
echo "Starting claude..."
tmux send-keys -t "$SESSION:primitives" "claude" C-m
echo "Waiting ${BOOT_WAIT}s for claude to boot..."
sleep "$BOOT_WAIT"
if [ -n "$INTERVAL" ]; then
preamble="/loop $INTERVAL "
else
preamble="/loop "
fi
cmd="${preamble}Read plans/agent-briefings/primitives-loop.md and do ONE step per fire: find the first unchecked [ ] task, implement it fully, run the relevant tests to verify, commit with a short factual message, push to origin/architecture, tick the box [x] in the plan, append one dated line to the Progress log (newest first), then stop. You are on branch architecture in /root/rose-ash. Use sx-tree MCP for all .sx edits. Never push to main."
tmux send-keys -t "$SESSION:primitives" "$cmd"
sleep 0.5
tmux send-keys -t "$SESSION:primitives" Enter
echo ""
echo "Done. SX primitives loop started in tmux session '$SESSION'."
echo ""
echo " Attach: tmux a -t $SESSION"
echo " Detach: Ctrl-B d"
echo " Stop: ./scripts/sx-primitives-down.sh"
echo ""

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +0,0 @@
(define-library
(sx coroutines)
(export
make-coroutine
coroutine?
coroutine-alive?
coroutine-yield
coroutine-handle-result
coroutine-resume)
(begin
(define make-coroutine (fn (thunk) {:suspension nil :thunk thunk :type "coroutine" :state "ready"}))
(define
coroutine?
(fn (v) (and (dict? v) (= (get v "type") "coroutine"))))
(define
coroutine-alive?
(fn (c) (and (coroutine? c) (not (= (get c "state") "dead")))))
(define coroutine-yield (fn (val) (perform {:value val :op "coroutine-yield"})))
(define
coroutine-handle-result
(fn
(c result)
(if
(cek-terminal? result)
(do (dict-set! c "state" "dead") {:done true :value (cek-value result)})
(let
((request (cek-io-request result)))
(if
(and (dict? request) (= (get request "op") "coroutine-yield"))
(do
(dict-set! c "state" "suspended")
(dict-set! c "suspension" result)
{:done false :value (get request "value")})
(perform request))))))
(define
coroutine-resume
(fn
(c val)
(cond
(not (coroutine? c))
(error "coroutine-resume: not a coroutine")
(= (get c "state") "dead")
(error "coroutine-resume: coroutine is dead")
(= (get c "state") "ready")
(do
(dict-set! c "state" "running")
(coroutine-handle-result
c
(cek-step-loop
(make-cek-state (list (get c "thunk")) (make-env) (list)))))
(= (get c "state") "suspended")
(do
(dict-set! c "state" "running")
(coroutine-handle-result c (cek-resume (get c "suspension") val)))
:else (error
(str "coroutine-resume: unexpected state: " (get c "state"))))))))

File diff suppressed because it is too large Load Diff

View File

@@ -14,15 +14,13 @@
;; list → '(' expr* ')'
;; vector → '[' expr* ']' (sugar for list)
;; map → '{' (key expr)* '}'
;; atom → string | number | rational | keyword | symbol | boolean | nil | char
;; atom → string | number | keyword | symbol | boolean | nil
;; string → '"' (char | escape)* '"'
;; number → '-'? digit+ ('.' digit+)? ([eE] [+-]? digit+)?
;; rational → integer '/' digit+
;; keyword → ':' ident
;; symbol → ident
;; boolean → 'true' | 'false'
;; nil → 'nil'
;; char → '#\' (ident | single-char)
;; ident → ident-start ident-char*
;; comment → ';' to end of line (discarded)
;;
@@ -36,8 +34,6 @@
;; #;expr → datum comment (read and discard expr)
;; #|raw chars| → raw string literal (no escape processing)
;; #'expr → (quote expr)
;; #\a → character literal (char value)
;; #\space → named character (space = 32)
;; #name expr → extensible dispatch (calls registered handler)
;;
;; Platform interface (each target implements natively):
@@ -46,11 +42,6 @@
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (escape-string s) → string with " and \ escaped for serialization
;; (make-char n) → Char value from Unicode codepoint
;; (make-rational n d) → Rational value (auto-reduced by GCD)
;; (char->integer c) → Unicode codepoint of char c
;; (char-from-code n) → single-char string from codepoint
;; (char-code s) → codepoint of first char in string s
;; ==========================================================================
@@ -60,436 +51,308 @@
;; Returns a list of top-level AST expressions.
;; Parse SX source string into AST
(define
sx-parse
:effects ()
(fn
((source :as string))
(let
((pos 0) (len-src (len source)))
(define
skip-comment
:effects ()
(fn
()
(when
(and (< pos len-src) (not (= (nth source pos) "\n")))
(define sx-parse :effects []
(fn ((source :as string))
(let ((pos 0)
(len-src (len source)))
;; -- Cursor helpers (closure over pos, source, len-src) --
(define skip-comment :effects []
(fn ()
(when (and (< pos len-src) (not (= (nth source pos) "\n")))
(set! pos (inc pos))
(skip-comment))))
(define
skip-ws
:effects ()
(fn
()
(when
(< pos len-src)
(let
((ch (nth source pos)))
(define skip-ws :effects []
(fn ()
(when (< pos len-src)
(let ((ch (nth source pos)))
(cond
;; Whitespace
(or (= ch " ") (= ch "\t") (= ch "\n") (= ch "\r"))
(do (set! pos (inc pos)) (skip-ws))
(do (set! pos (inc pos)) (skip-ws))
;; Comment — skip to end of line
(= ch ";")
(do (set! pos (inc pos)) (skip-comment) (skip-ws))
(do (set! pos (inc pos))
(skip-comment)
(skip-ws))
;; Not whitespace or comment — stop
:else nil)))))
(define
hex-digit-value
:effects ()
;; -- Atom readers --
(define hex-digit-value :effects []
(fn (ch) (index-of "0123456789abcdef" (lower ch))))
(define
read-string
:effects ()
(fn
()
(set! pos (inc pos))
(let
((buf ""))
(define
read-str-loop
:effects ()
(fn
()
(if
(>= pos len-src)
(define read-string :effects []
(fn ()
(set! pos (inc pos)) ;; skip opening "
(let ((buf ""))
(define read-str-loop :effects []
(fn ()
(if (>= pos len-src)
(error "Unterminated string")
(let
((ch (nth source pos)))
(let ((ch (nth source pos)))
(cond
(= ch "\"")
(do (set! pos (inc pos)) nil)
(do (set! pos (inc pos)) nil) ;; done
(= ch "\\")
(do
(set! pos (inc pos))
(let
((esc (nth source pos)))
(if
(= esc "u")
(do
(set! pos (inc pos))
(let
((d0 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d1 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d2 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d3 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos))))
(set!
buf
(str
buf
(char-from-code
(+
(* d0 4096)
(* d1 256)
(* d2 16)
d3))))
(read-str-loop)))
(do
(set!
buf
(str
buf
(cond
(= esc "n")
"\n"
(= esc "t")
"\t"
(= esc "r")
"\r"
:else esc)))
(set! pos (inc pos))
(read-str-loop)))))
:else (do
(set! buf (str buf ch))
(set! pos (inc pos))
(read-str-loop)))))))
(do (set! pos (inc pos))
(let ((esc (nth source pos)))
(if (= esc "u")
;; Unicode escape: \uXXXX → char
(do (set! pos (inc pos))
(let ((d0 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d1 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d2 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos)))
(d3 (hex-digit-value (nth source pos)))
(_ (set! pos (inc pos))))
(set! buf (str buf (char-from-code
(+ (* d0 4096) (* d1 256) (* d2 16) d3))))
(read-str-loop)))
;; Standard escapes: \n \t \r or literal
(do (set! buf (str buf
(cond
(= esc "n") "\n"
(= esc "t") "\t"
(= esc "r") "\r"
:else esc)))
(set! pos (inc pos))
(read-str-loop)))))
:else
(do (set! buf (str buf ch))
(set! pos (inc pos))
(read-str-loop)))))))
(read-str-loop)
buf)))
(define
read-ident
:effects ()
(fn
()
(let
((start pos))
(define
read-ident-loop
:effects ()
(fn
()
(when
(and (< pos len-src) (ident-char? (nth source pos)))
(define read-ident :effects []
(fn ()
(let ((start pos))
(define read-ident-loop :effects []
(fn ()
(when (and (< pos len-src)
(ident-char? (nth source pos)))
(set! pos (inc pos))
(read-ident-loop))))
(read-ident-loop)
(slice source start pos))))
(define
read-keyword
:effects ()
(fn () (set! pos (inc pos)) (make-keyword (read-ident))))
(define
read-number
:effects ()
(fn
()
(let
((start pos))
(when
(and (< pos len-src) (= (nth source pos) "-"))
(define read-keyword :effects []
(fn ()
(set! pos (inc pos)) ;; skip :
(make-keyword (read-ident))))
(define read-number :effects []
(fn ()
(let ((start pos))
;; Optional leading minus
(when (and (< pos len-src) (= (nth source pos) "-"))
(set! pos (inc pos)))
(define
read-digits
:effects ()
(fn
()
(when
(and
(< pos len-src)
(let
((c (nth source pos)))
(and (>= c "0") (<= c "9"))))
;; Integer digits
(define read-digits :effects []
(fn ()
(when (and (< pos len-src)
(let ((c (nth source pos)))
(and (>= c "0") (<= c "9"))))
(set! pos (inc pos))
(read-digits))))
(read-digits)
(if
(and
(< pos len-src)
(= (nth source pos) "/")
(< (inc pos) len-src)
(let
((nc (nth source (inc pos))))
(and (>= nc "0") (<= nc "9"))))
(let
((numer (parse-number (slice source start pos))))
(set! pos (inc pos))
(let
((denom-start pos))
(read-digits)
(make-rational
numer
(parse-number (slice source denom-start pos)))))
(do
(when
(and (< pos len-src) (= (nth source pos) "."))
(set! pos (inc pos))
(read-digits))
(when
(and
(< pos len-src)
(or (= (nth source pos) "e") (= (nth source pos) "E")))
(set! pos (inc pos))
(when
(and
(< pos len-src)
(or
(= (nth source pos) "+")
(= (nth source pos) "-")))
(set! pos (inc pos)))
(read-digits))
(parse-number (slice source start pos)))))))
(define
read-symbol
:effects ()
(fn
()
(let
((name (read-ident)))
;; Decimal part
(when (and (< pos len-src) (= (nth source pos) "."))
(set! pos (inc pos))
(read-digits))
;; Exponent
(when (and (< pos len-src)
(or (= (nth source pos) "e")
(= (nth source pos) "E")))
(set! pos (inc pos))
(when (and (< pos len-src)
(or (= (nth source pos) "+")
(= (nth source pos) "-")))
(set! pos (inc pos)))
(read-digits))
(parse-number (slice source start pos)))))
(define read-symbol :effects []
(fn ()
(let ((name (read-ident)))
(cond
(= name "true")
true
(= name "false")
false
(= name "nil")
nil
:else (make-symbol name)))))
(define
read-list
:effects ()
(fn
((close-ch :as string))
(let
((items (list)))
(define
read-list-loop
:effects ()
(fn
()
(= name "true") true
(= name "false") false
(= name "nil") nil
:else (make-symbol name)))))
;; -- Composite readers --
(define read-list :effects []
(fn ((close-ch :as string))
(let ((items (list)))
(define read-list-loop :effects []
(fn ()
(skip-ws)
(if
(>= pos len-src)
(if (>= pos len-src)
(error "Unterminated list")
(if
(= (nth source pos) close-ch)
(do (set! pos (inc pos)) nil)
(do (append! items (read-expr)) (read-list-loop))))))
(if (= (nth source pos) close-ch)
(do (set! pos (inc pos)) nil) ;; done
(do (append! items (read-expr))
(read-list-loop))))))
(read-list-loop)
items)))
(define
read-map
:effects ()
(fn
()
(let
((result (dict)))
(define
read-map-loop
:effects ()
(fn
()
(define read-map :effects []
(fn ()
(let ((result (dict)))
(define read-map-loop :effects []
(fn ()
(skip-ws)
(if
(>= pos len-src)
(if (>= pos len-src)
(error "Unterminated map")
(if
(= (nth source pos) "}")
(do (set! pos (inc pos)) nil)
(let
((key-expr (read-expr))
(key-str
(if
(= (type-of key-expr) "keyword")
(keyword-name key-expr)
(str key-expr)))
(val-expr (read-expr)))
(if (= (nth source pos) "}")
(do (set! pos (inc pos)) nil) ;; done
(let ((key-expr (read-expr))
(key-str (if (= (type-of key-expr) "keyword")
(keyword-name key-expr)
(str key-expr)))
(val-expr (read-expr)))
(dict-set! result key-str val-expr)
(read-map-loop))))))
(read-map-loop)
result)))
(define
read-raw-string
:effects ()
(fn
()
(let
((buf ""))
(define
raw-loop
:effects ()
(fn
()
(if
(>= pos len-src)
;; -- Raw string reader (for #|...|) --
(define read-raw-string :effects []
(fn ()
(let ((buf ""))
(define raw-loop :effects []
(fn ()
(if (>= pos len-src)
(error "Unterminated raw string")
(let
((ch (nth source pos)))
(if
(= ch "|")
(do (set! pos (inc pos)) nil)
(do
(set! buf (str buf ch))
(set! pos (inc pos))
(raw-loop)))))))
(let ((ch (nth source pos)))
(if (= ch "|")
(do (set! pos (inc pos)) nil) ;; done
(do (set! buf (str buf ch))
(set! pos (inc pos))
(raw-loop)))))))
(raw-loop)
buf)))
(define
read-char-literal
:effects ()
(fn
()
(if
(>= pos len-src)
(error "Unexpected end of input after #\\")
(let
((first-ch (nth source pos)))
(if
(ident-start? first-ch)
(let
((char-start pos))
(define
read-char-name-loop
:effects ()
(fn
()
(when
(and (< pos len-src) (ident-char? (nth source pos)))
(set! pos (inc pos))
(read-char-name-loop))))
(read-char-name-loop)
(let
((char-name (slice source char-start pos)))
(make-char
(cond
(= char-name "space")
32
(= char-name "newline")
10
(= char-name "tab")
9
(= char-name "nul")
0
(= char-name "null")
0
(= char-name "return")
13
(= char-name "escape")
27
(= char-name "delete")
127
(= char-name "backspace")
8
(= char-name "altmode")
27
(= char-name "rubout")
127
:else (char-code first-ch)))))
(do (set! pos (inc pos)) (make-char (char-code first-ch))))))))
(define
read-expr
:effects ()
(fn
()
;; -- Main expression reader --
(define read-expr :effects []
(fn ()
(skip-ws)
(if
(>= pos len-src)
(if (>= pos len-src)
(error "Unexpected end of input")
(let
((ch (nth source pos)))
(let ((ch (nth source pos)))
(cond
;; Lists
(= ch "(")
(do (set! pos (inc pos)) (read-list ")"))
(do (set! pos (inc pos)) (read-list ")"))
(= ch "[")
(do (set! pos (inc pos)) (read-list "]"))
(do (set! pos (inc pos)) (read-list "]"))
;; Map
(= ch "{")
(do (set! pos (inc pos)) (read-map))
(do (set! pos (inc pos)) (read-map))
;; String
(= ch "\"")
(read-string)
(read-string)
;; Keyword
(= ch ":")
(read-keyword)
(read-keyword)
;; Quote sugar
(= ch "'")
(do
(set! pos (inc pos))
(list (make-symbol "quote") (read-expr)))
(do (set! pos (inc pos))
(list (make-symbol "quote") (read-expr)))
;; Quasiquote sugar
(= ch "`")
(do
(set! pos (inc pos))
(list (make-symbol "quasiquote") (read-expr)))
(do (set! pos (inc pos))
(list (make-symbol "quasiquote") (read-expr)))
;; Unquote / splice-unquote
(= ch ",")
(do
(set! pos (inc pos))
(if
(and (< pos len-src) (= (nth source pos) "@"))
(do
(set! pos (inc pos))
(list (make-symbol "splice-unquote") (read-expr)))
(list (make-symbol "unquote") (read-expr))))
(do (set! pos (inc pos))
(if (and (< pos len-src) (= (nth source pos) "@"))
(do (set! pos (inc pos))
(list (make-symbol "splice-unquote") (read-expr)))
(list (make-symbol "unquote") (read-expr))))
;; Reader macros: #
(= ch "#")
(do
(set! pos (inc pos))
(if
(>= pos len-src)
(error "Unexpected end of input after #")
(let
((dispatch-ch (nth source pos)))
(cond
(= dispatch-ch ";")
(do (set! pos (inc pos)) (read-expr) (read-expr))
(= dispatch-ch "|")
(do (set! pos (inc pos)) (read-raw-string))
(= dispatch-ch "'")
(do
(set! pos (inc pos))
(list (make-symbol "quote") (read-expr)))
(= dispatch-ch "\\")
(do (set! pos (inc pos)) (read-char-literal))
(ident-start? dispatch-ch)
(let
((macro-name (read-ident)))
(let
((handler (reader-macro-get macro-name)))
(if
handler
(handler (read-expr))
(error
(str "Unknown reader macro: #" macro-name)))))
:else (error (str "Unknown reader macro: #" dispatch-ch))))))
(or
(and (>= ch "0") (<= ch "9"))
(and
(= ch "-")
(< (inc pos) len-src)
(let
((next-ch (nth source (inc pos))))
(and (>= next-ch "0") (<= next-ch "9")))))
(read-number)
(and
(= ch ".")
(< (+ pos 2) len-src)
(= (nth source (+ pos 1)) ".")
(= (nth source (+ pos 2)) "."))
(do (set! pos (+ pos 3)) (make-symbol "..."))
(do (set! pos (inc pos))
(if (>= pos len-src)
(error "Unexpected end of input after #")
(let ((dispatch-ch (nth source pos)))
(cond
;; #; — datum comment: read and discard next expr
(= dispatch-ch ";")
(do (set! pos (inc pos))
(read-expr) ;; read and discard
(read-expr)) ;; return the NEXT expr
;; #| — raw string
(= dispatch-ch "|")
(do (set! pos (inc pos))
(read-raw-string))
;; #' — quote shorthand
(= dispatch-ch "'")
(do (set! pos (inc pos))
(list (make-symbol "quote") (read-expr)))
;; #name — extensible dispatch
(ident-start? dispatch-ch)
(let ((macro-name (read-ident)))
(let ((handler (reader-macro-get macro-name)))
(if handler
(handler (read-expr))
(error (str "Unknown reader macro: #" macro-name)))))
:else
(error (str "Unknown reader macro: #" dispatch-ch))))))
;; Number (or negative number)
(or (and (>= ch "0") (<= ch "9"))
(and (= ch "-")
(< (inc pos) len-src)
(let ((next-ch (nth source (inc pos))))
(and (>= next-ch "0") (<= next-ch "9")))))
(read-number)
;; Ellipsis (... as a symbol)
(and (= ch ".")
(< (+ pos 2) len-src)
(= (nth source (+ pos 1)) ".")
(= (nth source (+ pos 2)) "."))
(do (set! pos (+ pos 3))
(make-symbol "..."))
;; Symbol (must be ident-start char)
(ident-start? ch)
(read-symbol)
:else (error (str "Unexpected character: " ch)))))))
(let
((exprs (list)))
(define
parse-loop
:effects ()
(fn
()
(read-symbol)
;; Unexpected
:else
(error (str "Unexpected character: " ch)))))))
;; -- Entry point: parse all top-level expressions --
(let ((exprs (list)))
(define parse-loop :effects []
(fn ()
(skip-ws)
(when (< pos len-src) (append! exprs (read-expr)) (parse-loop))))
(when (< pos len-src)
(append! exprs (read-expr))
(parse-loop))))
(parse-loop)
exprs))))
@@ -499,77 +362,30 @@
;; --------------------------------------------------------------------------
;; Serialize AST value back to SX source
(define
sx-serialize
:effects ()
(fn
(val)
(case
(type-of val)
"nil"
"nil"
"boolean"
(if val "true" "false")
"number"
(str val)
"rational"
(str (numerator val) "/" (denominator val))
"string"
(str "\"" (escape-string val) "\"")
"symbol"
(symbol-name val)
"keyword"
(str ":" (keyword-name val))
"list"
(str "(" (join " " (map sx-serialize val)) ")")
"dict"
(sx-serialize-dict val)
"sx-expr"
(sx-expr-source val)
"spread"
(str "(make-spread " (sx-serialize-dict (spread-attrs val)) ")")
"char"
(let
((n (char->integer val)))
(str
"#\\"
(cond
(= n 32)
"space"
(= n 10)
"newline"
(= n 9)
"tab"
(= n 13)
"return"
(= n 0)
"nul"
(= n 27)
"escape"
(= n 127)
"delete"
(= n 8)
"backspace"
:else (char-from-code n))))
:else (str val))))
(define sx-serialize :effects []
(fn (val)
(case (type-of val)
"nil" "nil"
"boolean" (if val "true" "false")
"number" (str val)
"string" (str "\"" (escape-string val) "\"")
"symbol" (symbol-name val)
"keyword" (str ":" (keyword-name val))
"list" (str "(" (join " " (map sx-serialize val)) ")")
"dict" (sx-serialize-dict val)
"sx-expr" (sx-expr-source val)
"spread" (str "(make-spread " (sx-serialize-dict (spread-attrs val)) ")")
:else (str val))))
;; Serialize a dict to SX {:key val} format
(define
sx-serialize-dict
:effects ()
(fn
((d :as dict))
(str
"{"
(join
" "
(define sx-serialize-dict :effects []
(fn ((d :as dict))
(str "{"
(join " "
(reduce
(fn
((acc :as list) (key :as string))
(concat
acc
(list (str ":" key) (sx-serialize (dict-get d key)))))
(fn ((acc :as list) (key :as string))
(concat acc (list (str ":" key) (sx-serialize (dict-get d key)))))
(list)
(keys d)))
"}")))
@@ -591,18 +407,13 @@
;; True for: ident-start chars plus: 0-9 . : / # ,
;;
;; Constructors (provided by the SX runtime):
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (parse-number s) → number (int or float from string)
;; (make-char n) → Char value from Unicode codepoint n
;; (make-rational n d) → Rational value (auto-reduced by GCD; d=0 is an error)
;; (char->integer c) → Unicode codepoint of char c
;; (make-symbol name) → Symbol value
;; (make-keyword name) → Keyword value
;; (parse-number s) → number (int or float from string)
;;
;; String utilities:
;; (escape-string s) → string with " and \ escaped
;; (sx-expr-source e) → unwrap SxExpr to its source string
;; (char-from-code n) → single-char string from codepoint n
;; (char-code s) → codepoint of first char in string s
;;
;; Reader macro registry:
;; (reader-macro-get name) → handler fn or nil

View File

@@ -43,35 +43,35 @@
"+"
:params (&rest (args :as number))
:returns "number"
:doc "Sum all arguments. Returns integer iff all args are exact integers (float contagion)."
:doc "Sum all arguments."
:body (reduce (fn (a b) (native-add a b)) 0 args))
(define-primitive
"-"
:params ((a :as number) &rest (b :as number))
:returns "number"
:doc "Subtract. Unary: negate. Binary: a - b. Float contagion: returns integer iff all args are integers."
:doc "Subtract. Unary: negate. Binary: a - b."
:body (if (empty? b) (native-neg a) (native-sub a (first b))))
(define-primitive
"*"
:params (&rest (args :as number))
:returns "number"
:doc "Multiply all arguments. Float contagion: integer result iff all args are exact integers."
:doc "Multiply all arguments."
:body (reduce (fn (a b) (native-mul a b)) 1 args))
(define-primitive
"/"
:params ((a :as number) (b :as number))
:returns "float"
:doc "Divide a by b. Always returns inexact float."
:returns "number"
:doc "Divide a by b."
:body (native-div a b))
(define-primitive
"mod"
:params ((a :as number) (b :as number))
:returns "number"
:doc "Modulo a % b. Returns integer iff both args are integers."
:doc "Modulo a % b."
:body (native-mod a b))
(define-primitive
@@ -108,26 +108,26 @@
(define-primitive
"floor"
:params ((x :as number))
:returns "integer"
:doc "Floor toward negative infinity — returns exact integer.")
:returns "number"
:doc "Floor to integer.")
(define-primitive
"ceil"
:params ((x :as number))
:returns "integer"
:doc "Ceiling toward positive infinity — returns exact integer.")
:returns "number"
:doc "Ceiling to integer.")
(define-primitive
"round"
:params ((x :as number) &rest (ndigits :as number))
:returns "number"
:doc "Round to ndigits decimal places (default 0). Returns integer when ndigits is 0.")
:doc "Round to ndigits decimal places (default 0).")
(define-primitive
"truncate"
:params ((x :as number))
:returns "integer"
:doc "Truncate toward zero — returns exact integer.")
:params (((x :as number)))
:returns "number"
:doc "Truncate toward zero.")
(define-primitive
"remainder"
@@ -143,42 +143,42 @@
(define-primitive
"exact?"
:params ((x :as number))
:params (((x :as number)))
:returns "boolean"
:doc "True if x is an exact integer (not an inexact float).")
:doc "True if x is exact (integer-valued).")
(define-primitive
"inexact?"
:params ((x :as number))
:params (((x :as number)))
:returns "boolean"
:doc "True if x is an inexact float (not an exact integer).")
:doc "True if x is inexact (non-integer).")
;; --------------------------------------------------------------------------
;; Core — Comparison
;; --------------------------------------------------------------------------
(define-primitive
"exact->inexact"
:params ((x :as number))
:returns "float"
:doc "Convert exact integer to inexact float. Floats pass through unchanged.")
:params (((x :as number)))
:returns "number"
:doc "Convert exact to inexact (identity for float tower).")
(define-primitive
"inexact->exact"
:params ((x :as number))
:returns "integer"
:doc "Convert inexact float to nearest exact integer (truncates). Integers pass through unchanged.")
:params (((x :as number)))
:returns "number"
:doc "Convert inexact to nearest exact integer.")
(define-primitive
"make-vector"
:params ((n :as number) (fill :as any :optional true))
:params ((n :as number))
:returns "vector"
:doc "Create vector of length n, each element initialised to fill (default nil).")
:doc "Create vector of size n, optionally filled.")
(define-primitive
"vector"
:params (:rest (elts :as any))
:params ()
:returns "vector"
:doc "Construct a vector from its arguments.")
:doc "Create vector from arguments.")
(define-primitive
"vector?"
@@ -190,31 +190,31 @@
"vector-length"
:params ((v :as vector))
:returns "number"
:doc "Number of elements in vector v.")
:doc "Number of elements.")
(define-primitive
"vector-ref"
:params ((v :as vector) (i :as number))
:returns "any"
:doc "Element at 0-based index i. Error if out of bounds.")
:doc "Element at index.")
(define-primitive
"vector-set!"
:params ((v :as vector) (i :as number) (val :as any))
:returns "nil"
:doc "Mutate element at index i to val. Error if out of bounds.")
:doc "Set element at index.")
(define-primitive
"vector->list"
:params ((v :as vector))
:returns "list"
:doc "Convert vector to a fresh list.")
:doc "Convert vector to list.")
(define-primitive
"list->vector"
:params ((l :as list))
:returns "vector"
:doc "Convert list to a fresh vector.")
:doc "Convert list to vector.")
;; --------------------------------------------------------------------------
;; Core — Predicates
@@ -223,15 +223,13 @@
"vector-fill!"
:params ((v :as vector) (val :as any))
:returns "nil"
:doc "Set every element of v to val in place.")
:doc "Fill all elements.")
(define-primitive
"vector-copy"
:params ((v :as vector)
(start :as number :optional true)
(end :as number :optional true))
:params ((v :as vector))
:returns "vector"
:doc "Shallow copy of vector, optionally sliced from start (inclusive) to end (exclusive).")
:doc "Independent shallow copy.")
(define-primitive
"min"
@@ -374,20 +372,8 @@
"number?"
:params (x)
:returns "boolean"
:doc "True if x is any number — exact integer or inexact float."
:body (or (= (type-of x) "number") (integer? x)))
(define-primitive
"integer?"
:params (x)
:returns "boolean"
:doc "True if x is an exact integer, or a float with no fractional part (e.g. 1.0).")
(define-primitive
"float?"
:params (x)
:returns "boolean"
:doc "True if x is an inexact float (Number type). Does not match exact integers.")
:doc "True if x is a number (int or float)."
:body (= (type-of x) "number"))
(define-primitive
"string?"
@@ -492,12 +478,6 @@
:returns "string"
:doc "Convert Unicode code point to single-character string.")
(define-primitive
"char-code"
:params ((s :as string))
:returns "number"
:doc "Unicode codepoint of the first character of string s.")
(define-primitive
"substring"
:params ((s :as string) (start :as number) (end :as number))
@@ -552,15 +532,15 @@
:returns "boolean"
:doc "True if string s starts with prefix.")
;; --------------------------------------------------------------------------
;; Core — Dict operations
;; --------------------------------------------------------------------------
(define-primitive
"ends-with?"
:params ((s :as string) (suffix :as string))
:returns "boolean"
:doc "True if string s ends with suffix.")
;; --------------------------------------------------------------------------
;; Core — Dict operations
;; --------------------------------------------------------------------------
(define-module :core.collections)
(define-primitive
@@ -605,15 +585,15 @@
:returns "any"
:doc "Last element, or nil if empty.")
;; --------------------------------------------------------------------------
;; Stdlib — Format
;; --------------------------------------------------------------------------
(define-primitive
"rest"
:params ((coll :as list))
:returns "list"
:doc "All elements except the first.")
;; --------------------------------------------------------------------------
;; Stdlib — Format
;; --------------------------------------------------------------------------
(define-primitive
"nth"
:params ((coll :as list) (n :as number))
@@ -638,15 +618,15 @@
:returns "list"
:doc "Mutate coll by appending x in-place. Returns coll.")
;; --------------------------------------------------------------------------
;; Stdlib — Text
;; --------------------------------------------------------------------------
(define-primitive
"reverse"
:params ((coll :as list))
:returns "list"
:doc "Return coll in reverse order.")
;; --------------------------------------------------------------------------
;; Stdlib — Text
;; --------------------------------------------------------------------------
(define-primitive
"flatten"
:params ((coll :as list))
@@ -665,29 +645,29 @@
:returns "list"
:doc "Consecutive pairs: (1 2 3 4) → ((1 2) (2 3) (3 4)).")
(define-module :core.dict)
;; --------------------------------------------------------------------------
;; Stdlib — Style
;; --------------------------------------------------------------------------
;; --------------------------------------------------------------------------
;; Stdlib — Debug
;; --------------------------------------------------------------------------
(define-module :core.dict)
(define-primitive
"keys"
:params ((d :as dict))
:returns "list"
:doc "List of dict keys.")
;; --------------------------------------------------------------------------
;; Type introspection — platform primitives
;; --------------------------------------------------------------------------
(define-primitive
"vals"
:params ((d :as dict))
:returns "list"
:doc "List of dict values.")
;; --------------------------------------------------------------------------
;; Type introspection — platform primitives
;; --------------------------------------------------------------------------
(define-primitive
"merge"
:params (&rest (dicts :as dict))
@@ -803,532 +783,3 @@
:params ((source :as string))
:returns "list"
:doc "Parse SX source string into a list of AST expressions.")
(define-primitive
"make-string-buffer"
:params ()
:returns "string-buffer"
:doc "Create a new empty mutable string buffer for O(1) amortised append.")
(define-module :stdlib.coroutines)
(define-module :stdlib.bitwise)
(define-primitive
"bitwise-and"
:params (((a :as number) (b :as number)))
:returns "number"
:doc "Bitwise AND of two integers.")
(define-primitive
"bitwise-or"
:params (((a :as number) (b :as number)))
:returns "number"
:doc "Bitwise OR of two integers.")
(define-primitive
"bitwise-xor"
:params (((a :as number) (b :as number)))
:returns "number"
:doc "Bitwise XOR of two integers.")
(define-primitive
"bitwise-not"
:params ((a :as number))
:returns "number"
:doc "Bitwise NOT (one's complement) of an integer.")
(define-primitive
"arithmetic-shift"
:params (((a :as number) (count :as number)))
:returns "number"
:doc "Arithmetic shift: left if count > 0, right if count < 0.")
(define-primitive
"bit-count"
:params ((a :as number))
:returns "number"
:doc "Count set bits (popcount) in a non-negative integer.")
(define-primitive
"integer-length"
:params ((a :as number))
:returns "number"
:doc "Number of bits needed to represent integer a (excluding sign).")
(define-module :stdlib.ports)
(define-primitive
"eof-object"
:params ()
:returns "eof-object"
:doc "The EOF sentinel value.")
(define-primitive
"eof-object?"
:params (v)
:returns "boolean"
:doc "True if v is the EOF sentinel.")
(define-primitive
"open-input-string"
:params ((s :as string))
:returns "input-port"
:doc "Open a string as an input port.")
(define-primitive
"open-output-string"
:params ()
:returns "output-port"
:doc "Open a fresh output string port.")
(define-primitive
"get-output-string"
:params ((p :as output-port))
:returns "string"
:doc "Flush output port contents to a string.")
(define-primitive
"port?"
:params (v)
:returns "boolean"
:doc "True if v is any port.")
(define-primitive
"input-port?"
:params (v)
:returns "boolean"
:doc "True if v is an input port.")
(define-primitive
"output-port?"
:params (v)
:returns "boolean"
:doc "True if v is an output port.")
(define-primitive
"close-port"
:params ((p :as port))
:returns "nil"
:doc "Close a port.")
(define-primitive
"read-char"
:params (&rest (p :as input-port))
:returns "any"
:doc "Read next char from port; returns eof-object at end.")
(define-primitive
"peek-char"
:params (&rest (p :as input-port))
:returns "any"
:doc "Peek next char without consuming; returns eof-object at end.")
(define-primitive
"read-line"
:params (&rest (p :as input-port))
:returns "any"
:doc "Read a line from port; returns eof-object at end.")
(define-primitive
"write-char"
:params ((c :as char) &rest (p :as output-port))
:returns "nil"
:doc "Write a char to output port.")
(define-primitive
"write-string"
:params ((s :as string) &rest (p :as output-port))
:returns "nil"
:doc "Write a string to output port.")
(define-primitive
"char-ready?"
:params (&rest (p :as input-port))
:returns "boolean"
:doc "True if a char is immediately available on the port.")
(define-primitive
"read"
:params (&rest (p :as input-port))
:returns "any"
:doc "Read one datum from port; returns eof-object at end.")
(define-primitive
"write"
:params (v &rest (p :as output-port))
:returns "nil"
:doc "Serialize v to port with quoting — strings quoted, chars as #\\a notation.")
(define-primitive
"display"
:params (v &rest (p :as output-port))
:returns "nil"
:doc "Serialize v to port without quoting — strings unquoted, chars as characters.")
(define-primitive
"newline"
:params (&rest (p :as output-port))
:returns "nil"
:doc "Write a newline to port.")
(define-primitive
"write-to-string"
:params (v)
:returns "string"
:doc "Serialize v with write quoting, return as string.")
(define-primitive
"display-to-string"
:params (v)
:returns "string"
:doc "Serialize v with display format, return as string.")
(define-primitive
"current-input-port"
:params ()
:returns "any"
:doc "Return current default input port.")
(define-primitive
"current-output-port"
:params ()
:returns "any"
:doc "Return current default output port.")
(define-primitive
"current-error-port"
:params ()
:returns "any"
:doc "Return current error port.")
(define-module :stdlib.math)
(define-primitive
"sin"
:params ((x :as number))
:returns "float"
:doc "Sine of x (radians).")
(define-primitive
"cos"
:params ((x :as number))
:returns "float"
:doc "Cosine of x (radians).")
(define-primitive
"tan"
:params ((x :as number))
:returns "float"
:doc "Tangent of x (radians).")
(define-primitive
"asin"
:params ((x :as number))
:returns "float"
:doc "Arc sine of x; result in radians.")
(define-primitive
"acos"
:params ((x :as number))
:returns "float"
:doc "Arc cosine of x; result in radians.")
(define-primitive
"atan"
:params ((x :as number) &rest (y :as number))
:returns "float"
:doc "Arc tangent. (atan x) → radians in (-π/2, π/2). (atan y x) → atan2(y, x).")
(define-primitive
"exp"
:params ((x :as number))
:returns "float"
:doc "e raised to the power x.")
(define-primitive
"log"
:params ((x :as number))
:returns "float"
:doc "Natural logarithm of x.")
(define-primitive
"expt"
:params ((base :as number) (exp :as number))
:returns "number"
:doc "base raised to the power exp. Alias: pow.")
(define-primitive
"quotient"
:params ((a :as number) (b :as number))
:returns "integer"
:doc "Integer quotient: truncate(a / b) toward zero. Sign follows dividend.")
(define-primitive
"gcd"
:params ((a :as number) (b :as number))
:returns "integer"
:doc "Greatest common divisor of a and b.")
(define-primitive
"lcm"
:params ((a :as number) (b :as number))
:returns "integer"
:doc "Least common multiple of a and b.")
(define-primitive
"number->string"
:params ((n :as number) &rest (radix :as number))
:returns "string"
:doc "Convert number n to string. Optional radix (default 10). E.g. (number->string 255 16) → \"ff\".")
(define-primitive
"string->number"
:params ((s :as string) &rest (radix :as number))
:returns "any"
:doc "Parse string s as a number. Optional radix (default 10). Returns nil on failure.")
(define-module :stdlib.rational)
(define-primitive
"make-rational"
:params (n d)
:returns "rational"
:doc "Rational n/d, auto-reduced by GCD. Error if d=0.")
(define-primitive
"rational?"
:params (v)
:returns "boolean"
:doc "True if v is a rational number.")
(define-primitive
"numerator"
:params ((r :as rational))
:returns "integer"
:doc "Numerator of rational r (after reduction).")
(define-primitive
"denominator"
:params ((r :as rational))
:returns "integer"
:doc "Denominator of rational r (after reduction, always positive).")
(define-module :stdlib.hash-table)
(define-module :stdlib.sets)
(define-primitive
"make-set"
:params (&rest (lst :as list))
:returns "set"
:doc "Create a fresh empty set. Optional list argument seeds the set: (make-set '(1 2 3)).")
(define-primitive
"set?"
:params (v)
:returns "boolean"
:doc "True if v is a set.")
(define-primitive
"set-add!"
:params (s val)
:returns "nil"
:doc "Add val to set s in place. No-op if already present.")
(define-primitive
"set-member?"
:params (s val)
:returns "boolean"
:doc "True if val is in set s.")
(define-primitive
"set-remove!"
:params (s val)
:returns "nil"
:doc "Remove val from set s in place. No-op if absent.")
(define-primitive
"set-size"
:params (s)
:returns "integer"
:doc "Number of elements in set s.")
(define-primitive
"set->list"
:params (s)
:returns "list"
:doc "All elements of set s as a list (unspecified order).")
(define-primitive
"list->set"
:params (lst)
:returns "set"
:doc "Create a new set containing all elements of lst.")
(define-primitive
"set-union"
:params (s1 s2)
:returns "set"
:doc "New set with all elements from s1 and s2.")
(define-primitive
"set-intersection"
:params (s1 s2)
:returns "set"
:doc "New set with elements present in both s1 and s2.")
(define-primitive
"set-difference"
:params (s1 s2)
:returns "set"
:doc "New set with elements in s1 that are not in s2.")
(define-primitive
"set-for-each"
:params (s fn)
:returns "nil"
:doc "Call (fn val) for each element in set s. Order unspecified.")
(define-primitive
"set-map"
:params (s fn)
:returns "set"
:doc "New set of results of (fn val) for each element in s.")
(define-module :stdlib.regexp)
(define-primitive
"make-regexp"
:params ((pattern :as string) &rest (flags :as string))
:returns "regexp"
:doc "Compile regexp from pattern string and optional flags string (\"i\" case-insensitive, \"m\" multiline, \"s\" dotall).")
(define-primitive
"regexp?"
:params (v)
:returns "boolean"
:doc "True if v is a compiled regexp.")
(define-primitive
"regexp-source"
:params ((re :as regexp))
:returns "string"
:doc "Pattern string of a regexp.")
(define-primitive
"regexp-flags"
:params ((re :as regexp))
:returns "string"
:doc "Flags string of a regexp.")
(define-primitive
"regexp-match"
:params ((re :as regexp) (str :as string))
:returns "any"
:doc "First match of re in str. Returns {:match \"...\" :start N :end N :groups (...)} or nil.")
(define-primitive
"regexp-match-all"
:params ((re :as regexp) (str :as string))
:returns "list"
:doc "All non-overlapping matches of re in str as a list of match dicts.")
(define-primitive
"regexp-replace"
:params ((re :as regexp) (str :as string) (replacement :as string))
:returns "string"
:doc "Replace first match of re in str with replacement. $& = whole match, $1..$9 = groups.")
(define-primitive
"regexp-replace-all"
:params ((re :as regexp) (str :as string) (replacement :as string))
:returns "string"
:doc "Replace all matches of re in str with replacement.")
(define-primitive
"regexp-split"
:params ((re :as regexp) (str :as string))
:returns "list"
:doc "Split str on every match of re; returns list of strings.")
(define-module :stdlib.bytevectors)
(define-primitive
"make-bytevector"
:params (n &rest fill)
:returns "bytevector"
:doc "Create a bytevector of n bytes, all initialised to fill (default 0).")
(define-primitive
"bytevector?"
:params (v)
:returns "boolean"
:doc "True if v is a bytevector.")
(define-primitive
"bytevector-length"
:params ((bv :as bytevector))
:returns "number"
:doc "Number of bytes in bv.")
(define-primitive
"bytevector-u8-ref"
:params ((bv :as bytevector) (i :as number))
:returns "number"
:doc "Byte value 0-255 at index i.")
(define-primitive
"bytevector-u8-set!"
:params ((bv :as bytevector) (i :as number) (byte :as number))
:returns "nil"
:doc "Set byte at index i to byte 0-255. Mutates bv.")
(define-primitive
"bytevector-copy"
:params ((bv :as bytevector) &rest bounds)
:returns "bytevector"
:doc "Fresh copy of bv, optionally sliced to [start, end).")
(define-primitive
"bytevector-copy!"
:params ((dst :as bytevector) (at :as number) (src :as bytevector) &rest bounds)
:returns "nil"
:doc "Copy bytes from src[start..end) into dst starting at at. Mutates dst.")
(define-primitive
"bytevector-append"
:params (&rest bvs)
:returns "bytevector"
:doc "Concatenate bytevectors into a new bytevector.")
(define-primitive
"utf8->string"
:params ((bv :as bytevector) &rest bounds)
:returns "string"
:doc "Decode bv[start..end) as UTF-8 and return the string.")
(define-primitive
"string->utf8"
:params ((s :as string) &rest bounds)
:returns "bytevector"
:doc "Encode s[start..end) as UTF-8 and return a bytevector.")
(define-primitive
"bytevector->list"
:params ((bv :as bytevector))
:returns "list"
:doc "Convert bytevector to a list of byte integers.")
(define-primitive
"list->bytevector"
:params ((lst :as list))
:returns "bytevector"
:doc "Build a bytevector from a list of byte integers 0-255.")
(define-primitive
"format"
:params ((template :as string) &rest args)
:returns "string"
:doc "CL-style format string. Directives: ~a display, ~s write, ~d decimal, ~x hex, ~o octal, ~b binary, ~f fixed-point, ~e scientific, ~% newline, ~& fresh-line, ~~ tilde, ~t tab. Optional first arg: output-port.")

View File

@@ -1,134 +0,0 @@
;; ==========================================================================
;; stdlib.sx — Pure SX standard library functions
;;
;; Loaded by test runners after primitives. These functions are implemented
;; in SX and require no host-specific code.
;;
;; IMPORTANT: SX let/when bodies evaluate only the LAST expression.
;; Multi-step bodies must be wrapped in (do expr1 expr2 ...).
;; ==========================================================================
;; --------------------------------------------------------------------------
;; format — CL-style string formatting
;;
;; Directives:
;; ~a display (no quotes) ~s write (with quotes)
;; ~d decimal ~x hex ~o octal ~b binary
;; ~f fixed-point (6dp) ~% newline
;; ~& fresh line ~~ literal tilde
;; ~t tab
;;
;; Signature: (format template arg...) -> string
;; --------------------------------------------------------------------------
(define
(format template &rest args)
(let
((buf (make-string-buffer)) (n (string-length template)))
(define
(consume-arg args)
(if
(nil? args)
(list "" nil)
(list (display-to-string (first args)) (rest args))))
(define
(consume-num args radix)
(if
(nil? args)
(list "" nil)
(list (number->string (first args) radix) (rest args))))
(define
(loop i args)
(cond
((>= i n) (string-buffer->string buf))
((and (= (substring template i (+ i 1)) "~") (< (+ i 1) n))
(let
((dir (substring template (+ i 1) (+ i 2))))
(cond
((= dir "a")
(let
((p (consume-arg args)))
(do
(string-buffer-append! buf (first p))
(loop (+ i 2) (first (rest p))))))
((= dir "s")
(if
(nil? args)
(loop (+ i 2) args)
(do
(string-buffer-append!
buf
(write-to-string (first args)))
(loop (+ i 2) (rest args)))))
((= dir "d")
(let
((p (consume-num args 10)))
(do
(string-buffer-append! buf (first p))
(loop (+ i 2) (first (rest p))))))
((= dir "x")
(let
((p (consume-num args 16)))
(do
(string-buffer-append! buf (first p))
(loop (+ i 2) (first (rest p))))))
((= dir "o")
(let
((p (consume-num args 8)))
(do
(string-buffer-append! buf (first p))
(loop (+ i 2) (first (rest p))))))
((= dir "b")
(let
((p (consume-num args 2)))
(do
(string-buffer-append! buf (first p))
(loop (+ i 2) (first (rest p))))))
((= dir "f")
(if
(nil? args)
(loop (+ i 2) args)
(do
(string-buffer-append!
buf
(format-decimal (first args) 6))
(loop (+ i 2) (rest args)))))
((= dir "%")
(do
(string-buffer-append! buf "\n")
(loop (+ i 2) args)))
((= dir "&")
(do
(let
((so-far (string-buffer->string buf)))
(when
(or
(= (string-length so-far) 0)
(not
(=
(substring
so-far
(- (string-length so-far) 1)
(string-length so-far))
"\n")))
(string-buffer-append! buf "\n")))
(loop (+ i 2) args)))
((= dir "~")
(do
(string-buffer-append! buf "~")
(loop (+ i 2) args)))
((= dir "t")
(do
(string-buffer-append! buf "\t")
(loop (+ i 2) args)))
(else
(do
(string-buffer-append! buf "~")
(loop (+ i 1) args))))))
(else
(do
(string-buffer-append!
buf
(substring template i (+ i 1)))
(loop (+ i 1) args)))))
(loop 0 args)))

View File

@@ -1,278 +0,0 @@
(defsuite
"algebraic-data-types"
(deftest
"constructor creates dict with adt marker"
(do
(define-type Maybe (Just value) (Nothing))
(assert= true (get (Just 42) :_adt))))
(deftest
"constructor stores type name"
(do
(define-type Shape (Circle radius) (Square side))
(assert= "Shape" (get (Circle 5) :_type))
(assert= "Shape" (get (Square 3) :_type))))
(deftest
"constructor stores constructor name"
(do
(define-type Opt (Some val) (None))
(assert= "Some" (get (Some 1) :_ctor))
(assert= "None" (get (None) :_ctor))))
(deftest
"constructor stores fields as list"
(do
(define-type Pair (Pair-of fst snd))
(assert-equal
(list 1 2)
(get (Pair-of 1 2) :_fields))))
(deftest
"zero-arg constructor has empty fields"
(do
(define-type Flag (Set) (Unset))
(assert-equal (list) (get (Set) :_fields))
(assert-equal (list) (get (Unset) :_fields))))
(deftest
"type predicate true for all constructors"
(do
(define-type Expr (Num n) (Add left right) (Neg e))
(assert= true (Expr? (Num 5)))
(assert= true (Expr? (Add (Num 1) (Num 2))))
(assert= true (Expr? (Neg (Num 3))))))
(deftest
"type predicate false for non-adt values"
(do
(define-type Box (Box-of x))
(assert= false (Box? 42))
(assert= false (Box? "hello"))
(assert= false (Box? nil))
(assert= false (Box? (list 1 2)))
(assert= false (Box? {}))))
(deftest
"type predicate false for wrong adt type"
(do
(define-type AT (AV x))
(define-type BT (BV x))
(assert= false (AT? (BV 1)))
(assert= false (BT? (AV 1)))))
(deftest
"constructor predicate true for matching constructor"
(do
(define-type Result (Ok value) (Err msg))
(assert= true (Ok? (Ok 42)))
(assert= true (Err? (Err "bad")))))
(deftest
"constructor predicate false for wrong constructor"
(do
(define-type Coin (Heads) (Tails))
(assert= false (Heads? (Tails)))
(assert= false (Tails? (Heads)))))
(deftest
"constructor predicate false for non-adt"
(do
(define-type Wrap (Wrapped x))
(assert= false (Wrapped? 42))
(assert= false (Wrapped? nil))
(assert= false (Wrapped? "str"))))
(deftest
"single-field accessor returns field value"
(do
(define-type Holder (Held content))
(assert= 99 (Held-content (Held 99)))
(assert= "hello" (Held-content (Held "hello")))))
(deftest
"multi-field accessors return correct fields"
(do
(define-type Triple (Triple-of a b c))
(let
((t (Triple-of 10 20 30)))
(assert= 10 (Triple-of-a t))
(assert= 20 (Triple-of-b t))
(assert= 30 (Triple-of-c t)))))
(deftest
"tree constructors and accessors"
(do
(define-type Tree (Leaf) (Node left val right))
(let
((t (Node (Leaf) 5 (Node (Leaf) 3 (Leaf)))))
(assert= true (Node? t))
(assert= 5 (Node-val t))
(assert= true (Leaf? (Node-left t)))
(assert= true (Node? (Node-right t)))
(assert= 3 (Node-val (Node-right t))))))
(deftest
"arity error on too few args"
(do
(define-type Pair2 (Pair2-of a b))
(let
((ok false))
(guard (exn (else (set! ok true))) (Pair2-of 1))
(assert ok))))
(deftest
"arity error on too many args"
(do
(define-type Single (Single-of x))
(let
((ok false))
(guard
(exn (else (set! ok true)))
(Single-of 1 2))
(assert ok))))
(deftest
"multiple types are independent"
(do
(define-type Color2 (Red2) (Green2) (Blue2))
(define-type Suit (Hearts) (Diamonds) (Clubs) (Spades))
(assert= false (Color2? (Hearts)))
(assert= false (Suit? (Red2)))
(assert= true (Color2? (Blue2)))
(assert= true (Suit? (Spades)))))
(deftest
"adt fields can hold any value"
(do
(define-type Container (Hold x))
(assert-equal
(list 1 2 3)
(Hold-x (Hold (list 1 2 3))))
(assert-equal {:a 1} (Hold-x (Hold {:a 1})))))
(deftest
"adt-registry tracks type constructor names"
(do
(define-type Days (Mon) (Tue) (Wed) (Thu) (Fri))
(assert-equal
(list "Mon" "Tue" "Wed" "Thu" "Fri")
(get *adt-registry* "Days"))))
(deftest
"constructors with same field name in different types are independent"
(do
(define-type P1 (P1-ctor value))
(define-type P2 (P2-ctor value))
(assert= 10 (P1-ctor-value (P1-ctor 10)))
(assert= 20 (P2-ctor-value (P2-ctor 20)))))
(deftest
"match dispatches on first matching constructor"
(do
(define-type Color (Red) (Green) (Blue))
(assert= "red" (match (Red) ((Red) "red") ((Green) "green") ((Blue) "blue")))
(assert= "green" (match (Green) ((Red) "red") ((Green) "green") ((Blue) "blue")))
(assert= "blue" (match (Blue) ((Red) "red") ((Green) "green") ((Blue) "blue")))))
(deftest
"match binds field to variable"
(do
(define-type Wrapper (Wrap val))
(assert= 42 (match (Wrap 42) ((Wrap v) v)))
(assert= "hi" (match (Wrap "hi") ((Wrap v) v)))))
(deftest
"match zero-arg constructor"
(do
(define-type Signal (On) (Off))
(assert= "on" (match (On) ((On) "on") ((Off) "off")))
(assert= "off" (match (Off) ((On) "on") ((Off) "off")))))
(deftest
"match multi-field constructor binds all fields"
(do
(define-type Vec2 (V2 x y))
(let ((v (V2 3 4)))
(assert= 7 (match v ((V2 a b) (+ a b)))))))
(deftest
"match with else clause"
(do
(define-type Opt2 (Some2 val) (None2))
(assert= 10 (match (Some2 10) ((Some2 v) v) (else 0)))
(assert= 0 (match (None2) ((Some2 v) v) (else 0)))))
(deftest
"match else catches non-adt values"
(do
(assert= "other" (match 42 ((else) "other") (else "other")))
(assert= "other" (match "str" (else "other")))))
(deftest
"match returns body expression value"
(do
(define-type Num (Num-of n))
(assert= 100 (match (Num-of 10) ((Num-of n) (* n n))))))
(deftest
"match second arm fires when first does not match"
(do
(define-type Either (Left val) (Right val))
(assert= "left-1" (match (Left 1) ((Left v) (str "left-" v)) ((Right v) (str "right-" v))))
(assert= "right-2" (match (Right 2) ((Left v) (str "left-" v)) ((Right v) (str "right-" v))))))
(deftest
"match wildcard _ in constructor pattern"
(do
(define-type Pair3 (Pair3-of a b))
(assert= 5 (match (Pair3-of 5 99) ((Pair3-of x _) x)))
(assert= 99 (match (Pair3-of 5 99) ((Pair3-of _ y) y)))))
(deftest
"match nested adt constructor pattern"
(do
(define-type Tree2 (Leaf2) (Node2 left val right))
(let ((t (Node2 (Leaf2) 7 (Leaf2))))
(assert= 7 (match t ((Node2 _ v _) v)))
(assert= true (match t ((Node2 (Leaf2) _ _) true) (else false))))))
(deftest
"match literal pattern"
(do
(assert= "zero" (match 0 (0 "zero") (else "nonzero")))
(assert= "hello" (match "hello" ("hello" "hello") (else "other")))))
(deftest
"match symbol binding pattern"
(do
(assert= 42 (match 42 (x x)))))
(deftest
"match no matching clause raises error"
(do
(define-type AB (A-val) (B-val))
(let ((ok false))
(guard (exn (else (set! ok true)))
(match (A-val) ((B-val) "b")))
(assert ok))))
(deftest
"match result used in further computation"
(do
(define-type Num2 (N v))
(assert= 30
(+
(match (N 10) ((N v) v))
(match (N 20) ((N v) v))))))
(deftest
"match with define"
(do
(define-type Tag (Tagged label value))
(define get-label (fn (t) (match t ((Tagged lbl _) lbl))))
(define get-value (fn (t) (match t ((Tagged _ val) val))))
(let ((t (Tagged "name" 99)))
(assert= "name" (get-label t))
(assert= 99 (get-value t)))))
(deftest
"match three-field constructor"
(do
(define-type Triple2 (T3 a b c))
(assert= 6 (match (T3 1 2 3) ((T3 a b c) (+ a b c))))))
(deftest
"match clauses tried in order"
(do
(define-type Expr2 (Lit n) (Add l r) (Mul l r))
(define eval-expr2 (fn (e)
(match e
((Lit n) n)
((Add l r) (+ (eval-expr2 l) (eval-expr2 r)))
((Mul l r) (* (eval-expr2 l) (eval-expr2 r))))))
(assert= 7 (eval-expr2 (Add (Lit 3) (Lit 4))))
(assert= 12 (eval-expr2 (Mul (Lit 3) (Lit 4))))
(assert= 11 (eval-expr2 (Add (Lit 2) (Mul (Lit 3) (Lit 3)))))))
(deftest
"match else binding captures value"
(do
(define-type Coin2 (Heads2) (Tails2))
(assert= "Tails2" (match (Tails2) ((Heads2) "Heads2") (x (get x :_ctor))))))
(deftest
"match on adt with string field"
(do
(define-type Msg (Hello name) (Bye name))
(assert= "Hello, Alice" (match (Hello "Alice") ((Hello n) (str "Hello, " n)) ((Bye n) (str "Bye, " n))))
(assert= "Bye, Bob" (match (Bye "Bob") ((Hello n) (str "Hello, " n)) ((Bye n) (str "Bye, " n))))))
(deftest
"match nested pattern with variable binding"
(do
(define-type Box2 (Box2-of v))
(define-type Inner (Inner-of n))
(assert= 5 (match (Box2-of (Inner-of 5)) ((Box2-of (Inner-of n)) n)))))
)

View File

@@ -1,157 +0,0 @@
(defsuite
"bitwise-operations"
(deftest
"bitwise-and basic"
(do
(assert= 0 (bitwise-and 0 0))
(assert= 1 (bitwise-and 3 1))
(assert= 0 (bitwise-and 5 2))
(assert= 4 (bitwise-and 12 6))))
(deftest
"bitwise-and identity and zero"
(do
(assert= 255 (bitwise-and 255 255))
(assert= 0 (bitwise-and 255 0))))
(deftest
"bitwise-or basic"
(do
(assert= 0 (bitwise-or 0 0))
(assert= 3 (bitwise-or 1 2))
(assert= 7 (bitwise-or 5 3))
(assert= 15 (bitwise-or 9 6))))
(deftest
"bitwise-or identity"
(do
(assert= 255 (bitwise-or 255 0))
(assert= 255 (bitwise-or 0 255))))
(deftest
"bitwise-xor basic"
(do
(assert= 0 (bitwise-xor 0 0))
(assert= 3 (bitwise-xor 1 2))
(assert= 6 (bitwise-xor 3 5))
(assert= 0 (bitwise-xor 255 255))))
(deftest
"bitwise-xor toggle bits"
(do
(assert= 14 (bitwise-xor 10 4))
(assert= 10 (bitwise-xor 14 4))))
(deftest
"bitwise-not zero"
(do (assert= -1 (bitwise-not 0))))
(deftest
"bitwise-not positive"
(do
(assert= -2 (bitwise-not 1))
(assert= -5 (bitwise-not 4))
(assert= -256 (bitwise-not 255))))
(deftest
"bitwise-not negative"
(do
(assert= 0 (bitwise-not -1))
(assert= 1 (bitwise-not -2))
(assert= 4 (bitwise-not -5))))
(deftest
"bitwise-not double negation"
(do
(assert= 42 (bitwise-not (bitwise-not 42)))
(assert= 0 (bitwise-not (bitwise-not 0)))))
(deftest
"arithmetic-shift left"
(do
(assert= 2 (arithmetic-shift 1 1))
(assert= 4 (arithmetic-shift 1 2))
(assert= 16 (arithmetic-shift 1 4))
(assert= 8 (arithmetic-shift 2 2))))
(deftest
"arithmetic-shift right"
(do
(assert= 1 (arithmetic-shift 2 -1))
(assert= 1 (arithmetic-shift 4 -2))
(assert= 5 (arithmetic-shift 10 -1))
(assert= 2 (arithmetic-shift 16 -3))))
(deftest
"arithmetic-shift by zero"
(do
(assert= 42 (arithmetic-shift 42 0))
(assert= 0 (arithmetic-shift 0 5))))
(deftest
"arithmetic-shift negative value right preserves sign"
(do
(assert= -1 (arithmetic-shift -1 -1))
(assert= -2 (arithmetic-shift -4 -1))))
(deftest
"bit-count zero"
(do (assert= 0 (bit-count 0))))
(deftest
"bit-count powers of two"
(do
(assert= 1 (bit-count 1))
(assert= 1 (bit-count 2))
(assert= 1 (bit-count 4))
(assert= 1 (bit-count 128))))
(deftest
"bit-count all-ones values"
(do
(assert= 8 (bit-count 255))
(assert= 4 (bit-count 15))
(assert= 2 (bit-count 3))))
(deftest
"bit-count mixed"
(do
(assert= 3 (bit-count 7))
(assert= 2 (bit-count 5))
(assert= 3 (bit-count 11))
(assert= 4 (bit-count 30))))
(deftest
"integer-length zero"
(do (assert= 0 (integer-length 0))))
(deftest
"integer-length powers of two"
(do
(assert= 1 (integer-length 1))
(assert= 2 (integer-length 2))
(assert= 3 (integer-length 4))
(assert= 4 (integer-length 8))
(assert= 8 (integer-length 128))))
(deftest
"integer-length non-powers"
(do
(assert= 2 (integer-length 3))
(assert= 3 (integer-length 5))
(assert= 3 (integer-length 7))
(assert= 8 (integer-length 255))
(assert= 9 (integer-length 256))))
(deftest
"bitwise ops compose"
(do
(assert=
5
(bitwise-and
(bitwise-or 5 3)
(bitwise-xor 7 2)))
(assert= 0 (bitwise-and 170 85))))
(deftest
"arithmetic-shift round-trip"
(do
(assert=
10
(arithmetic-shift (arithmetic-shift 10 3) -3))))
(deftest
"extract bits with mask"
(do
(let
((x 52))
(assert=
5
(bitwise-and (arithmetic-shift x -2) 7)))))
(deftest
"clear low bits with bitwise-not mask"
(do
(assert= 252 (bitwise-and 255 (bitwise-not 3)))))
(deftest
"integer-length after shift"
(do
(assert=
4
(integer-length (arithmetic-shift 1 3))))))

View File

@@ -1,236 +0,0 @@
;; ==========================================================================
;; test-bytevectors.sx — Tests for bytevector primitives
;; ==========================================================================
;; --------------------------------------------------------------------------
;; make-bytevector / bytevector?
;; --------------------------------------------------------------------------
(defsuite
"bv:create"
(deftest
"make-bytevector returns bytevector"
(assert (bytevector? (make-bytevector 4))))
(deftest
"make-bytevector zeroes by default"
(let
((bv (make-bytevector 3)))
(assert
(and
(= (bytevector-u8-ref bv 0) 0)
(= (bytevector-u8-ref bv 1) 0)
(= (bytevector-u8-ref bv 2) 0)))))
(deftest
"make-bytevector with fill"
(let
((bv (make-bytevector 3 42)))
(assert
(and
(= (bytevector-u8-ref bv 0) 42)
(= (bytevector-u8-ref bv 1) 42)
(= (bytevector-u8-ref bv 2) 42)))))
(deftest
"make-bytevector length 0"
(assert= (bytevector-length (make-bytevector 0)) 0))
(deftest
"bytevector? true for bytevector"
(assert (bytevector? (make-bytevector 2))))
(deftest
"bytevector? false for string"
(assert (not (bytevector? "hello"))))
(deftest "bytevector? false for nil" (assert (not (bytevector? nil))))
(deftest
"bytevector? false for list"
(assert (not (bytevector? (list 1 2 3))))))
;; --------------------------------------------------------------------------
;; bytevector-length / u8-ref / u8-set!
;; --------------------------------------------------------------------------
(defsuite
"bv:access"
(deftest
"bytevector-length"
(assert= (bytevector-length (make-bytevector 5)) 5))
(deftest
"u8-ref reads fill byte"
(assert=
(bytevector-u8-ref (make-bytevector 4 99) 2)
99))
(deftest
"u8-set! mutates"
(let
((bv (make-bytevector 3 0)))
(bytevector-u8-set! bv 1 200)
(assert= (bytevector-u8-ref bv 1) 200)))
(deftest
"u8-set! boundary byte 255"
(let
((bv (make-bytevector 1 0)))
(bytevector-u8-set! bv 0 255)
(assert= (bytevector-u8-ref bv 0) 255)))
(deftest
"u8-set! byte 0"
(let
((bv (make-bytevector 1 255)))
(bytevector-u8-set! bv 0 0)
(assert= (bytevector-u8-ref bv 0) 0))))
;; --------------------------------------------------------------------------
;; bytevector-copy
;; --------------------------------------------------------------------------
(defsuite
"bv:copy"
(deftest
"copy produces equal content"
(let
((bv (make-bytevector 3 7)))
(let
((bv2 (bytevector-copy bv)))
(assert
(and
(= (bytevector-u8-ref bv2 0) 7)
(= (bytevector-u8-ref bv2 1) 7)
(= (bytevector-u8-ref bv2 2) 7))))))
(deftest
"copy is independent"
(let
((bv (make-bytevector 2 0)))
(let
((bv2 (bytevector-copy bv)))
(bytevector-u8-set! bv2 0 99)
(assert= (bytevector-u8-ref bv 0) 0))))
(deftest
"copy with start"
(let
((bv (list->bytevector (list 10 20 30 40))))
(let
((bv2 (bytevector-copy bv 2)))
(assert
(and
(= (bytevector-length bv2) 2)
(= (bytevector-u8-ref bv2 0) 30))))))
(deftest
"copy with start and end"
(let
((bv (list->bytevector (list 10 20 30 40))))
(let
((bv2 (bytevector-copy bv 1 3)))
(assert
(and
(= (bytevector-length bv2) 2)
(= (bytevector-u8-ref bv2 0) 20)
(= (bytevector-u8-ref bv2 1) 30)))))))
;; --------------------------------------------------------------------------
;; bytevector-copy!
;; --------------------------------------------------------------------------
(defsuite
"bv:copy-bang"
(deftest
"copy! overwrites dst region"
(let
((dst (make-bytevector 4 0)))
(let
((src (list->bytevector (list 1 2 3))))
(bytevector-copy! dst 1 src)
(assert
(and
(= (bytevector-u8-ref dst 0) 0)
(= (bytevector-u8-ref dst 1) 1)
(= (bytevector-u8-ref dst 2) 2)
(= (bytevector-u8-ref dst 3) 3))))))
(deftest
"copy! with src bounds"
(let
((dst (make-bytevector 2 0)))
(let
((src (list->bytevector (list 10 20 30 40))))
(bytevector-copy! dst 0 src 1 3)
(assert
(and
(= (bytevector-u8-ref dst 0) 20)
(= (bytevector-u8-ref dst 1) 30)))))))
;; --------------------------------------------------------------------------
;; bytevector-append
;; --------------------------------------------------------------------------
(defsuite
"bv:append"
(deftest
"append two bytevectors"
(let
((bv (bytevector-append (list->bytevector (list 1 2)) (list->bytevector (list 3 4)))))
(assert
(and
(= (bytevector-length bv) 4)
(= (bytevector-u8-ref bv 0) 1)
(= (bytevector-u8-ref bv 3) 4)))))
(deftest
"append three bytevectors"
(let
((bv (bytevector-append (list->bytevector (list 1)) (list->bytevector (list 2)) (list->bytevector (list 3)))))
(assert= (bytevector-length bv) 3)))
(deftest
"append empty"
(assert=
(bytevector-length
(bytevector-append
(make-bytevector 0)
(make-bytevector 0)))
0)))
;; --------------------------------------------------------------------------
;; list->bytevector / bytevector->list
;; --------------------------------------------------------------------------
(defsuite
"bv:conversion"
(deftest
"list->bytevector roundtrip"
(let
((lst (list 10 20 30)))
(assert= (bytevector->list (list->bytevector lst)) lst)))
(deftest
"list->bytevector empty"
(assert= (bytevector-length (list->bytevector nil)) 0))
(deftest
"bytevector->list from make-bytevector"
(let
((lst (bytevector->list (make-bytevector 3 5))))
(assert= lst (list 5 5 5)))))
;; --------------------------------------------------------------------------
;; utf8 roundtrip
;; --------------------------------------------------------------------------
(defsuite
"bv:utf8"
(deftest
"string->utf8 returns bytevector"
(assert (bytevector? (string->utf8 "hello"))))
(deftest
"string->utf8 length"
(assert= (bytevector-length (string->utf8 "abc")) 3))
(deftest
"utf8->string roundtrip"
(assert= (utf8->string (string->utf8 "hello")) "hello"))
(deftest
"utf8->string with slice"
(let
((bv (string->utf8 "hello")))
(assert= (utf8->string bv 1 4) "ell")))
(deftest
"string->utf8 with start"
(assert= (utf8->string (string->utf8 "hello" 2)) "llo"))
(deftest
"string->utf8 with start and end"
(assert=
(utf8->string (string->utf8 "hello" 1 4))
"ell"))
(deftest
"empty string round-trips"
(assert= (utf8->string (string->utf8 "")) "")))

View File

@@ -1,185 +0,0 @@
;; Tests for character type (Phase 13)
;; Uses (make-char n) and (char-code "x") instead of #\x literals
;; (char literal parser syntax tested via sx-parse call)
(deftest
"make-char produces a char"
(assert= true (char? (make-char 97))))
(deftest "char? false for string" (assert= false (char? "a")))
(deftest "char? false for number" (assert= false (char? 65)))
(deftest "char? false for nil" (assert= false (char? nil)))
(deftest
"char->integer extracts codepoint"
(assert= 97 (char->integer (make-char 97))))
(deftest
"integer->char alias for make-char"
(assert= 65 (char->integer (integer->char 65))))
(deftest
"char->integer round-trip"
(assert= 122 (char->integer (make-char 122))))
(deftest
"char=? equal"
(assert= true (char=? (make-char 97) (make-char 97))))
(deftest
"char=? unequal"
(assert= false (char=? (make-char 97) (make-char 98))))
(deftest
"char<? ordering"
(assert= true (char<? (make-char 97) (make-char 98))))
(deftest
"char>? ordering"
(assert= true (char>? (make-char 98) (make-char 97))))
(deftest
"char<=? equal"
(assert= true (char<=? (make-char 65) (make-char 65))))
(deftest
"char>=? greater"
(assert= true (char>=? (make-char 90) (make-char 65))))
(deftest
"char-ci=? ignores case (a vs A)"
(assert= true (char-ci=? (make-char 97) (make-char 65))))
(deftest
"char-ci<? a < b case-insensitive"
(assert= true (char-ci<? (make-char 97) (make-char 98))))
(deftest
"char-ci>? b > a case-insensitive"
(assert= true (char-ci>? (make-char 66) (make-char 65))))
(deftest
"char-alphabetic? true for a"
(assert= true (char-alphabetic? (make-char 97))))
(deftest
"char-alphabetic? true for Z"
(assert= true (char-alphabetic? (make-char 90))))
(deftest
"char-alphabetic? false for digit"
(assert= false (char-alphabetic? (make-char 48))))
(deftest
"char-numeric? true for 0"
(assert= true (char-numeric? (make-char 48))))
(deftest
"char-numeric? true for 9"
(assert= true (char-numeric? (make-char 57))))
(deftest
"char-numeric? false for letter"
(assert= false (char-numeric? (make-char 65))))
(deftest
"char-whitespace? true for space"
(assert= true (char-whitespace? (make-char 32))))
(deftest
"char-whitespace? true for newline"
(assert= true (char-whitespace? (make-char 10))))
(deftest
"char-whitespace? false for letter"
(assert= false (char-whitespace? (make-char 65))))
(deftest
"char-upper-case? true for A"
(assert= true (char-upper-case? (make-char 65))))
(deftest
"char-upper-case? false for a"
(assert= false (char-upper-case? (make-char 97))))
(deftest
"char-lower-case? true for a"
(assert= true (char-lower-case? (make-char 97))))
(deftest
"char-lower-case? false for A"
(assert= false (char-lower-case? (make-char 65))))
(deftest
"char-upcase converts a to A"
(assert= 65 (char->integer (char-upcase (make-char 97)))))
(deftest
"char-downcase converts A to a"
(assert=
97
(char->integer (char-downcase (make-char 65)))))
(deftest
"char-upcase idempotent on uppercase"
(assert= 65 (char->integer (char-upcase (make-char 65)))))
(deftest
"string->list returns list of chars"
(assert= 3 (len (string->list "abc"))))
(deftest
"string->list element 0 is char"
(assert= true (char? (get (string->list "abc") 0))))
(deftest
"string->list codepoints correct"
(assert= 97 (char->integer (get (string->list "abc") 0))))
(deftest
"list->string from chars produces string"
(assert=
"abc"
(list->string
(list
(make-char 97)
(make-char 98)
(make-char 99)))))
(deftest
"string->list list->string round-trip"
(let ((s "hello")) (assert= s (list->string (string->list s)))))
(deftest
"char literal parsed via sx-parse"
(let
((ast (sx-parse "#\\a")))
(assert= true (char? (get ast 0)))))
(deftest
"char literal codepoint via sx-parse"
(let
((ast (sx-parse "#\\a")))
(assert= 97 (char->integer (get ast 0)))))
(deftest
"named char space via sx-parse"
(let
((ast (sx-parse "#\\space")))
(assert= 32 (char->integer (get ast 0)))))
(deftest
"named char newline via sx-parse"
(let
((ast (sx-parse "#\\newline")))
(assert= 10 (char->integer (get ast 0)))))
(deftest
"char-ci<=? equal case-insensitive"
(assert= true (char-ci<=? (make-char 65) (make-char 97))))
(deftest
"char-ci>=? equal case-insensitive"
(assert= true (char-ci>=? (make-char 97) (make-char 65))))

View File

@@ -1,305 +0,0 @@
(import (sx coroutines))
(defsuite
"coroutine"
(deftest
"coroutine? recognizes coroutine objects"
(let
((co (make-coroutine (fn () nil))))
(assert (coroutine? co))
(assert= false (coroutine? 42))
(assert= false (coroutine? "hello"))
(assert= false (coroutine? nil))
(assert= false (coroutine? (list)))))
(deftest
"coroutine-alive? true for ready coroutine"
(let
((co (make-coroutine (fn () nil))))
(assert (coroutine-alive? co))))
(deftest
"coroutine-alive? false for non-coroutine"
(assert= false (coroutine-alive? 42)))
(deftest
"immediate return — done true, value is body result"
(let
((co (make-coroutine (fn () 42))))
(let
((r (coroutine-resume co nil)))
(assert= true (get r "done"))
(assert= 42 (get r "value")))))
(deftest
"immediate nil return"
(let
((co (make-coroutine (fn () nil))))
(let
((r (coroutine-resume co nil)))
(assert= true (get r "done"))
(assert= nil (get r "value")))))
(deftest
"coroutine-alive? false after completion"
(let
((co (make-coroutine (fn () nil))))
(coroutine-resume co nil)
(assert= false (coroutine-alive? co))))
(deftest
"single yield — done false on yield, done true on finish"
(let
((co (make-coroutine (fn () (coroutine-yield 10) 20))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(assert= false (get r1 "done"))
(assert= 10 (get r1 "value"))
(assert= true (get r2 "done"))
(assert= 20 (get r2 "value"))))))
(deftest
"coroutine-alive? true between yield and next resume"
(let
((co (make-coroutine (fn () (coroutine-yield nil) nil))))
(assert (coroutine-alive? co))
(coroutine-resume co nil)
(assert (coroutine-alive? co))
(coroutine-resume co nil)
(assert= false (coroutine-alive? co))))
(deftest
"three yields then return"
(let
((co (make-coroutine (fn () (coroutine-yield "a") (coroutine-yield "b") (coroutine-yield "c") "z"))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(let
((r3 (coroutine-resume co nil)))
(let
((r4 (coroutine-resume co nil)))
(assert= "a" (get r1 "value"))
(assert= false (get r1 "done"))
(assert= "b" (get r2 "value"))
(assert= false (get r2 "done"))
(assert= "c" (get r3 "value"))
(assert= false (get r3 "done"))
(assert= "z" (get r4 "value"))
(assert= true (get r4 "done"))))))))
(deftest
"final return vs yield — done flag distinguishes them"
(let
((co (make-coroutine (fn () (coroutine-yield "yielded") "returned"))))
(let
((y (coroutine-resume co nil)))
(let
((r (coroutine-resume co nil)))
(assert= false (get y "done"))
(assert= "yielded" (get y "value"))
(assert= true (get r "done"))
(assert= "returned" (get r "value"))))))
(deftest
"resume val becomes yield return value"
(let
((co (make-coroutine (fn () (let ((received (coroutine-yield "first"))) received)))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co 99)))
(assert= "first" (get r1 "value"))
(assert= false (get r1 "done"))
(assert= 99 (get r2 "value"))
(assert= true (get r2 "done"))))))
(deftest
"multiple resume values passed through yields"
(let
((co (make-coroutine (fn () (let ((a (coroutine-yield 1))) (let ((b (coroutine-yield 2))) (+ a b)))))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co 10)))
(let
((r3 (coroutine-resume co 20)))
(assert= 1 (get r1 "value"))
(assert= 2 (get r2 "value"))
(assert= true (get r3 "done"))
(assert= 30 (get r3 "value")))))))
(deftest
"coroutine captures lexical environment"
(let
((x 10)
(co
(make-coroutine
(fn () (coroutine-yield (* x 2)) (* x 3)))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(assert= 20 (get r1 "value"))
(assert= 30 (get r2 "value"))))))
(deftest
"resuming dead coroutine raises error"
(let
((co (make-coroutine (fn () nil))))
(coroutine-resume co nil)
(assert-throws (fn () (coroutine-resume co nil)))))
(deftest
"coroutine drives iteration via recursive body"
(let
((co (make-coroutine (fn () (define loop (fn (i) (when (< i 4) (coroutine-yield i) (loop (+ i 1))))) (loop 0))))
(results (list)))
(let
drive
()
(let
((r (coroutine-resume co nil)))
(when
(not (get r "done"))
(append! results (get r "value"))
(drive))))
(assert= 4 (len results))
(assert= 0 (nth results 0))
(assert= 1 (nth results 1))
(assert= 2 (nth results 2))
(assert= 3 (nth results 3))))
(deftest
"nested coroutine — inner resumed from outer body"
(let
((inner (make-coroutine (fn () (coroutine-yield "inner-a") "inner-done")))
(outer
(make-coroutine
(fn
()
(let
((i1 (coroutine-resume inner nil)))
(coroutine-yield (get i1 "value")))
(let ((i2 (coroutine-resume inner nil))) (get i2 "value"))))))
(let
((o1 (coroutine-resume outer nil)))
(let
((o2 (coroutine-resume outer nil)))
(assert= false (get o1 "done"))
(assert= "inner-a" (get o1 "value"))
(assert= true (get o2 "done"))
(assert= "inner-done" (get o2 "value"))))))
(deftest
"two independent coroutines interleave correctly"
(let
((co1 (make-coroutine (fn () (coroutine-yield 1) 5)))
(co2
(make-coroutine (fn () (coroutine-yield 2) 6))))
(let
((a (coroutine-resume co1 nil)))
(let
((b (coroutine-resume co2 nil)))
(let
((c (coroutine-resume co1 nil)))
(let
((d (coroutine-resume co2 nil)))
(assert= false (get a "done"))
(assert= 1 (get a "value"))
(assert= false (get b "done"))
(assert= 2 (get b "value"))
(assert= true (get c "done"))
(assert= 5 (get c "value"))
(assert= true (get d "done"))
(assert= 6 (get d "value"))))))))
(deftest
"coroutine state field is ready before first resume"
(let
((co (make-coroutine (fn () (coroutine-yield 1)))))
(assert= "ready" (get co "state"))))
(deftest
"coroutine state field is suspended between yields"
(let
((co (make-coroutine (fn () (coroutine-yield 1) 2))))
(coroutine-resume co nil)
(assert= "suspended" (get co "state"))))
(deftest
"coroutine state field is dead after completion"
(let
((co (make-coroutine (fn () nil))))
(coroutine-resume co nil)
(assert= "dead" (get co "state"))))
(deftest
"yield works when called from nested helper function"
(let
((co (make-coroutine (fn () (define helper (fn (x) (coroutine-yield x))) (helper 10) (helper 20)))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(let
((r3 (coroutine-resume co nil)))
(assert= false (get r1 "done"))
(assert= 10 (get r1 "value"))
(assert= false (get r2 "done"))
(assert= 20 (get r2 "value"))
(assert= true (get r3 "done")))))))
(deftest
"initial resume argument is ignored by ready coroutine"
(let
((co (make-coroutine (fn () (coroutine-yield 42)))))
(let
((r (coroutine-resume co "ignored")))
(assert= false (get r "done"))
(assert= 42 (get r "value")))))
(deftest
"coroutine with mutable closure state"
(let
((counter {:value 0}))
(let
((co (make-coroutine (fn () (dict-set! counter "value" 1) (coroutine-yield "a") (dict-set! counter "value" 2) (coroutine-yield "b")))))
(assert= 0 (get counter "value"))
(coroutine-resume co nil)
(assert= 1 (get counter "value"))
(coroutine-resume co nil)
(assert= 2 (get counter "value")))))
(deftest
"coroutine can yield complex values"
(let
((co (make-coroutine (fn () (coroutine-yield (list 1 2 3)) (coroutine-yield {:key "val"})))))
(let
((r1 (coroutine-resume co nil)))
(let
((r2 (coroutine-resume co nil)))
(assert= false (get r1 "done"))
(assert= 3 (len (get r1 "value")))
(assert= false (get r2 "done"))
(assert= "val" (get (get r2 "value") "key"))))))
(deftest
"round-robin scheduling of multiple coroutines"
(let
((results (list))
(co1
(make-coroutine
(fn () (coroutine-yield "a") (coroutine-yield "b"))))
(co2
(make-coroutine
(fn () (coroutine-yield "c") (coroutine-yield "d")))))
(append! results (get (coroutine-resume co1 nil) "value"))
(append! results (get (coroutine-resume co2 nil) "value"))
(append! results (get (coroutine-resume co1 nil) "value"))
(append! results (get (coroutine-resume co2 nil) "value"))
(assert= 4 (len results))
(assert= "a" (nth results 0))
(assert= "c" (nth results 1))
(assert= "b" (nth results 2))
(assert= "d" (nth results 3))))
(deftest
"coroutines created from same factory share no state"
(let
((make-counter (fn (start) (make-coroutine (fn () (define loop (fn (n) (coroutine-yield n) (loop (+ n 1)))) (loop start))))))
(let
((c1 (make-counter 0)) (c2 (make-counter 100)))
(let
((a (get (coroutine-resume c1 nil) "value")))
(let
((b (get (coroutine-resume c2 nil) "value")))
(let
((c (get (coroutine-resume c1 nil) "value")))
(let
((d (get (coroutine-resume c2 nil) "value")))
(assert= 0 a)
(assert= 100 b)
(assert= 1 c)
(assert= 101 d))))))))
(deftest
"resuming non-coroutine raises error"
(assert-throws (fn () (coroutine-resume "not-a-coroutine" nil)))))

View File

@@ -1,113 +0,0 @@
;; Tests for dynamic-wind: after-thunk fires on normal return,
;; non-local exit via raise/guard, and call/cc escape.
(defsuite
"dynamic-wind-basic"
(deftest
"after fires on normal return"
(let
((log (list)))
(dynamic-wind
(fn () (append! log "before"))
(fn () (append! log "body"))
(fn () (append! log "after")))
(assert= 3 (len log))
(assert= "before" (nth log 0))
(assert= "body" (nth log 1))
(assert= "after" (nth log 2))))
(deftest
"after fires on raise escape"
(let
((log (list)))
(guard
(e (true nil))
(dynamic-wind
(fn () (append! log "before"))
(fn () (append! log "body") (error "boom"))
(fn () (append! log "after"))))
(assert= 3 (len log))
(assert= "before" (nth log 0))
(assert= "body" (nth log 1))
(assert= "after" (nth log 2))))
(deftest
"after fires on call/cc escape"
(let
((log (list)))
(call/cc
(fn
(k)
(dynamic-wind
(fn () (append! log "before"))
(fn () (append! log "body") (k nil))
(fn () (append! log "after")))))
(assert= 3 (len log))
(assert= "before" (nth log 0))
(assert= "body" (nth log 1))
(assert= "after" (nth log 2))))
(deftest
"nested dynamic-wind after-thunks fire LIFO on normal return"
(let
((log (list)))
(dynamic-wind
(fn () (append! log "outer-before"))
(fn
()
(dynamic-wind
(fn () (append! log "inner-before"))
(fn () (append! log "inner-body"))
(fn () (append! log "inner-after"))))
(fn () (append! log "outer-after")))
(assert= 5 (len log))
(assert= "outer-before" (nth log 0))
(assert= "inner-before" (nth log 1))
(assert= "inner-body" (nth log 2))
(assert= "inner-after" (nth log 3))
(assert= "outer-after" (nth log 4))))
(deftest
"nested dynamic-wind after-thunks fire LIFO on raise"
(let
((log (list)))
(guard
(e (true nil))
(dynamic-wind
(fn () (append! log "outer-before"))
(fn
()
(dynamic-wind
(fn () (append! log "inner-before"))
(fn () (append! log "inner-body") (error "boom"))
(fn () (append! log "inner-after"))))
(fn () (append! log "outer-after"))))
(assert= 5 (len log))
(assert= "outer-before" (nth log 0))
(assert= "inner-before" (nth log 1))
(assert= "inner-body" (nth log 2))
(assert= "inner-after" (nth log 3))
(assert= "outer-after" (nth log 4))))
(deftest
"before and after are called"
(let
((count 0))
(dynamic-wind
(fn () (set! count (+ count 1)))
(fn () nil)
(fn () (set! count (+ count 10))))
(assert= 11 count)))
(deftest
"dynamic-wind return value is body result"
(let
((result (dynamic-wind (fn () nil) (fn () 42) (fn () nil))))
(assert= 42 result)))
(deftest
"after fires before guard handler"
(let
((log (list)))
(guard
(e (true (append! log "guard-handler")))
(dynamic-wind
(fn () nil)
(fn () (error "boom"))
(fn () (append! log "after"))))
(assert= 2 (len log))
(assert= "after" (nth log 0))
(assert= "guard-handler" (nth log 1)))))

View File

@@ -10,56 +10,57 @@
;; Literals and types
;; --------------------------------------------------------------------------
(defsuite
"literals"
(deftest
"numbers are numbers"
(defsuite "literals"
(deftest "numbers are numbers"
(assert-type "number" 42)
(assert-type "number" 3.14)
(assert-type "number" -1))
(deftest
"strings are strings"
(deftest "strings are strings"
(assert-type "string" "hello")
(assert-type "string" ""))
(deftest
"booleans are booleans"
(deftest "booleans are booleans"
(assert-type "boolean" true)
(assert-type "boolean" false))
(deftest "nil is nil" (assert-type "nil" nil) (assert-nil nil))
(deftest
"lists are lists"
(deftest "nil is nil"
(assert-type "nil" nil)
(assert-nil nil))
(deftest "lists are lists"
(assert-type "list" (list 1 2 3))
(assert-type "list" (list)))
(deftest "dicts are dicts" (assert-type "dict" {:b 2 :a 1})))
(deftest "dicts are dicts"
(assert-type "dict" {:a 1 :b 2})))
;; --------------------------------------------------------------------------
;; Arithmetic
;; --------------------------------------------------------------------------
(defsuite
"arithmetic"
(deftest
"addition"
(defsuite "arithmetic"
(deftest "addition"
(assert-equal 3 (+ 1 2))
(assert-equal 0 (+ 0 0))
(assert-equal -1 (+ 1 -2))
(assert-equal 10 (+ 1 2 3 4)))
(deftest
"subtraction"
(deftest "subtraction"
(assert-equal 1 (- 3 2))
(assert-equal -1 (- 2 3)))
(deftest
"multiplication"
(deftest "multiplication"
(assert-equal 6 (* 2 3))
(assert-equal 0 (* 0 100))
(assert-equal 24 (* 1 2 3 4)))
(deftest
"division"
(deftest "division"
(assert-equal 2 (/ 6 3))
(assert-equal 2.5 (/ 5 2)))
(deftest
"modulo"
(deftest "modulo"
(assert-equal 1 (mod 7 3))
(assert-equal 0 (mod 6 3))))
@@ -68,26 +69,20 @@
;; Comparison
;; --------------------------------------------------------------------------
(defsuite
"comparison"
(deftest
"equality"
(defsuite "comparison"
(deftest "equality"
(assert-true (= 1 1))
(assert-false (= 1 2))
(assert-true (= "a" "a"))
(assert-false (= "a" "b")))
(deftest
"deep equality"
(assert-true
(equal?
(list 1 2 3)
(list 1 2 3)))
(assert-false
(equal? (list 1 2) (list 1 3)))
(deftest "deep equality"
(assert-true (equal? (list 1 2 3) (list 1 2 3)))
(assert-false (equal? (list 1 2) (list 1 3)))
(assert-true (equal? {:a 1} {:a 1}))
(assert-false (equal? {:a 1} {:a 2})))
(deftest
"ordering"
(deftest "ordering"
(assert-true (< 1 2))
(assert-false (< 2 1))
(assert-true (> 2 1))
@@ -101,36 +96,34 @@
;; String operations
;; --------------------------------------------------------------------------
(defsuite
"strings"
(deftest
"str concatenation"
(defsuite "strings"
(deftest "str concatenation"
(assert-equal "abc" (str "a" "b" "c"))
(assert-equal "hello world" (str "hello" " " "world"))
(assert-equal "42" (str 42))
(assert-equal "" (str)))
(deftest
"string-length"
(deftest "string-length"
(assert-equal 5 (string-length "hello"))
(assert-equal 0 (string-length "")))
(deftest
"substring"
(deftest "substring"
(assert-equal "ell" (substring "hello" 1 4))
(assert-equal "hello" (substring "hello" 0 5)))
(deftest
"string-contains?"
(deftest "string-contains?"
(assert-true (string-contains? "hello world" "world"))
(assert-false (string-contains? "hello" "xyz")))
(deftest
"upcase and downcase"
(deftest "upcase and downcase"
(assert-equal "HELLO" (upcase "hello"))
(assert-equal "hello" (downcase "HELLO")))
(deftest
"trim"
(deftest "trim"
(assert-equal "hello" (trim " hello "))
(assert-equal "hello" (trim "hello")))
(deftest
"split and join"
(deftest "split and join"
(assert-equal (list "a" "b" "c") (split "a,b,c" ","))
(assert-equal "a-b-c" (join "-" (list "a" "b" "c")))))
@@ -139,145 +132,121 @@
;; List operations
;; --------------------------------------------------------------------------
(defsuite
"lists"
(deftest
"constructors"
(assert-equal
(list 1 2 3)
(list 1 2 3))
(defsuite "lists"
(deftest "constructors"
(assert-equal (list 1 2 3) (list 1 2 3))
(assert-equal (list) (list))
(assert-length 3 (list 1 2 3)))
(deftest
"first and rest"
(deftest "first and rest"
(assert-equal 1 (first (list 1 2 3)))
(assert-equal
(list 2 3)
(rest (list 1 2 3)))
(assert-equal (list 2 3) (rest (list 1 2 3)))
(assert-nil (first (list)))
(assert-equal (list) (rest (list))))
(deftest
"nth"
(assert-equal
1
(nth (list 1 2 3) 0))
(assert-equal
2
(nth (list 1 2 3) 1))
(assert-equal
3
(nth (list 1 2 3) 2)))
(deftest
"last"
(deftest "nth"
(assert-equal 1 (nth (list 1 2 3) 0))
(assert-equal 2 (nth (list 1 2 3) 1))
(assert-equal 3 (nth (list 1 2 3) 2)))
(deftest "last"
(assert-equal 3 (last (list 1 2 3)))
(assert-nil (last (list))))
(deftest
"cons and append"
(assert-equal
(list 0 1 2)
(cons 0 (list 1 2)))
(assert-equal
(list 1 2 3 4)
(append (list 1 2) (list 3 4))))
(deftest
"reverse"
(assert-equal
(list 3 2 1)
(reverse (list 1 2 3)))
(deftest "cons and append"
(assert-equal (list 0 1 2) (cons 0 (list 1 2)))
(assert-equal (list 1 2 3 4) (append (list 1 2) (list 3 4))))
(deftest "reverse"
(assert-equal (list 3 2 1) (reverse (list 1 2 3)))
(assert-equal (list) (reverse (list))))
(deftest
"empty?"
(deftest "empty?"
(assert-true (empty? (list)))
(assert-false (empty? (list 1))))
(deftest
"len"
(deftest "len"
(assert-equal 0 (len (list)))
(assert-equal 3 (len (list 1 2 3))))
(deftest
"contains?"
(assert-true
(contains? (list 1 2 3) 2))
(assert-false
(contains? (list 1 2 3) 4)))
(deftest
"flatten"
(assert-equal
(list 1 2 3 4)
(flatten
(list (list 1 2) (list 3 4))))))
(deftest "contains?"
(assert-true (contains? (list 1 2 3) 2))
(assert-false (contains? (list 1 2 3) 4)))
(deftest "flatten"
(assert-equal (list 1 2 3 4) (flatten (list (list 1 2) (list 3 4))))))
;; --------------------------------------------------------------------------
;; Dict operations
;; --------------------------------------------------------------------------
(defsuite
"dicts"
(deftest
"dict literal"
(assert-type "dict" {:b 2 :a 1})
(defsuite "dicts"
(deftest "dict literal"
(assert-type "dict" {:a 1 :b 2})
(assert-equal 1 (get {:a 1} "a"))
(assert-equal 2 (get {:b 2 :a 1} "b")))
(deftest
"assoc"
(assert-equal {:b 2 :a 1} (assoc {:a 1} "b" 2))
(assert-equal 2 (get {:a 1 :b 2} "b")))
(deftest "assoc"
(assert-equal {:a 1 :b 2} (assoc {:a 1} "b" 2))
(assert-equal {:a 99} (assoc {:a 1} "a" 99)))
(deftest "dissoc" (assert-equal {:b 2} (dissoc {:b 2 :a 1} "a")))
(deftest
"keys and vals"
(let
((d {:b 2 :a 1}))
(deftest "dissoc"
(assert-equal {:b 2} (dissoc {:a 1 :b 2} "a")))
(deftest "keys and vals"
(let ((d {:a 1 :b 2}))
(assert-length 2 (keys d))
(assert-length 2 (vals d))
(assert-contains "a" (keys d))
(assert-contains "b" (keys d))))
(deftest
"has-key?"
(deftest "has-key?"
(assert-true (has-key? {:a 1} "a"))
(assert-false (has-key? {:a 1} "b")))
(deftest
"merge"
(assert-equal {:c 3 :b 2 :a 1} (merge {:b 2 :a 1} {:c 3}))
(assert-equal {:b 2 :a 99} (merge {:b 2 :a 1} {:a 99}))))
(deftest "merge"
(assert-equal {:a 1 :b 2 :c 3}
(merge {:a 1 :b 2} {:c 3}))
(assert-equal {:a 99 :b 2}
(merge {:a 1 :b 2} {:a 99}))))
;; --------------------------------------------------------------------------
;; Predicates
;; --------------------------------------------------------------------------
(defsuite
"predicates"
(deftest
"nil?"
(defsuite "predicates"
(deftest "nil?"
(assert-true (nil? nil))
(assert-false (nil? 0))
(assert-false (nil? false))
(assert-false (nil? "")))
(deftest
"number?"
(deftest "number?"
(assert-true (number? 42))
(assert-true (number? 3.14))
(assert-false (number? "42")))
(deftest
"string?"
(deftest "string?"
(assert-true (string? "hello"))
(assert-false (string? 42)))
(deftest
"list?"
(deftest "list?"
(assert-true (list? (list 1 2)))
(assert-false (list? "not a list")))
(deftest
"dict?"
(deftest "dict?"
(assert-true (dict? {:a 1}))
(assert-false (dict? (list 1))))
(deftest
"boolean?"
(deftest "boolean?"
(assert-true (boolean? true))
(assert-true (boolean? false))
(assert-false (boolean? nil))
(assert-false (boolean? 0)))
(deftest
"not"
(deftest "not"
(assert-true (not false))
(assert-true (not nil))
(assert-false (not true))
@@ -289,67 +258,77 @@
;; Special forms
;; --------------------------------------------------------------------------
(defsuite
"special-forms"
(deftest
"if"
(defsuite "special-forms"
(deftest "if"
(assert-equal "yes" (if true "yes" "no"))
(assert-equal "no" (if false "yes" "no"))
(assert-equal "no" (if nil "yes" "no"))
(assert-nil (if false "yes")))
(deftest
"when"
(deftest "when"
(assert-equal "yes" (when true "yes"))
(assert-nil (when false "yes")))
(deftest
"cond"
(deftest "cond"
(assert-equal "a" (cond true "a" :else "b"))
(assert-equal "b" (cond false "a" :else "b"))
(assert-equal "c" (cond false "a" false "b" :else "c")))
(deftest
"cond with 2-element predicate as first test"
(assert-equal "c" (cond
false "a"
false "b"
:else "c")))
(deftest "cond with 2-element predicate as first test"
;; Regression: cond misclassifies Clojure-style as scheme-style when
;; the first test is a 2-element list like (nil? x) or (empty? x).
;; The evaluator checks: is first arg a 2-element list? If yes, treats
;; as scheme-style ((test body) ...) — returning the arg instead of
;; evaluating the predicate call.
(assert-equal 0 (cond (nil? nil) 0 :else 1))
(assert-equal 1 (cond (nil? "x") 0 :else 1))
(assert-equal "empty" (cond (empty? (list)) "empty" :else "not-empty"))
(assert-equal
"not-empty"
(cond (empty? (list 1)) "empty" :else "not-empty"))
(assert-equal "not-empty" (cond (empty? (list 1)) "empty" :else "not-empty"))
(assert-equal "yes" (cond (not false) "yes" :else "no"))
(assert-equal "no" (cond (not true) "yes" :else "no")))
(deftest
"cond with 2-element predicate and no :else"
(assert-equal "found" (cond (nil? nil) "found" (nil? "x") "other"))
(assert-equal "b" (cond (nil? "x") "a" (not false) "b")))
(deftest
"and"
(deftest "cond with 2-element predicate and no :else"
;; Same bug, but without :else — this is the worst case because the
;; bootstrapper heuristic also breaks (all clauses are 2-element lists).
(assert-equal "found"
(cond (nil? nil) "found"
(nil? "x") "other"))
(assert-equal "b"
(cond (nil? "x") "a"
(not false) "b")))
(deftest "and"
(assert-true (and true true))
(assert-false (and true false))
(assert-false (and false true))
(assert-equal 3 (and 1 2 3)))
(deftest
"or"
(deftest "or"
(assert-equal 1 (or 1 2))
(assert-equal 2 (or false 2))
(assert-equal "fallback" (or nil false "fallback"))
(assert-false (or false false)))
(deftest
"let"
(assert-equal
3
(let ((x 1) (y 2)) (+ x y)))
(assert-equal
"hello world"
(deftest "let"
(assert-equal 3 (let ((x 1) (y 2)) (+ x y)))
(assert-equal "hello world"
(let ((a "hello") (b " world")) (str a b))))
(deftest
"let clojure-style"
(deftest "let clojure-style"
(assert-equal 3 (let (x 1 y 2) (+ x y))))
(deftest
"do / begin"
(deftest "do / begin"
(assert-equal 3 (do 1 2 3))
(assert-equal "last" (begin "first" "middle" "last")))
(deftest "define" (define x 42) (assert-equal 42 x))
(deftest
"set!"
(deftest "define"
(define x 42)
(assert-equal 42 x))
(deftest "set!"
(define x 1)
(set! x 2)
(assert-equal 2 x)))
@@ -359,126 +338,86 @@
;; Lambda and closures
;; --------------------------------------------------------------------------
(defsuite
"lambdas"
(deftest
"basic lambda"
(let
((add (fn (a b) (+ a b))))
(defsuite "lambdas"
(deftest "basic lambda"
(let ((add (fn (a b) (+ a b))))
(assert-equal 3 (add 1 2))))
(deftest
"closure captures env"
(let
((x 10))
(let
((add-x (fn (y) (+ x y))))
(deftest "closure captures env"
(let ((x 10))
(let ((add-x (fn (y) (+ x y))))
(assert-equal 15 (add-x 5)))))
(deftest
"lambda as argument"
(assert-equal
(list 2 4 6)
(map
(fn (x) (* x 2))
(list 1 2 3))))
(deftest
"recursive lambda via define"
(define
factorial
(fn
(n)
(if
(<= n 1)
1
(* n (factorial (- n 1))))))
(deftest "lambda as argument"
(assert-equal (list 2 4 6)
(map (fn (x) (* x 2)) (list 1 2 3))))
(deftest "recursive lambda via define"
(define factorial
(fn (n) (if (<= n 1) 1 (* n (factorial (- n 1))))))
(assert-equal 120 (factorial 5)))
(deftest
"higher-order returns lambda"
(let
((make-adder (fn (n) (fn (x) (+ n x)))))
(let
((add5 (make-adder 5)))
(deftest "higher-order returns lambda"
(let ((make-adder (fn (n) (fn (x) (+ n x)))))
(let ((add5 (make-adder 5)))
(assert-equal 8 (add5 3)))))
(deftest
"multi-body lambda returns last value"
(let
((f (fn (x) (+ x 1) (+ x 2) (+ x 3))))
(deftest "multi-body lambda returns last value"
;; All body expressions must execute. Return value is the last.
;; Catches: sf-lambda using nth(args,1) instead of rest(args).
(let ((f (fn (x) (+ x 1) (+ x 2) (+ x 3))))
(assert-equal 13 (f 10))))
(deftest
"multi-body lambda side effects via dict mutation"
(let
((state (dict "a" 0 "b" 0)))
(let
((f (fn () (dict-set! state "a" 1) (dict-set! state "b" 2) "done")))
(deftest "multi-body lambda side effects via dict mutation"
;; Verify all body expressions run by mutating a shared dict.
(let ((state (dict "a" 0 "b" 0)))
(let ((f (fn ()
(dict-set! state "a" 1)
(dict-set! state "b" 2)
"done")))
(assert-equal "done" (f))
(assert-equal 1 (get state "a"))
(assert-equal 2 (get state "b")))))
(deftest
"multi-body lambda two expressions"
(assert-equal
20
(deftest "multi-body lambda two expressions"
;; Simplest case: two body expressions, return value is second.
(assert-equal 20
((fn (x) (+ x 1) (* x 2)) 10))
(assert-equal 42 ((fn () (+ 1 2) 42)))))
;; And with zero-arg lambda
(assert-equal 42
((fn () (+ 1 2) 42)))))
;; --------------------------------------------------------------------------
;; Higher-order forms
;; --------------------------------------------------------------------------
(defsuite
"higher-order"
(deftest
"map"
(assert-equal
(list 2 4 6)
(map
(fn (x) (* x 2))
(list 1 2 3)))
(defsuite "higher-order"
(deftest "map"
(assert-equal (list 2 4 6)
(map (fn (x) (* x 2)) (list 1 2 3)))
(assert-equal (list) (map (fn (x) x) (list))))
(deftest
"filter"
(assert-equal
(list 2 4)
(filter
(fn (x) (= (mod x 2) 0))
(list 1 2 3 4)))
(assert-equal
(list)
(deftest "filter"
(assert-equal (list 2 4)
(filter (fn (x) (= (mod x 2) 0)) (list 1 2 3 4)))
(assert-equal (list)
(filter (fn (x) false) (list 1 2 3))))
(deftest
"reduce"
(assert-equal
10
(reduce
(fn (acc x) (+ acc x))
0
(list 1 2 3 4)))
(assert-equal
0
(reduce (fn (acc x) (+ acc x)) 0 (list))))
(deftest
"some"
(assert-true
(some
(fn (x) (> x 3))
(list 1 2 3 4 5)))
(assert-false
(some
(fn (x) (> x 10))
(list 1 2 3))))
(deftest
"every?"
(assert-true
(every?
(fn (x) (> x 0))
(list 1 2 3)))
(assert-false
(every?
(fn (x) (> x 2))
(list 1 2 3))))
(deftest
"map-indexed"
(assert-equal
(list "0:a" "1:b" "2:c")
(deftest "reduce"
(assert-equal 10 (reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4)))
(assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list))))
(deftest "some"
(assert-true (some (fn (x) (> x 3)) (list 1 2 3 4 5)))
(assert-false (some (fn (x) (> x 10)) (list 1 2 3))))
(deftest "every?"
(assert-true (every? (fn (x) (> x 0)) (list 1 2 3)))
(assert-false (every? (fn (x) (> x 2)) (list 1 2 3))))
(deftest "map-indexed"
(assert-equal (list "0:a" "1:b" "2:c")
(map-indexed (fn (i x) (str i ":" x)) (list "a" "b" "c")))))
@@ -486,39 +425,49 @@
;; Components
;; --------------------------------------------------------------------------
(defsuite
"components"
(deftest
"defcomp creates component"
(defcomp ~test-comp (&key title) (div title))
(defsuite "components"
(deftest "defcomp creates component"
(defcomp ~test-comp (&key title)
(div title))
(assert-true (not (nil? ~test-comp))))
(deftest
"component renders with keyword args"
(defcomp ~greeting (&key name) (span (str "Hello, " name "!")))
(deftest "component renders with keyword args"
(defcomp ~greeting (&key name)
(span (str "Hello, " name "!")))
(assert-true (not (nil? ~greeting))))
(deftest
"component with children"
(defcomp ~box (&key &rest children) (div :class "box" children))
(deftest "component with children"
(defcomp ~box (&key &rest children)
(div :class "box" children))
(assert-true (not (nil? ~box))))
(deftest
"component with default via or"
(defcomp ~label (&key text) (span (or text "default")))
(deftest "component with default via or"
(defcomp ~label (&key text)
(span (or text "default")))
(assert-true (not (nil? ~label))))
(deftest
"defcomp default affinity is auto"
(defcomp ~aff-default (&key x) (div x))
(deftest "defcomp default affinity is auto"
(defcomp ~aff-default (&key x)
(div x))
(assert-equal "auto" (component-affinity ~aff-default)))
(deftest
"defcomp affinity client"
(defcomp ~aff-client (&key x) :affinity :client (div x))
(deftest "defcomp affinity client"
(defcomp ~aff-client (&key x)
:affinity :client
(div x))
(assert-equal "client" (component-affinity ~aff-client)))
(deftest
"defcomp affinity server"
(defcomp ~aff-server (&key x) :affinity :server (div x))
(deftest "defcomp affinity server"
(defcomp ~aff-server (&key x)
:affinity :server
(div x))
(assert-equal "server" (component-affinity ~aff-server)))
(deftest
"defcomp affinity preserves body"
(defcomp ~aff-body (&key val) :affinity :client (span val))
(deftest "defcomp affinity preserves body"
(defcomp ~aff-body (&key val)
:affinity :client
(span val))
;; Component should still render correctly
(assert-equal "client" (component-affinity ~aff-body))
(assert-true (not (nil? ~aff-body)))))
@@ -527,98 +476,93 @@
;; Macros
;; --------------------------------------------------------------------------
(defsuite
"macros"
(deftest
"defmacro creates macro"
(defmacro
unless
(cond &rest body)
(quasiquote (if (not (unquote cond)) (do (splice-unquote body)))))
(defsuite "macros"
(deftest "defmacro creates macro"
(defmacro unless (cond &rest body)
`(if (not ,cond) (do ,@body)))
(assert-equal "yes" (unless false "yes"))
(assert-nil (unless true "no")))
(deftest
"quasiquote and unquote"
(let
((x 42))
(assert-equal
(list 1 42 3)
(quasiquote (1 (unquote x) 3)))))
(deftest
"splice-unquote"
(let
((xs (list 2 3 4)))
(assert-equal
(list 1 2 3 4 5)
(quasiquote (1 (splice-unquote xs) 5))))))
(deftest "quasiquote and unquote"
(let ((x 42))
(assert-equal (list 1 42 3) `(1 ,x 3))))
(deftest "splice-unquote"
(let ((xs (list 2 3 4)))
(assert-equal (list 1 2 3 4 5) `(1 ,@xs 5)))))
;; --------------------------------------------------------------------------
;; Threading macro
;; --------------------------------------------------------------------------
(defsuite
"threading"
(deftest
"thread-first"
(defsuite "threading"
(deftest "thread-first"
(assert-equal 8 (-> 5 (+ 1) (+ 2)))
(assert-equal "HELLO" (-> "hello" upcase))
(assert-equal "HELLO WORLD" (-> "hello" (str " world") upcase))))
(assert-equal "HELLO WORLD"
(-> "hello"
(str " world")
upcase))))
;; --------------------------------------------------------------------------
;; Truthiness
;; --------------------------------------------------------------------------
(defsuite
"truthiness"
(deftest
"truthy values"
(defsuite "truthiness"
(deftest "truthy values"
(assert-true (if 1 true false))
(assert-true (if "x" true false))
(assert-true (if (list 1) true false))
(assert-true (if true true false)))
(deftest
"falsy values"
(deftest "falsy values"
(assert-false (if false true false))
(assert-false (if nil true false))))
(assert-false (if nil true false)))
;; NOTE: empty list, zero, and empty string truthiness is
;; platform-dependent. Python treats all three as falsy.
;; JavaScript treats [] as truthy but 0 and "" as falsy.
;; These tests are omitted — each bootstrapper should emit
;; platform-specific truthiness tests instead.
)
;; --------------------------------------------------------------------------
;; Edge cases and regression tests
;; --------------------------------------------------------------------------
(defsuite
"edge-cases"
(deftest
"nested let scoping"
(let
((x 1))
(let ((x 2)) (assert-equal 2 x))))
(deftest
"recursive map"
(assert-equal
(list (list 2 4) (list 6 8))
(map
(fn (sub) (map (fn (x) (* x 2)) sub))
(list (list 1 2) (list 3 4)))))
(deftest
"keyword as value"
(defsuite "edge-cases"
(deftest "nested let scoping"
(let ((x 1))
(let ((x 2))
(assert-equal 2 x))
;; outer x should be unchanged by inner let
;; (this tests that let creates a new scope)
))
(deftest "recursive map"
(assert-equal (list (list 2 4) (list 6 8))
(map (fn (sub) (map (fn (x) (* x 2)) sub))
(list (list 1 2) (list 3 4)))))
(deftest "keyword as value"
(assert-equal "class" :class)
(assert-equal "id" :id))
(deftest
"dict with evaluated values"
(let ((x 42)) (assert-equal 42 (get {:val x} "val"))))
(deftest
"nil propagation"
(deftest "dict with evaluated values"
(let ((x 42))
(assert-equal 42 (get {:val x} "val"))))
(deftest "nil propagation"
(assert-nil (get {:a 1} "missing"))
(assert-equal "default" (or (get {:a 1} "missing") "default")))
(deftest
"empty operations"
(deftest "empty operations"
(assert-equal (list) (map (fn (x) x) (list)))
(assert-equal (list) (filter (fn (x) true) (list)))
(assert-equal
0
(reduce (fn (acc x) (+ acc x)) 0 (list)))
(assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list)))
(assert-equal 0 (len (list)))
(assert-equal "" (str))))

View File

@@ -1,90 +0,0 @@
;; ==========================================================================
;; test-format.sx — Tests for CL-style format function
;; ==========================================================================
;; --------------------------------------------------------------------------
;; basic directives
;; --------------------------------------------------------------------------
(defsuite
"format:basic"
(deftest "format returns string" (assert (string? (format "hello"))))
(deftest
"format no directives"
(assert= (format "hello world") "hello world"))
(deftest "format empty template" (assert= (format "") ""))
(deftest "~a display string" (assert= (format "~a" "hello") "hello"))
(deftest "~a display number" (assert= (format "~a" 42) "42"))
(deftest "~a display nil" (assert= (format "~a" nil) "()"))
(deftest
"~s write string (with quotes)"
(assert= (format "~s" "hi") "\"hi\""))
(deftest "~s write number" (assert= (format "~s" 42) "42"))
(deftest
"multiple args"
(assert= (format "~a and ~a" "foo" "bar") "foo and bar")))
;; --------------------------------------------------------------------------
;; numeric directives
;; --------------------------------------------------------------------------
(defsuite
"format:numeric"
(deftest "~d decimal" (assert= (format "~d" 255) "255"))
(deftest "~x hex" (assert= (format "~x" 255) "ff"))
(deftest "~o octal" (assert= (format "~o" 8) "10"))
(deftest "~b binary" (assert= (format "~b" 10) "1010"))
(deftest "~d zero" (assert= (format "~d" 0) "0"))
(deftest
"~x uppercase digits"
(assert= (format "value: ~x" 16) "value: 10")))
;; --------------------------------------------------------------------------
;; float directives
;; --------------------------------------------------------------------------
(defsuite
"format:float"
(deftest "~f fixed point" (assert= (format "~f" 3.14) "3.140000"))
(deftest "~f zero" (assert= (format "~f" 0) "0.000000")))
;; --------------------------------------------------------------------------
;; control directives
;; --------------------------------------------------------------------------
(defsuite
"format:control"
(deftest "~% newline" (assert= (format "a~%b") "a\nb"))
(deftest "~~ literal tilde" (assert= (format "100~~") "100~"))
(deftest "~t tab" (assert= (format "a~tb") "a\tb"))
(deftest "~& fresh line at start" (assert= (format "~&hello") "\nhello"))
(deftest
"~& no newline if already at newline"
(assert= (format "line~%~&next") "line\nnext")))
;; --------------------------------------------------------------------------
;; mixed / compound
;; --------------------------------------------------------------------------
(defsuite
"format:compound"
(deftest
"name and age"
(assert=
(format "Hello ~a, age ~d" "Alice" 30)
"Hello Alice, age 30"))
(deftest
"hex dump style"
(assert=
(format "~d = 0x~x = 0b~b" 10 10 10)
"10 = 0xa = 0b1010"))
(deftest "multiple newlines" (assert= (format "~%~%") "\n\n"))
(deftest "text with no args" (assert= (format "status: ok") "status: ok"))
(deftest
"tilde at end (unknown directive)"
(assert (string? (format "test~"))))
(deftest
"nested strings in ~a"
(assert=
(format "got: ~a" (list 1 2 3))
"got: (1 2 3)")))

View File

@@ -1,78 +0,0 @@
(defsuite
"gensym"
(deftest "gensym returns a symbol" (assert= true (symbol? (gensym))))
(deftest
"gensym default prefix is g"
(let
((s (symbol-name (gensym))))
(assert= true (string-contains? s "g"))))
(deftest
"gensym with prefix uses that prefix"
(let
((s (symbol-name (gensym "var"))))
(assert= "var" (substring s 0 3))))
(deftest
"gensym produces unique symbols"
(let
((a (gensym)) (b (gensym)))
(assert= false (= (symbol-name a) (symbol-name b)))))
(deftest
"gensym same prefix produces unique symbols"
(let
((a (gensym "x")) (b (gensym "x")) (c (gensym "x")))
(assert= false (= (symbol-name a) (symbol-name b)))
(assert= false (= (symbol-name b) (symbol-name c)))))
(deftest
"gensym counter increases: names differ"
(let
((a (gensym "k")) (b (gensym "k")))
(assert= false (= (symbol-name a) (symbol-name b)))))
(deftest
"gensym no-arg and prefix-arg both unique"
(let
((a (gensym)) (b (gensym "g")))
(assert= false (= (symbol-name a) (symbol-name b)))))
(deftest
"string->symbol returns a symbol"
(assert= true (symbol? (string->symbol "hello"))))
(deftest
"string->symbol symbol has correct name"
(assert= "hello" (symbol-name (string->symbol "hello"))))
(deftest
"string->symbol empty string"
(assert= true (symbol? (string->symbol ""))))
(deftest
"symbol->string returns a string"
(assert= true (string? (symbol->string (quote foo)))))
(deftest
"symbol->string round-trips with string->symbol"
(assert= "hello" (symbol->string (string->symbol "hello"))))
(deftest
"string->symbol/symbol->string round-trip"
(let
((sym (string->symbol "my-var")))
(assert= "my-var" (symbol->string sym))))
(deftest
"intern returns a symbol"
(assert= true (symbol? (intern "foo"))))
(deftest
"intern same as string->symbol"
(assert= "bar" (symbol-name (intern "bar"))))
(deftest
"symbol-interned? true for literal symbols"
(assert= true (symbol-interned? (quote hello))))
(deftest
"symbol-interned? true for gensym'd symbol"
(assert= true (symbol-interned? (gensym "g"))))
(deftest
"symbol-interned? true for string->symbol"
(assert= true (symbol-interned? (string->symbol "test"))))
(deftest
"multiple gensym calls all unique"
(let
((syms (map (fn (i) (gensym "t")) (in-range 5))))
(let
((names (map symbol-name syms)))
(let
((unique-names (reduce (fn (acc n) (if (some (fn (x) (= x n)) acc) acc (cons n acc))) (list) names)))
(assert-equal 5 (len unique-names)))))))

View File

@@ -1,166 +0,0 @@
;; Tests for mutable hash tables (Phase 10)
(defsuite
"hash-table"
(deftest
"make-hash-table returns a hash table"
(assert (hash-table? (make-hash-table))))
(deftest
"hash-table? false for dict"
(assert= false (hash-table? {:a 1})))
(deftest "hash-table? false for nil" (assert= false (hash-table? nil)))
(deftest
"hash-table? false for list"
(assert= false (hash-table? (list 1 2))))
(deftest
"empty table has size 0"
(assert= 0 (hash-table-size (make-hash-table))))
(deftest
"size after one set"
(let
((ht (make-hash-table)))
(hash-table-set! ht "a" 1)
(assert= 1 (hash-table-size ht))))
(deftest
"size after multiple sets"
(let
((ht (make-hash-table)))
(hash-table-set! ht "a" 1)
(hash-table-set! ht "b" 2)
(hash-table-set! ht "c" 3)
(assert= 3 (hash-table-size ht))))
(deftest
"set same key does not grow size"
(let
((ht (make-hash-table)))
(hash-table-set! ht "a" 1)
(hash-table-set! ht "a" 2)
(assert= 1 (hash-table-size ht))))
(deftest
"ref returns set value"
(let
((ht (make-hash-table)))
(hash-table-set! ht "k" 42)
(assert= 42 (hash-table-ref ht "k"))))
(deftest
"ref returns updated value after overwrite"
(let
((ht (make-hash-table)))
(hash-table-set! ht "k" 1)
(hash-table-set! ht "k" 99)
(assert= 99 (hash-table-ref ht "k"))))
(deftest
"ref with default returns default for missing key"
(assert=
"fallback"
(hash-table-ref (make-hash-table) "missing" "fallback")))
(deftest
"ref with default returns value when key exists"
(let
((ht (make-hash-table)))
(hash-table-set! ht "x" 7)
(assert= 7 (hash-table-ref ht "x" 0))))
(deftest
"keyword keys work"
(let
((ht (make-hash-table)))
(hash-table-set! ht :foo "bar")
(assert= "bar" (hash-table-ref ht :foo))))
(deftest
"number keys work"
(let
((ht (make-hash-table)))
(hash-table-set! ht 0 "zero")
(assert= "zero" (hash-table-ref ht 0))))
(deftest
"delete removes key"
(let
((ht (make-hash-table)))
(hash-table-set! ht "x" 1)
(hash-table-delete! ht "x")
(assert= "gone" (hash-table-ref ht "x" "gone"))))
(deftest
"delete reduces size"
(let
((ht (make-hash-table)))
(hash-table-set! ht "a" 1)
(hash-table-set! ht "b" 2)
(hash-table-delete! ht "a")
(assert= 1 (hash-table-size ht))))
(deftest
"delete missing key is no-op"
(let
((ht (make-hash-table)))
(hash-table-delete! ht "absent")
(assert= 0 (hash-table-size ht))))
(deftest
"keys of empty table is empty"
(assert (empty? (hash-table-keys (make-hash-table)))))
(deftest
"keys has correct count"
(let
((ht (make-hash-table)))
(hash-table-set! ht "a" 1)
(hash-table-set! ht "b" 2)
(assert= 2 (len (hash-table-keys ht)))))
(deftest
"values has correct count"
(let
((ht (make-hash-table)))
(hash-table-set! ht "a" 10)
(hash-table-set! ht "b" 20)
(assert= 2 (len (hash-table-values ht)))))
(deftest
"alist of empty table is empty"
(assert (empty? (hash-table->alist (make-hash-table)))))
(deftest
"alist has correct length"
(let
((ht (make-hash-table)))
(hash-table-set! ht "x" 1)
(hash-table-set! ht "y" 2)
(assert= 2 (len (hash-table->alist ht)))))
(deftest
"for-each visits all entries"
(let
((ht (make-hash-table)) (count 0))
(hash-table-set! ht "a" 1)
(hash-table-set! ht "b" 2)
(hash-table-set! ht "c" 3)
(hash-table-for-each ht (fn (k v) (set! count (+ count 1))))
(assert= 3 count)))
(deftest
"for-each sums values"
(let
((ht (make-hash-table)) (total 0))
(hash-table-set! ht "a" 10)
(hash-table-set! ht "b" 20)
(hash-table-set! ht "c" 30)
(hash-table-for-each ht (fn (k v) (set! total (+ total v))))
(assert= 60 total)))
(deftest
"merge copies entries from src to dst"
(let
((dst (make-hash-table)) (src (make-hash-table)))
(hash-table-set! src "x" 1)
(hash-table-set! src "y" 2)
(hash-table-merge! dst src)
(assert= 2 (hash-table-size dst))))
(deftest
"merge overwrites existing keys in dst"
(let
((dst (make-hash-table)) (src (make-hash-table)))
(hash-table-set! dst "k" "old")
(hash-table-set! src "k" "new")
(hash-table-merge! dst src)
(assert= "new" (hash-table-ref dst "k"))))
(deftest
"merge does not modify src"
(let
((dst (make-hash-table)) (src (make-hash-table)))
(hash-table-set! src "a" 1)
(hash-table-merge! dst src)
(assert= 1 (hash-table-size src))))
(deftest
"type-of returns hash-table"
(assert= "hash-table" (type-of (make-hash-table)))))

View File

@@ -88,27 +88,6 @@
(raise _e))))
(handler me-val))))))
;; Evaluate a hyperscript expression, catch the first error raised, and
;; return its message string. Used by runtimeErrors tests.
;; Returns nil if no error is raised (test would then fail equality).
(define eval-hs-error
(fn (src)
(let ((sx (hs-to-sx (hs-compile src))))
(let ((handler (eval-expr-cek
(list (quote fn) (list (quote me))
(list (quote let) (list (list (quote it) nil) (list (quote event) nil)) sx)))))
(guard
(_e
(true
(if
(string? _e)
_e
(if
(and (list? _e) (= (first _e) "hs-return"))
nil
(str _e)))))
(begin (handler nil) nil))))))
;; ── add (19 tests) ──
(defsuite "hs-upstream-add"
(deftest "can add a value to a set"
@@ -2174,75 +2153,41 @@
;; ── core/runtimeErrors (18 tests) ──
(defsuite "hs-upstream-core/runtimeErrors"
(deftest "reports basic function invocation null errors properly"
(assert= (eval-hs-error "x()") "'x' is null")
(assert= (eval-hs-error "x.y()") "'x' is null")
(assert= (eval-hs-error "x.y.z()") "'x.y' is null")
)
(error "SKIP (untranslated): reports basic function invocation null errors properly"))
(deftest "reports basic function invocation null errors properly w/ of"
(assert= (eval-hs-error "z() of y of x") "'z' is null")
)
(error "SKIP (untranslated): reports basic function invocation null errors properly w/ of"))
(deftest "reports basic function invocation null errors properly w/ possessives"
(assert= (eval-hs-error "x's y()") "'x' is null")
(assert= (eval-hs-error "x's y's z()") "'x's y' is null")
)
(error "SKIP (untranslated): reports basic function invocation null errors properly w/ possessives"))
(deftest "reports null errors on add command properly"
(assert= (eval-hs-error "add .foo to #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "add @foo to #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "add {display:none} to #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on add command properly"))
(deftest "reports null errors on decrement command properly"
(assert= (eval-hs-error "decrement #doesntExist's innerHTML") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on decrement command properly"))
(deftest "reports null errors on default command properly"
(assert= (eval-hs-error "default #doesntExist's innerHTML to 'foo'") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on default command properly"))
(deftest "reports null errors on hide command properly"
(assert= (eval-hs-error "hide #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on hide command properly"))
(deftest "reports null errors on increment command properly"
(assert= (eval-hs-error "increment #doesntExist's innerHTML") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on increment command properly"))
(deftest "reports null errors on measure command properly"
(assert= (eval-hs-error "measure #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on measure command properly"))
(deftest "reports null errors on put command properly"
(assert= (eval-hs-error "put 'foo' into #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' into #doesntExist's innerHTML") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' into #doesntExist.innerHTML") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' before #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' after #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' at the start of #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "put 'foo' at the end of #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on put command properly"))
(deftest "reports null errors on remove command properly"
(assert= (eval-hs-error "remove .foo from #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "remove @foo from #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "remove #doesntExist from #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on remove command properly"))
(deftest "reports null errors on send command properly"
(assert= (eval-hs-error "send 'foo' to #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on send command properly"))
(deftest "reports null errors on sets properly"
(assert= (eval-hs-error "set x's y to true") "'x' is null")
(assert= (eval-hs-error "set x's @y to true") "'x' is null")
)
(error "SKIP (untranslated): reports null errors on sets properly"))
(deftest "reports null errors on settle command properly"
(assert= (eval-hs-error "settle #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on settle command properly"))
(deftest "reports null errors on show command properly"
(assert= (eval-hs-error "show #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on show command properly"))
(deftest "reports null errors on toggle command properly"
(assert= (eval-hs-error "toggle .foo on #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "toggle between .foo and .bar on #doesntExist") "'#doesntExist' is null")
(assert= (eval-hs-error "toggle @foo on #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on toggle command properly"))
(deftest "reports null errors on transition command properly"
(assert= (eval-hs-error "transition #doesntExist's *visibility to 0") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on transition command properly"))
(deftest "reports null errors on trigger command properly"
(assert= (eval-hs-error "trigger 'foo' on #doesntExist") "'#doesntExist' is null")
)
(error "SKIP (untranslated): reports null errors on trigger command properly"))
)
;; ── core/scoping (20 tests) ──

View File

@@ -1,131 +0,0 @@
(deftest
"math completeness"
(deftest
"trigonometry"
(deftest
"sin"
(assert= 0 (round (sin 0)) "sin 0 = 0")
(assert=
1
(round (sin (/ 3.14159 2)))
"sin pi/2 = 1")
(assert= 0 (round (sin 3.14159)) "sin pi = 0"))
(deftest
"cos"
(assert= 1 (round (cos 0)) "cos 0 = 1")
(assert=
0
(round (cos (/ 3.14159 2)))
"cos pi/2 = 0")
(assert= -1 (round (cos 3.14159)) "cos pi = -1"))
(deftest
"tan"
(assert= 0 (round (tan 0)) "tan 0 = 0")
(assert= 1 (round (tan 0.785398)) "tan pi/4 = 1"))
(deftest
"asin"
(assert= 0 (round (asin 0)) "asin 0 = 0")
(let
(r (asin 1))
(assert= true (and (> r 1.5) (< r 1.6)) "asin 1 ≈ pi/2")))
(deftest
"acos"
(assert= 0 (round (acos 1)) "acos 1 = 0")
(let
(r (acos 0))
(assert= true (and (> r 1.5) (< r 1.6)) "acos 0 ≈ pi/2")))
(deftest
"atan"
(assert= 0 (round (atan 0)) "atan 0 = 0")
(let
(r (atan 1))
(assert= true (and (> r 0.78) (< r 0.8)) "atan 1 ≈ pi/4"))
(let
(r (atan 1 1))
(assert=
true
(and (> r 0.78) (< r 0.8))
"atan 1 1 = atan2(1,1) ≈ pi/4"))
(let
(r (atan 1 0))
(assert= true (and (> r 1.5) (< r 1.6)) "atan 1 0 ≈ pi/2")))
(deftest
"exp"
(assert= 1 (round (exp 0)) "exp 0 = 1")
(let
(r (exp 1))
(assert= true (and (> r 2.71) (< r 2.72)) "exp 1 ≈ e")))
(deftest
"log"
(assert= 0 (round (log 1)) "log 1 = 0")
(let
(r (log 2.71828))
(assert= true (and (> r 0.99) (< r 1.01)) "log e ≈ 1"))))
(deftest
"expt"
(assert= 8 (expt 2 3) "2^3 = 8")
(assert= 1 (expt 5 0) "5^0 = 1")
(assert= 1000 (expt 10 3) "10^3 = 1000")
(let
(r (expt 2 0.5))
(assert= true (and (> r 1.41) (< r 1.43)) "2^0.5 ≈ sqrt(2)")))
(deftest
"quotient"
(assert= 3 (quotient 13 4) "13/4 = 3")
(assert=
-3
(quotient -13 4)
"-13/4 = -3 (truncate toward zero)")
(assert=
-3
(quotient 13 -4)
"13/-4 = -3 (truncate toward zero)")
(assert= 3 (quotient -13 -4) "-13/-4 = 3")
(assert= 0 (quotient 0 5) "0/5 = 0"))
(deftest
"gcd"
(assert= 6 (gcd 12 18) "gcd 12 18 = 6")
(assert= 1 (gcd 7 13) "gcd 7 13 = 1 (coprime)")
(assert= 4 (gcd 8 12) "gcd 8 12 = 4")
(assert= 5 (gcd 0 5) "gcd 0 5 = 5")
(assert= 6 (gcd -12 18) "gcd handles negatives"))
(deftest
"lcm"
(assert= 12 (lcm 4 6) "lcm 4 6 = 12")
(assert= 36 (lcm 12 18) "lcm 12 18 = 36")
(assert= 0 (lcm 0 5) "lcm 0 5 = 0")
(assert= 15 (lcm 3 5) "lcm 3 5 = 15"))
(deftest
"number->string"
(assert= "42" (number->string 42) "integer to string")
(assert= "0" (number->string 0) "zero to string")
(assert= "-7" (number->string -7) "negative to string")
(assert= "ff" (number->string 255 16) "255 in hex")
(assert= "1111" (number->string 15 2) "15 in binary")
(assert= "377" (number->string 255 8) "255 in octal")
(assert= "z" (number->string 35 36) "35 in base 36"))
(deftest
"string->number"
(assert= 42 (string->number "42") "string to integer")
(assert= -7 (string->number "-7") "negative string to integer")
(assert= 255 (string->number "ff" 16) "hex string")
(assert= 15 (string->number "1111" 2) "binary string")
(assert= 255 (string->number "377" 8) "octal string")
(assert= nil (string->number "not-a-number") "invalid returns nil")
(assert= nil (string->number "fg" 16) "invalid hex returns nil"))
(deftest
"numeric tower integration"
(assert=
true
(< (abs (- (sin (asin 0.5)) 0.5)) 0.0001)
"sin(asin(x)) = x")
(assert=
true
(< (abs (- (cos (acos 0.5)) 0.5)) 0.0001)
"cos(acos(x)) = x")
(assert= true (< (abs (- (exp (log 2)) 2)) 0.0001) "exp(log(x)) = x")
(assert=
(* 12 18)
(* (gcd 12 18) (lcm 12 18))
"gcd * lcm = a * b")))

View File

@@ -1,230 +0,0 @@
;; ==========================================================================
;; test-numeric-tower.sx — Numeric tower: Integer vs Float distinction
;;
;; Tests for float contagion, integer arithmetic, predicates,
;; coercions, parsing, and rendering.
;;
;; Note: Use fractional floats (1.5, 3.14) or exact->inexact for round floats,
;; since the SX serializer renders Number 1.0 as "1" (int form).
;; ==========================================================================
;; --------------------------------------------------------------------------
;; Integer arithmetic — result stays Integer when all args are Integer
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:int-arithmetic"
(deftest "int + int = int" (assert (integer? (+ 1 2))))
(deftest "int + int value" (assert= (+ 1 2) 3))
(deftest "int - int = int" (assert (integer? (- 10 3))))
(deftest "int - int value" (assert= (- 10 3) 7))
(deftest "int * int = int" (assert (integer? (* 4 5))))
(deftest "int * int value" (assert= (* 4 5) 20))
(deftest "zero identity" (assert= (+ 0 0) 0))
(deftest "negative int" (assert= (- 0 5) -5))
(deftest
"int negation is int"
(assert (integer? (- 0 7))))
(deftest
"large int product"
(assert= (* 100 100) 10000)))
;; --------------------------------------------------------------------------
;; Float contagion — any float arg promotes result to float
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:float-contagion"
(deftest "int + float = float" (assert (float? (+ 1 1.5))))
(deftest "int + float value" (assert= (+ 1 1.5) 2.5))
(deftest "float + int = float" (assert (float? (+ 1.5 2))))
(deftest "float + float = float" (assert (float? (+ 1.5 2.5))))
(deftest "int * float = float" (assert (float? (* 2 1.5))))
(deftest "int * float value" (assert= (* 2 1.5) 3))
(deftest "int - float = float" (assert (float? (- 5 2.5))))
(deftest "float - int = float" (assert (float? (- 5.5 2))))
(deftest
"three args with float"
(assert (float? (+ 1 2 3.5))))
(deftest
"exact->inexact promotes to float"
(assert (float? (exact->inexact 5)))))
;; --------------------------------------------------------------------------
;; Division
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:division"
(deftest
"exact division value"
(assert= (/ 6 2) 3))
(deftest "inexact division value" (assert= (/ 1 4) 0.25))
(deftest "float / float = float" (assert (float? (/ 3.5 2.5))))
(deftest
"rational / int = rational"
(assert (rational? (/ 1/2 2))))
(deftest "rational division value" (assert= (/ 1/2 2) 1/4)))
;; --------------------------------------------------------------------------
;; Type predicates
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:predicates"
(deftest "integer? on int" (assert (integer? 42)))
(deftest "integer? on negative" (assert (integer? -7)))
(deftest "integer? on zero" (assert (integer? 0)))
(deftest
"integer? on float-int"
(assert (integer? (exact->inexact 2))))
(deftest "integer? on fractional float" (assert (not (integer? 1.5))))
(deftest "float? on 1.5" (assert (float? 1.5)))
(deftest
"float? on exact->inexact"
(assert (float? (exact->inexact 2))))
(deftest "float? on int" (assert (not (float? 42))))
(deftest "number? on int" (assert (number? 42)))
(deftest "number? on float" (assert (number? 3.14)))
(deftest "number? on rational" (assert (number? 1/3)))
(deftest "number? on string" (assert (not (number? "42"))))
(deftest "exact? on int" (assert (exact? 1)))
(deftest "exact? on rational" (assert (exact? 1/3)))
(deftest
"exact? on exact->inexact"
(assert (not (exact? (exact->inexact 1)))))
(deftest "inexact? on 1.5" (assert (inexact? 1.5)))
(deftest "inexact? on int" (assert (not (inexact? 3)))))
;; --------------------------------------------------------------------------
;; Coercions
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:coercions"
(deftest
"exact->inexact int"
(assert= (exact->inexact 3) 3))
(deftest
"exact->inexact produces float"
(assert (float? (exact->inexact 5))))
(deftest
"exact->inexact float passthrough"
(assert= (exact->inexact 1.5) 1.5))
(deftest "exact->inexact rational" (assert= (exact->inexact 1/4) 0.25))
(deftest "inexact->exact 1.5" (assert= (inexact->exact 1.5) 2))
(deftest
"inexact->exact produces int"
(assert (integer? (inexact->exact (exact->inexact 4)))))
(deftest "inexact->exact 2.7" (assert= (inexact->exact 2.7) 3))
(deftest
"inexact->exact int passthrough"
(assert= (inexact->exact 5) 5)))
;; --------------------------------------------------------------------------
;; floor / ceiling / truncate / round — return Integer for floats
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:rounding"
(deftest "floor 3.7" (assert= (floor 3.7) 3))
(deftest "floor produces int" (assert (integer? (floor 3.7))))
(deftest "floor negative" (assert= (floor -2.3) -3))
(deftest "truncate 3.9" (assert= (truncate 3.9) 3))
(deftest "truncate negative" (assert= (truncate -3.9) -3))
(deftest "truncate produces int" (assert (integer? (truncate 3.9))))
(deftest "round 2.3 down" (assert= (round 2.3) 2))
(deftest "round produces int" (assert (integer? (round 2.3))))
(deftest
"floor of int passthrough"
(assert= (floor 5) 5))
(deftest "floor of int stays int" (assert (integer? (floor 5)))))
;; --------------------------------------------------------------------------
;; parse-number distinguishes int vs float strings
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:parse-number"
(deftest
"parse-number int string"
(assert= (parse-number "42") 42))
(deftest
"parse-number int is integer?"
(assert (integer? (parse-number "42"))))
(deftest "parse-number 3.14" (assert= (parse-number "3.14") 3.14))
(deftest
"parse-number float is float?"
(assert (float? (parse-number "3.14"))))
(deftest
"parse-number 1.5 is float?"
(assert (float? (parse-number "1.5"))))
(deftest
"parse-number negative int"
(assert= (parse-number "-5") -5))
(deftest
"parse-number negative int is integer?"
(assert (integer? (parse-number "-5"))))
(deftest "parse-int returns integer" (assert (integer? (parse-int "7"))))
(deftest "parse-int value" (assert= (parse-int "7") 7)))
;; --------------------------------------------------------------------------
;; Equality across numeric types
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:equality"
(deftest "int = same int" (assert= 5 5))
(deftest
"int = float eq"
(assert (= 1 (exact->inexact 1))))
(deftest
"float = int eq"
(assert (= (exact->inexact 1) 1)))
(deftest "int != different int" (assert (!= 1 2)))
(deftest "int < float" (assert (< 1 1.5)))
(deftest "float > int" (assert (> 2.5 2)))
(deftest "int <= float" (assert (<= 2 2.5)))
(deftest "int >= int" (assert (>= 3 3))))
;; --------------------------------------------------------------------------
;; mod / remainder / modulo with integers
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:modulo"
(deftest
"mod int int = int"
(assert (integer? (mod 10 3))))
(deftest "mod value" (assert= (mod 10 3) 1))
(deftest
"remainder int int = int"
(assert (integer? (remainder 10 3))))
(deftest
"remainder value"
(assert= (remainder 10 3) 1)))
;; --------------------------------------------------------------------------
;; min / max with mixed types
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:min-max"
(deftest "min two ints" (assert= (min 3 7) 3))
(deftest
"min int result type"
(assert (integer? (min 3 7))))
(deftest "max two ints" (assert= (max 3 7) 7))
(deftest "min with float" (assert= (min 3 2.5) 2.5))
(deftest "max with float" (assert= (max 3 3.5) 3.5)))
;; --------------------------------------------------------------------------
;; str rendering of int vs float
;; --------------------------------------------------------------------------
(defsuite
"numeric-tower:stringify"
(deftest "str of int" (assert= (str 42) "42"))
(deftest "str of negative int" (assert= (str -5) "-5"))
(deftest "str of 3.14" (assert= (str 3.14) "3.14"))
(deftest "str of 1.5" (assert= (str 1.5) "1.5")))

View File

@@ -1,232 +0,0 @@
;; Phase 14 — String ports + eof-object
(deftest
"eof-object"
(deftest
"eof-object is eof"
(assert=
true
(eof-object? (eof-object))
"eof-object? returns true for eof-object"))
(deftest
"non-eof values are not eof"
(assert= false (eof-object? nil) "nil is not eof")
(assert= false (eof-object? "") "string is not eof")
(assert= false (eof-object? 0) "zero is not eof")
(assert= false (eof-object? false) "false is not eof"))
(deftest
"type-of eof-object"
(assert=
"eof-object"
(type-of (eof-object))
"type-of eof-object is eof-object")))
(deftest
"open-input-string"
(deftest
"creates input port"
(let
(p (open-input-string "hello"))
(assert= true (port? p) "is a port")
(assert= true (input-port? p) "is an input port")
(assert= false (output-port? p) "is not an output port")))
(deftest
"type-of input port"
(let
(p (open-input-string "x"))
(assert= "input-port" (type-of p) "type-of is input-port"))))
(deftest
"open-output-string"
(deftest
"creates output port"
(let
(p (open-output-string))
(assert= true (port? p) "is a port")
(assert= true (output-port? p) "is an output port")
(assert= false (input-port? p) "is not an input port")))
(deftest
"type-of output port"
(let
(p (open-output-string))
(assert= "output-port" (type-of p) "type-of is output-port"))))
(deftest
"read-char"
(deftest
"reads chars sequentially"
(let
(p (open-input-string "ab"))
(let
(c1 (read-char p))
(assert= true (char? c1) "first result is char")
(assert= 97 (char->integer c1) "first char is a"))))
(deftest
"reads second char"
(let
(p (open-input-string "ab"))
(read-char p)
(let
(c2 (read-char p))
(assert= true (char? c2) "second result is char")
(assert= 98 (char->integer c2) "second char is b"))))
(deftest
"returns eof at end"
(let
(p (open-input-string "x"))
(read-char p)
(assert= true (eof-object? (read-char p)) "eof after last char")))
(deftest
"empty string yields eof immediately"
(let
(p (open-input-string ""))
(assert= true (eof-object? (read-char p)) "eof from empty string"))))
(deftest
"peek-char"
(deftest
"peeks without consuming"
(let
(p (open-input-string "x"))
(let
(c1 (peek-char p))
(let
(c2 (peek-char p))
(assert=
(char->integer c1)
(char->integer c2)
"peek twice gives same char")))))
(deftest
"peek then read"
(let
(p (open-input-string "z"))
(let
(peeked (peek-char p))
(let
(read (read-char p))
(assert=
(char->integer peeked)
(char->integer read)
"peek and read agree")))))
(deftest
"peek at end returns eof"
(let
(p (open-input-string ""))
(assert= true (eof-object? (peek-char p)) "eof on empty peek"))))
(deftest
"read-line"
(deftest
"reads a single line"
(let
(p (open-input-string "hello"))
(assert= "hello" (read-line p) "reads whole string as line")))
(deftest
"reads line up to newline"
(let
(p (open-input-string "foo\nbar"))
(assert= "foo" (read-line p) "first line is foo")))
(deftest
"reads second line"
(let
(p (open-input-string "foo\nbar"))
(read-line p)
(assert= "bar" (read-line p) "second line is bar")))
(deftest
"returns eof on empty port"
(let
(p (open-input-string ""))
(assert= true (eof-object? (read-line p)) "eof on empty")))
(deftest
"returns eof after last line"
(let
(p (open-input-string "hi"))
(read-line p)
(assert= true (eof-object? (read-line p)) "eof after reading"))))
(deftest
"write-char and get-output-string"
(deftest
"write single char"
(let
(p (open-output-string))
(write-char (make-char 65) p)
(assert= "A" (get-output-string p) "write char A")))
(deftest
"write multiple chars"
(let
(p (open-output-string))
(write-char (make-char 72) p)
(write-char (make-char 105) p)
(assert= "Hi" (get-output-string p) "write Hi"))))
(deftest
"write-string"
(deftest
"write a string to port"
(let
(p (open-output-string))
(write-string "hello" p)
(assert= "hello" (get-output-string p) "write-string result")))
(deftest
"multiple writes concatenate"
(let
(p (open-output-string))
(write-string "foo" p)
(write-string "bar" p)
(assert= "foobar" (get-output-string p) "concatenated writes"))))
(deftest
"get-output-string idempotent"
(let
(p (open-output-string))
(write-string "test" p)
(assert= "test" (get-output-string p) "first call")
(assert= "test" (get-output-string p) "second call same result")))
(deftest
"char-ready?"
(deftest
"ready when chars available"
(let
(p (open-input-string "x"))
(assert= true (char-ready? p) "ready with content")))
(deftest
"not ready when empty"
(let
(p (open-input-string ""))
(assert= false (char-ready? p) "not ready when empty"))))
(deftest
"close-port"
(deftest
"close input port"
(let
(p (open-input-string "hello"))
(close-port p)
(assert= true (eof-object? (read-char p)) "read after close gives eof")))
(deftest
"close output port"
(let
(p (open-output-string))
(write-string "ok" p)
(close-port p)
(assert= "ok" (get-output-string p) "output preserved after close"))))
(deftest
"roundtrip string via ports"
(let
(in (open-input-string "abc"))
(let
(out (open-output-string))
(do
(let
(c1 (read-char in))
(when (not (eof-object? c1)) (write-char c1 out)))
(let
(c2 (read-char in))
(when (not (eof-object? c2)) (write-char c2 out)))
(let
(c3 (read-char in))
(when (not (eof-object? c3)) (write-char c3 out)))
(assert= "abc" (get-output-string out) "roundtrip via ports")))))

View File

@@ -6,36 +6,20 @@
;; Arithmetic
;; --------------------------------------------------------------------------
(defsuite
"arithmetic"
(defsuite "arithmetic"
(deftest "add" (assert-equal 3 (+ 1 2)))
(deftest
"add multiple"
(assert-equal 10 (+ 1 2 3 4)))
(deftest "add multiple" (assert-equal 10 (+ 1 2 3 4)))
(deftest "add zero" (assert-equal 5 (+ 5 0)))
(deftest
"add negative"
(assert-equal -1 (+ 1 -2)))
(deftest "add negative" (assert-equal -1 (+ 1 -2)))
(deftest "subtract" (assert-equal 3 (- 5 2)))
(deftest
"subtract negative"
(assert-equal 7 (- 5 -2)))
(deftest "subtract negative" (assert-equal 7 (- 5 -2)))
(deftest "multiply" (assert-equal 12 (* 3 4)))
(deftest
"multiply zero"
(assert-equal 0 (* 5 0)))
(deftest
"multiply negative"
(assert-equal -6 (* 2 -3)))
(deftest "multiply zero" (assert-equal 0 (* 5 0)))
(deftest "multiply negative" (assert-equal -6 (* 2 -3)))
(deftest "divide" (assert-equal 3 (/ 9 3)))
(deftest "divide float" (assert-equal 2.5 (/ 5 2)))
(deftest "mod" (assert-equal 1 (mod 7 3)))
(deftest
"mod negative"
(assert-true
(or
(= (mod -1 3) 2)
(= (mod -1 3) -1))))
(deftest "mod negative" (assert-true (or (= (mod -1 3) 2) (= (mod -1 3) -1))))
(deftest "inc" (assert-equal 6 (inc 5)))
(deftest "dec" (assert-equal 4 (dec 5)))
(deftest "abs positive" (assert-equal 5 (abs 5)))
@@ -48,8 +32,7 @@
;; Comparison
;; --------------------------------------------------------------------------
(defsuite
"comparison"
(defsuite "comparison"
(deftest "equal numbers" (assert-true (= 1 1)))
(deftest "not equal numbers" (assert-false (= 1 2)))
(deftest "equal strings" (assert-true (= "a" "a")))
@@ -69,8 +52,7 @@
;; Predicates
;; --------------------------------------------------------------------------
(defsuite
"predicates"
(defsuite "predicates"
(deftest "nil? nil" (assert-true (nil? nil)))
(deftest "nil? number" (assert-false (nil? 0)))
(deftest "nil? string" (assert-false (nil? "")))
@@ -94,22 +76,15 @@
;; String operations
;; --------------------------------------------------------------------------
(defsuite
"strings"
(deftest
"str concat"
(assert-equal "hello world" (str "hello" " " "world")))
(defsuite "strings"
(deftest "str concat" (assert-equal "hello world" (str "hello" " " "world")))
(deftest "str number" (assert-equal "42" (str 42)))
(deftest "str empty" (assert-equal "" (str)))
(deftest "len string" (assert-equal 5 (len "hello")))
(deftest "len empty" (assert-equal 0 (len "")))
(deftest
"slice"
(assert-equal "ell" (slice "hello" 1 4)))
(deftest "slice" (assert-equal "ell" (slice "hello" 1 4)))
(deftest "slice from" (assert-equal "llo" (slice "hello" 2)))
(deftest
"slice empty"
(assert-equal "" (slice "hello" 2 2)))
(deftest "slice empty" (assert-equal "" (slice "hello" 2 2)))
(deftest "join" (assert-equal "a,b,c" (join "," (list "a" "b" "c"))))
(deftest "join empty" (assert-equal "" (join "," (list))))
(deftest "join single" (assert-equal "a" (join "," (list "a"))))
@@ -126,238 +101,88 @@
(deftest "replace" (assert-equal "hXllo" (replace "hello" "e" "X")))
(deftest "string-length" (assert-equal 5 (string-length "hello")))
(deftest "index-of found" (assert-equal 2 (index-of "hello" "l")))
(deftest
"index-of not found"
(assert-equal -1 (index-of "hello" "z"))))
(deftest "index-of not found" (assert-equal -1 (index-of "hello" "z"))))
;; --------------------------------------------------------------------------
;; List operations
;; --------------------------------------------------------------------------
(defsuite
"lists"
(deftest
"list create"
(assert-equal
(list 1 2 3)
(list 1 2 3)))
(deftest
"first"
(assert-equal 1 (first (list 1 2 3))))
(defsuite "lists"
(deftest "list create" (assert-equal (list 1 2 3) (list 1 2 3)))
(deftest "first" (assert-equal 1 (first (list 1 2 3))))
(deftest "first empty" (assert-nil (first (list))))
(deftest
"rest"
(assert-equal
(list 2 3)
(rest (list 1 2 3))))
(deftest "rest" (assert-equal (list 2 3) (rest (list 1 2 3))))
(deftest "rest single" (assert-equal (list) (rest (list 1))))
(deftest "rest empty" (assert-equal (list) (rest (list))))
(deftest
"nth"
(assert-equal
2
(nth (list 1 2 3) 1)))
(deftest
"nth out of bounds"
(assert-nil (nth (list 1 2) 5)))
(deftest
"last"
(assert-equal 3 (last (list 1 2 3))))
(deftest "nth" (assert-equal 2 (nth (list 1 2 3) 1)))
(deftest "nth out of bounds" (assert-nil (nth (list 1 2) 5)))
(deftest "last" (assert-equal 3 (last (list 1 2 3))))
(deftest "last single" (assert-equal 1 (last (list 1))))
(deftest
"len list"
(assert-equal 3 (len (list 1 2 3))))
(deftest "len list" (assert-equal 3 (len (list 1 2 3))))
(deftest "len empty" (assert-equal 0 (len (list))))
(deftest
"cons"
(assert-equal
(list 0 1 2)
(cons 0 (list 1 2))))
(deftest
"append"
(assert-equal
(list 1 2 3 4)
(append (list 1 2) (list 3 4))))
(deftest
"append element"
(assert-equal
(list 1 2 3)
(append (list 1 2) (list 3))))
(deftest
"slice list"
(assert-equal
(list 2 3)
(slice
(list 1 2 3 4)
1
3)))
(deftest
"concat"
(assert-equal
(list 1 2 3 4)
(concat (list 1 2) (list 3 4))))
(deftest
"reverse"
(assert-equal
(list 3 2 1)
(reverse (list 1 2 3))))
(deftest "cons" (assert-equal (list 0 1 2) (cons 0 (list 1 2))))
(deftest "append" (assert-equal (list 1 2 3 4) (append (list 1 2) (list 3 4))))
(deftest "append element" (assert-equal (list 1 2 3) (append (list 1 2) (list 3))))
(deftest "slice list" (assert-equal (list 2 3) (slice (list 1 2 3 4) 1 3)))
(deftest "concat" (assert-equal (list 1 2 3 4) (concat (list 1 2) (list 3 4))))
(deftest "reverse" (assert-equal (list 3 2 1) (reverse (list 1 2 3))))
(deftest "reverse empty" (assert-equal (list) (reverse (list))))
(deftest
"contains? list"
(assert-true
(contains? (list 1 2 3) 2)))
(deftest
"contains? list false"
(assert-false
(contains? (list 1 2 3) 5)))
(deftest
"range"
(assert-equal
(list 0 1 2)
(range 0 3)))
(deftest
"range step"
(assert-equal
(list 0 2 4)
(range 0 6 2)))
(deftest
"flatten"
(assert-equal
(list 1 2 3 4)
(flatten
(list (list 1 2) (list 3 4))))))
(deftest "contains? list" (assert-true (contains? (list 1 2 3) 2)))
(deftest "contains? list false" (assert-false (contains? (list 1 2 3) 5)))
(deftest "range" (assert-equal (list 0 1 2) (range 0 3)))
(deftest "range step" (assert-equal (list 0 2 4) (range 0 6 2)))
(deftest "flatten" (assert-equal (list 1 2 3 4) (flatten (list (list 1 2) (list 3 4))))))
;; --------------------------------------------------------------------------
;; Dict operations
;; --------------------------------------------------------------------------
(defsuite
"dicts"
(deftest
"dict create"
(assert-equal 1 (get (dict "a" 1 "b" 2) "a")))
(defsuite "dicts"
(deftest "dict create" (assert-equal 1 (get (dict "a" 1 "b" 2) "a")))
(deftest "get missing" (assert-nil (get (dict "a" 1) "z")))
(deftest
"get default"
(assert-equal 99 (get (dict "a" 1) "z" 99)))
(deftest
"keys"
(assert-true
(contains? (keys (dict "a" 1 "b" 2)) "a")))
(deftest "get default" (assert-equal 99 (get (dict "a" 1) "z" 99)))
(deftest "keys" (assert-true (contains? (keys (dict "a" 1 "b" 2)) "a")))
(deftest "has-key?" (assert-true (has-key? (dict "a" 1) "a")))
(deftest
"has-key? false"
(assert-false (has-key? (dict "a" 1) "z")))
(deftest
"assoc"
(assert-equal
2
(get (assoc (dict "a" 1) "b" 2) "b")))
(deftest
"dissoc"
(assert-false
(has-key? (dissoc (dict "a" 1 "b" 2) "a") "a")))
(deftest
"len dict"
(assert-equal 2 (len (dict "a" 1 "b" 2))))
(deftest "has-key? false" (assert-false (has-key? (dict "a" 1) "z")))
(deftest "assoc" (assert-equal 2 (get (assoc (dict "a" 1) "b" 2) "b")))
(deftest "dissoc" (assert-false (has-key? (dissoc (dict "a" 1 "b" 2) "a") "a")))
(deftest "len dict" (assert-equal 2 (len (dict "a" 1 "b" 2))))
(deftest "len empty dict" (assert-equal 0 (len (dict))))
(deftest "empty? dict" (assert-true (empty? (dict))))
(deftest
"empty? nonempty dict"
(assert-false (empty? (dict "a" 1)))))
(deftest "empty? nonempty dict" (assert-false (empty? (dict "a" 1)))))
;; --------------------------------------------------------------------------
;; Higher-order functions
;; --------------------------------------------------------------------------
(defsuite
"higher-order"
(deftest
"map"
(assert-equal
(list 2 4 6)
(map
(fn (x) (* x 2))
(list 1 2 3))))
(defsuite "higher-order"
(deftest "map" (assert-equal (list 2 4 6) (map (fn (x) (* x 2)) (list 1 2 3))))
(deftest "map empty" (assert-equal (list) (map (fn (x) x) (list))))
(deftest
"filter"
(assert-equal
(list 2 4)
(filter
(fn (x) (= (mod x 2) 0))
(list 1 2 3 4 5))))
(deftest
"filter none"
(assert-equal
(list)
(filter (fn (x) false) (list 1 2 3))))
(deftest
"reduce"
(assert-equal
10
(reduce
(fn (acc x) (+ acc x))
0
(list 1 2 3 4))))
(deftest
"reduce empty"
(assert-equal
0
(reduce (fn (acc x) (+ acc x)) 0 (list))))
(deftest
"some true"
(assert-true
(some
(fn (x) (> x 3))
(list 1 2 3 4 5))))
(deftest
"some false"
(assert-false
(some
(fn (x) (> x 10))
(list 1 2 3))))
(deftest "filter" (assert-equal (list 2 4) (filter (fn (x) (= (mod x 2) 0)) (list 1 2 3 4 5))))
(deftest "filter none" (assert-equal (list) (filter (fn (x) false) (list 1 2 3))))
(deftest "reduce" (assert-equal 10 (reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3 4))))
(deftest "reduce empty" (assert-equal 0 (reduce (fn (acc x) (+ acc x)) 0 (list))))
(deftest "some true" (assert-true (some (fn (x) (> x 3)) (list 1 2 3 4 5))))
(deftest "some false" (assert-false (some (fn (x) (> x 10)) (list 1 2 3))))
(deftest "some empty" (assert-false (some (fn (x) true) (list))))
(deftest
"every? true"
(assert-true
(every?
(fn (x) (> x 0))
(list 1 2 3))))
(deftest
"every? false"
(assert-false
(every?
(fn (x) (> x 2))
(list 1 2 3))))
(deftest "every? true" (assert-true (every? (fn (x) (> x 0)) (list 1 2 3))))
(deftest "every? false" (assert-false (every? (fn (x) (> x 2)) (list 1 2 3))))
(deftest "every? empty" (assert-true (every? (fn (x) false) (list))))
(deftest
"for-each returns nil"
(let
((log (list)))
(for-each
(fn (x) (append! log x))
(list 1 2 3))
(deftest "for-each returns nil"
(let ((log (list)))
(for-each (fn (x) (append! log x)) (list 1 2 3))
(assert-equal (list 1 2 3) log)))
(deftest
"map-indexed"
(assert-equal
(list (list 0 "a") (list 1 "b"))
(deftest "map-indexed"
(assert-equal (list (list 0 "a") (list 1 "b"))
(map-indexed (fn (i x) (list i x)) (list "a" "b")))))
;; --------------------------------------------------------------------------
;; Type coercion
;; --------------------------------------------------------------------------
(defsuite
"type-coercion"
(deftest
"str bool"
(assert-true (or (= (str true) "true") (= (str true) "True"))))
(defsuite "type-coercion"
(deftest "str bool" (assert-true (or (= (str true) "true") (= (str true) "True"))))
(deftest "str nil" (assert-equal "" (str nil)))
(deftest
"str list"
(assert-true
(not (empty? (str (list 1 2 3))))))
(deftest "str list" (assert-true (not (empty? (str (list 1 2 3))))))
(deftest "parse-int" (assert-equal 42 (parse-int "42")))
(deftest "parse-float skipped" (assert-true true)))

Some files were not shown because too many files have changed in this diff Show More