Compare commits
68 Commits
loops/fort
...
74ce9e7c75
| Author | SHA1 | Date | |
|---|---|---|---|
| 74ce9e7c75 | |||
| 985671cd76 | |||
| f07b6e497e | |||
| ef736112ef | |||
| e4eab6a309 | |||
| c311d4ebc4 | |||
| 99f8ccb30e | |||
| 4f9da65b3d | |||
| 025ddbebdd | |||
| f449f82fdd | |||
| 0e426cfea8 | |||
| 71c4b5e33f | |||
| 4cd8773766 | |||
| 733b1ebefa | |||
| 85911d7b84 | |||
| ab66b29a74 | |||
| 32a82a2e12 | |||
| 7d6df6fd5f | |||
| fd16776dd2 | |||
| a12a6a11cb | |||
| ce7243a1fb | |||
| 3f8fe41d4d | |||
| 4da91bb9b4 | |||
| 81f96df5fa | |||
| 1819156d1e | |||
| cdee007185 | |||
| bcf6057ac5 | |||
| 8fd55d6aa0 | |||
| 8a9c074141 | |||
| 13d0ebcce8 | |||
| 00db8b7763 | |||
| 788ac9dd05 | |||
| bf250a24bf | |||
| 537e2cdb5a | |||
| 0a8b30b7b8 | |||
| 2075db62ba | |||
| 1aca2c7bc5 | |||
| be2000a048 | |||
| 0be5eeafd8 | |||
| 04ed092f88 | |||
| 776ae18a20 | |||
| 5a83f4ef51 | |||
| 73080bb7de | |||
| 8f0af85d01 | |||
| 07a22257f6 | |||
| 8ef05514b5 | |||
| 0823832dcd | |||
| 8ee0928a3d | |||
| 25a4ce4a05 | |||
| f72868c445 | |||
| c6f58116bf | |||
| 76ee8cc39b | |||
| 373d57cbcb | |||
| 3190e770fb | |||
| e018ba9423 | |||
| 09683b8a18 | |||
| 64e3b3f44e | |||
| 1302f5a3cc | |||
| 93b31b6c8a | |||
| ffc3716b0e | |||
| 7fb4c52159 | |||
| 072735a6de | |||
| 1846be0bd8 | |||
| 3adad8e50e | |||
| f019d42727 | |||
| 738f44e47d | |||
| 1888c272f9 | |||
| 60b7f0d7bb |
500
lib/common-lisp/clos.sx
Normal file
500
lib/common-lisp/clos.sx
Normal file
@@ -0,0 +1,500 @@
|
||||
;; lib/common-lisp/clos.sx — CLOS: classes, instances, generic functions
|
||||
;;
|
||||
;; Class records: {:clos-type "class" :name "NAME" :slots {...} :parents [...] :methods [...]}
|
||||
;; Instance: {:clos-type "instance" :class "NAME" :slots {slot: val ...}}
|
||||
;; Method: {:qualifiers [...] :specializers [...] :fn (fn (args next-fn) ...)}
|
||||
;;
|
||||
;; SX primitive notes:
|
||||
;; dict->list: use (map (fn (k) (list k (get d k))) (keys d))
|
||||
;; dict-set (pure): use assoc
|
||||
;; fn?/callable?: use callable?
|
||||
|
||||
;; ── dict helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-dict->list
|
||||
(fn (d) (map (fn (k) (list k (get d k))) (keys d))))
|
||||
|
||||
;; ── class registry ─────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-class-registry
|
||||
(dict
|
||||
"t"
|
||||
{:parents (list) :clos-type "class" :slots (dict) :methods (list) :name "t"}
|
||||
"null"
|
||||
{:parents (list "t") :clos-type "class" :slots (dict) :methods (list) :name "null"}
|
||||
"integer"
|
||||
{:parents (list "t") :clos-type "class" :slots (dict) :methods (list) :name "integer"}
|
||||
"float"
|
||||
{:parents (list "t") :clos-type "class" :slots (dict) :methods (list) :name "float"}
|
||||
"string"
|
||||
{:parents (list "t") :clos-type "class" :slots (dict) :methods (list) :name "string"}
|
||||
"symbol"
|
||||
{:parents (list "t") :clos-type "class" :slots (dict) :methods (list) :name "symbol"}
|
||||
"cons"
|
||||
{:parents (list "t") :clos-type "class" :slots (dict) :methods (list) :name "cons"}
|
||||
"list"
|
||||
{:parents (list "t") :clos-type "class" :slots (dict) :methods (list) :name "list"}))
|
||||
|
||||
;; ── clos-generic-registry ─────────────────────────────────────────────────
|
||||
|
||||
(define clos-generic-registry (dict))
|
||||
|
||||
;; ── class-of ──────────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-class-of
|
||||
(fn
|
||||
(x)
|
||||
(cond
|
||||
((nil? x) "null")
|
||||
((integer? x) "integer")
|
||||
((float? x) "float")
|
||||
((string? x) "string")
|
||||
((symbol? x) "symbol")
|
||||
((and (list? x) (> (len x) 0)) "cons")
|
||||
((and (list? x) (= (len x) 0)) "null")
|
||||
((and (dict? x) (= (get x "clos-type") "instance")) (get x "class"))
|
||||
(:else "t"))))
|
||||
|
||||
;; ── subclass-of? ──────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Captures clos-class-registry at define time to avoid free-variable issues.
|
||||
|
||||
(define
|
||||
clos-subclass-of?
|
||||
(let
|
||||
((registry clos-class-registry))
|
||||
(fn
|
||||
(class-name super-name)
|
||||
(if
|
||||
(= class-name super-name)
|
||||
true
|
||||
(let
|
||||
((rec (get registry class-name)))
|
||||
(if
|
||||
(nil? rec)
|
||||
false
|
||||
(some
|
||||
(fn (p) (clos-subclass-of? p super-name))
|
||||
(get rec "parents"))))))))
|
||||
|
||||
;; ── instance-of? ──────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-instance-of?
|
||||
(fn (obj class-name) (clos-subclass-of? (clos-class-of obj) class-name)))
|
||||
|
||||
;; ── defclass ──────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; slot-specs: list of dicts with keys: name initarg initform accessor reader writer
|
||||
;; Each missing key defaults to nil.
|
||||
|
||||
(define clos-slot-spec (fn (spec) (if (string? spec) {:initform nil :initarg nil :reader nil :writer nil :accessor nil :name spec} spec)))
|
||||
|
||||
(define
|
||||
clos-defclass
|
||||
(fn
|
||||
(name parents slot-specs)
|
||||
(let
|
||||
((slots (dict)))
|
||||
(for-each
|
||||
(fn
|
||||
(pname)
|
||||
(let
|
||||
((prec (get clos-class-registry pname)))
|
||||
(when
|
||||
(not (nil? prec))
|
||||
(for-each
|
||||
(fn
|
||||
(k)
|
||||
(when
|
||||
(nil? (get slots k))
|
||||
(dict-set! slots k (get (get prec "slots") k))))
|
||||
(keys (get prec "slots"))))))
|
||||
parents)
|
||||
(for-each
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((spec (clos-slot-spec s)))
|
||||
(dict-set! slots (get spec "name") spec)))
|
||||
slot-specs)
|
||||
(let
|
||||
((class-rec {:parents parents :clos-type "class" :slots slots :methods (list) :name name}))
|
||||
(dict-set! clos-class-registry name class-rec)
|
||||
(clos-install-accessors-for name slots)
|
||||
name))))
|
||||
|
||||
;; ── accessor installation (forward-declared, defined after defmethod) ──────
|
||||
|
||||
(define
|
||||
clos-install-accessors-for
|
||||
(fn
|
||||
(class-name slots)
|
||||
(for-each
|
||||
(fn
|
||||
(k)
|
||||
(let
|
||||
((spec (get slots k)))
|
||||
(let
|
||||
((reader (get spec "reader")))
|
||||
(when
|
||||
(not (nil? reader))
|
||||
(clos-add-reader-method reader class-name k)))
|
||||
(let
|
||||
((accessor (get spec "accessor")))
|
||||
(when
|
||||
(not (nil? accessor))
|
||||
(clos-add-reader-method accessor class-name k)))))
|
||||
(keys slots))))
|
||||
|
||||
;; placeholder — real impl filled in after defmethod is defined
|
||||
(define clos-add-reader-method (fn (method-name class-name slot-name) nil))
|
||||
|
||||
;; ── make-instance ─────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-make-instance
|
||||
(fn
|
||||
(class-name &rest initargs)
|
||||
(let
|
||||
((class-rec (get clos-class-registry class-name)))
|
||||
(if
|
||||
(nil? class-rec)
|
||||
(error (str "No class named: " class-name))
|
||||
(let
|
||||
((slots (dict)))
|
||||
(for-each
|
||||
(fn
|
||||
(k)
|
||||
(let
|
||||
((spec (get (get class-rec "slots") k)))
|
||||
(let
|
||||
((initform (get spec "initform")))
|
||||
(when
|
||||
(not (nil? initform))
|
||||
(dict-set!
|
||||
slots
|
||||
k
|
||||
(if (callable? initform) (initform) initform))))))
|
||||
(keys (get class-rec "slots")))
|
||||
(define
|
||||
apply-args
|
||||
(fn
|
||||
(args)
|
||||
(when
|
||||
(>= (len args) 2)
|
||||
(let
|
||||
((key (str (first args))) (val (first (rest args))))
|
||||
(let
|
||||
((skey (if (= (slice key 0 1) ":") (slice key 1 (len key)) key)))
|
||||
(let
|
||||
((matched false))
|
||||
(for-each
|
||||
(fn
|
||||
(sk)
|
||||
(let
|
||||
((spec (get (get class-rec "slots") sk)))
|
||||
(let
|
||||
((ia (get spec "initarg")))
|
||||
(when
|
||||
(or
|
||||
(= ia key)
|
||||
(= ia (str ":" skey))
|
||||
(= sk skey))
|
||||
(dict-set! slots sk val)
|
||||
(set! matched true)))))
|
||||
(keys (get class-rec "slots")))))
|
||||
(apply-args (rest (rest args)))))))
|
||||
(apply-args initargs)
|
||||
{:clos-type "instance" :slots slots :class class-name})))))
|
||||
|
||||
;; ── slot-value ────────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-slot-value
|
||||
(fn
|
||||
(instance slot-name)
|
||||
(if
|
||||
(and (dict? instance) (= (get instance "clos-type") "instance"))
|
||||
(get (get instance "slots") slot-name)
|
||||
(error (str "Not a CLOS instance: " (inspect instance))))))
|
||||
|
||||
(define
|
||||
clos-set-slot-value!
|
||||
(fn
|
||||
(instance slot-name value)
|
||||
(if
|
||||
(and (dict? instance) (= (get instance "clos-type") "instance"))
|
||||
(dict-set! (get instance "slots") slot-name value)
|
||||
(error (str "Not a CLOS instance: " (inspect instance))))))
|
||||
|
||||
(define
|
||||
clos-slot-boundp
|
||||
(fn
|
||||
(instance slot-name)
|
||||
(and
|
||||
(dict? instance)
|
||||
(= (get instance "clos-type") "instance")
|
||||
(not (nil? (get (get instance "slots") slot-name))))))
|
||||
|
||||
;; ── find-class / change-class ─────────────────────────────────────────────
|
||||
|
||||
(define clos-find-class (fn (name) (get clos-class-registry name)))
|
||||
|
||||
(define
|
||||
clos-change-class!
|
||||
(fn
|
||||
(instance new-class-name)
|
||||
(if
|
||||
(and (dict? instance) (= (get instance "clos-type") "instance"))
|
||||
(dict-set! instance "class" new-class-name)
|
||||
(error (str "Not a CLOS instance: " (inspect instance))))))
|
||||
|
||||
;; ── defgeneric ────────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-defgeneric
|
||||
(fn
|
||||
(name options)
|
||||
(let
|
||||
((combination (or (get options "method-combination") "standard")))
|
||||
(when
|
||||
(nil? (get clos-generic-registry name))
|
||||
(dict-set! clos-generic-registry name {:methods (list) :combination combination :name name}))
|
||||
name)))
|
||||
|
||||
;; ── defmethod ─────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; method-fn: (fn (args next-fn) body)
|
||||
;; args = list of all call arguments
|
||||
;; next-fn = (fn () next-method-result) or nil
|
||||
|
||||
(define
|
||||
clos-defmethod
|
||||
(fn
|
||||
(generic-name qualifiers specializers method-fn)
|
||||
(when
|
||||
(nil? (get clos-generic-registry generic-name))
|
||||
(clos-defgeneric generic-name {}))
|
||||
(let
|
||||
((grec (get clos-generic-registry generic-name))
|
||||
(new-method {:fn method-fn :qualifiers qualifiers :specializers specializers}))
|
||||
(let
|
||||
((kept (filter (fn (m) (not (and (= (get m "qualifiers") qualifiers) (= (get m "specializers") specializers)))) (get grec "methods"))))
|
||||
(dict-set!
|
||||
clos-generic-registry
|
||||
generic-name
|
||||
(assoc grec "methods" (append kept (list new-method))))
|
||||
generic-name))))
|
||||
|
||||
;; Now install the real accessor-method installer
|
||||
(set!
|
||||
clos-add-reader-method
|
||||
(fn
|
||||
(method-name class-name slot-name)
|
||||
(clos-defmethod
|
||||
method-name
|
||||
(list)
|
||||
(list class-name)
|
||||
(fn (args next-fn) (clos-slot-value (first args) slot-name)))))
|
||||
|
||||
;; ── method specificity ─────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-method-matches?
|
||||
(fn
|
||||
(method args)
|
||||
(let
|
||||
((specs (get method "specializers")))
|
||||
(if
|
||||
(> (len specs) (len args))
|
||||
false
|
||||
(define
|
||||
check-all
|
||||
(fn
|
||||
(i)
|
||||
(if
|
||||
(>= i (len specs))
|
||||
true
|
||||
(let
|
||||
((spec (nth specs i)) (arg (nth args i)))
|
||||
(if
|
||||
(= spec "t")
|
||||
(check-all (+ i 1))
|
||||
(if
|
||||
(clos-instance-of? arg spec)
|
||||
(check-all (+ i 1))
|
||||
false))))))
|
||||
(check-all 0)))))
|
||||
|
||||
;; Precedence distance: how far class-name is from spec-name up the hierarchy.
|
||||
(define
|
||||
clos-specificity
|
||||
(let
|
||||
((registry clos-class-registry))
|
||||
(fn
|
||||
(class-name spec-name)
|
||||
(define
|
||||
walk
|
||||
(fn
|
||||
(cn depth)
|
||||
(if
|
||||
(= cn spec-name)
|
||||
depth
|
||||
(let
|
||||
((rec (get registry cn)))
|
||||
(if
|
||||
(nil? rec)
|
||||
nil
|
||||
(let
|
||||
((results (map (fn (p) (walk p (+ depth 1))) (get rec "parents"))))
|
||||
(let
|
||||
((non-nil (filter (fn (x) (not (nil? x))) results)))
|
||||
(if
|
||||
(empty? non-nil)
|
||||
nil
|
||||
(reduce
|
||||
(fn (a b) (if (< a b) a b))
|
||||
(first non-nil)
|
||||
(rest non-nil))))))))))
|
||||
(walk class-name 0))))
|
||||
|
||||
(define
|
||||
clos-method-more-specific?
|
||||
(fn
|
||||
(m1 m2 args)
|
||||
(let
|
||||
((s1 (get m1 "specializers")) (s2 (get m2 "specializers")))
|
||||
(define
|
||||
cmp
|
||||
(fn
|
||||
(i)
|
||||
(if
|
||||
(>= i (len s1))
|
||||
false
|
||||
(let
|
||||
((c1 (clos-specificity (clos-class-of (nth args i)) (nth s1 i)))
|
||||
(c2
|
||||
(clos-specificity (clos-class-of (nth args i)) (nth s2 i))))
|
||||
(cond
|
||||
((and (nil? c1) (nil? c2)) (cmp (+ i 1)))
|
||||
((nil? c1) false)
|
||||
((nil? c2) true)
|
||||
((< c1 c2) true)
|
||||
((> c1 c2) false)
|
||||
(:else (cmp (+ i 1))))))))
|
||||
(cmp 0))))
|
||||
|
||||
(define
|
||||
clos-sort-methods
|
||||
(fn
|
||||
(methods args)
|
||||
(define
|
||||
insert
|
||||
(fn
|
||||
(m sorted)
|
||||
(if
|
||||
(empty? sorted)
|
||||
(list m)
|
||||
(if
|
||||
(clos-method-more-specific? m (first sorted) args)
|
||||
(cons m sorted)
|
||||
(cons (first sorted) (insert m (rest sorted)))))))
|
||||
(reduce (fn (acc m) (insert m acc)) (list) methods)))
|
||||
|
||||
;; ── call-generic (standard method combination) ─────────────────────────────
|
||||
|
||||
(define
|
||||
clos-call-generic
|
||||
(fn
|
||||
(generic-name args)
|
||||
(let
|
||||
((grec (get clos-generic-registry generic-name)))
|
||||
(if
|
||||
(nil? grec)
|
||||
(error (str "No generic function: " generic-name))
|
||||
(let
|
||||
((applicable (filter (fn (m) (clos-method-matches? m args)) (get grec "methods"))))
|
||||
(if
|
||||
(empty? applicable)
|
||||
(error
|
||||
(str
|
||||
"No applicable method for "
|
||||
generic-name
|
||||
" with classes "
|
||||
(inspect (map clos-class-of args))))
|
||||
(let
|
||||
((primary (filter (fn (m) (empty? (get m "qualifiers"))) applicable))
|
||||
(before
|
||||
(filter
|
||||
(fn (m) (= (get m "qualifiers") (list "before")))
|
||||
applicable))
|
||||
(after
|
||||
(filter
|
||||
(fn (m) (= (get m "qualifiers") (list "after")))
|
||||
applicable))
|
||||
(around
|
||||
(filter
|
||||
(fn (m) (= (get m "qualifiers") (list "around")))
|
||||
applicable)))
|
||||
(let
|
||||
((sp (clos-sort-methods primary args))
|
||||
(sb (clos-sort-methods before args))
|
||||
(sa (clos-sort-methods after args))
|
||||
(sw (clos-sort-methods around args)))
|
||||
(define
|
||||
make-primary-chain
|
||||
(fn
|
||||
(methods)
|
||||
(if
|
||||
(empty? methods)
|
||||
(fn
|
||||
()
|
||||
(error (str "No next primary method: " generic-name)))
|
||||
(fn
|
||||
()
|
||||
((get (first methods) "fn")
|
||||
args
|
||||
(make-primary-chain (rest methods)))))))
|
||||
(define
|
||||
make-around-chain
|
||||
(fn
|
||||
(around-methods inner-thunk)
|
||||
(if
|
||||
(empty? around-methods)
|
||||
inner-thunk
|
||||
(fn
|
||||
()
|
||||
((get (first around-methods) "fn")
|
||||
args
|
||||
(make-around-chain
|
||||
(rest around-methods)
|
||||
inner-thunk))))))
|
||||
(for-each (fn (m) ((get m "fn") args (fn () nil))) sb)
|
||||
(let
|
||||
((primary-thunk (make-primary-chain sp)))
|
||||
(let
|
||||
((result (if (empty? sw) (primary-thunk) ((make-around-chain sw primary-thunk)))))
|
||||
(for-each
|
||||
(fn (m) ((get m "fn") args (fn () nil)))
|
||||
(reverse sa))
|
||||
result))))))))))
|
||||
|
||||
;; ── call-next-method / next-method-p ──────────────────────────────────────
|
||||
|
||||
(define clos-call-next-method (fn (next-fn) (next-fn)))
|
||||
|
||||
(define clos-next-method-p (fn (next-fn) (not (nil? next-fn))))
|
||||
|
||||
;; ── with-slots ────────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
clos-with-slots
|
||||
(fn
|
||||
(instance slot-names body-fn)
|
||||
(let
|
||||
((vals (map (fn (s) (clos-slot-value instance s)) slot-names)))
|
||||
(apply body-fn vals))))
|
||||
161
lib/common-lisp/conformance.sh
Executable file
161
lib/common-lisp/conformance.sh
Executable file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/common-lisp/conformance.sh — CL-on-SX conformance test runner
|
||||
#
|
||||
# Runs all Common Lisp test suites and writes scoreboard.json + scoreboard.md.
|
||||
#
|
||||
# Usage:
|
||||
# bash lib/common-lisp/conformance.sh
|
||||
# bash lib/common-lisp/conformance.sh -v
|
||||
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
||||
fi
|
||||
if [ ! -x "$SX_SERVER" ]; then
|
||||
echo "ERROR: sx_server.exe not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERBOSE="${1:-}"
|
||||
TOTAL_PASS=0; TOTAL_FAIL=0
|
||||
SUITE_NAMES=()
|
||||
SUITE_PASS=()
|
||||
SUITE_FAIL=()
|
||||
|
||||
# run_suite NAME "file1 file2 ..." PASS_VAR FAIL_VAR FAILURES_VAR
|
||||
run_suite() {
|
||||
local name="$1" load_files="$2" pass_var="$3" fail_var="$4" failures_var="$5"
|
||||
local TMP; TMP=$(mktemp)
|
||||
{
|
||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n'
|
||||
local i=2
|
||||
for f in $load_files; do
|
||||
printf '(epoch %d)\n(load "%s")\n' "$i" "$f"
|
||||
i=$((i+1))
|
||||
done
|
||||
printf '(epoch 100)\n(eval "%s")\n' "$pass_var"
|
||||
printf '(epoch 101)\n(eval "%s")\n' "$fail_var"
|
||||
} > "$TMP"
|
||||
local OUT; OUT=$(timeout 30 "$SX_SERVER" < "$TMP" 2>/dev/null)
|
||||
rm -f "$TMP"
|
||||
local P F
|
||||
P=$(echo "$OUT" | grep -A1 "^(ok-len 100 " | tail -1 | tr -d ' ()' || true)
|
||||
F=$(echo "$OUT" | grep -A1 "^(ok-len 101 " | tail -1 | tr -d ' ()' || true)
|
||||
# Also try plain (ok 100 N) format
|
||||
[ -z "$P" ] && P=$(echo "$OUT" | grep "^(ok 100 " | awk '{print $3}' | tr -d ')' || true)
|
||||
[ -z "$F" ] && F=$(echo "$OUT" | grep "^(ok 101 " | awk '{print $3}' | tr -d ')' || true)
|
||||
[ -z "$P" ] && P=0; [ -z "$F" ] && F=0
|
||||
SUITE_NAMES+=("$name")
|
||||
SUITE_PASS+=("$P")
|
||||
SUITE_FAIL+=("$F")
|
||||
TOTAL_PASS=$((TOTAL_PASS + P))
|
||||
TOTAL_FAIL=$((TOTAL_FAIL + F))
|
||||
if [ "$F" = "0" ] && [ "${P:-0}" -gt 0 ] 2>/dev/null; then
|
||||
echo " PASS $name ($P tests)"
|
||||
else
|
||||
echo " FAIL $name ($P passed, $F failed)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Common Lisp on SX — Conformance Run ==="
|
||||
echo ""
|
||||
|
||||
run_suite "Phase 1: tokenizer/reader" \
|
||||
"lib/common-lisp/reader.sx lib/common-lisp/tests/read.sx" \
|
||||
"cl-test-pass" "cl-test-fail" "cl-test-fails"
|
||||
|
||||
run_suite "Phase 1: parser/lambda-lists" \
|
||||
"lib/common-lisp/reader.sx lib/common-lisp/parser.sx lib/common-lisp/tests/lambda.sx" \
|
||||
"cl-test-pass" "cl-test-fail" "cl-test-fails"
|
||||
|
||||
run_suite "Phase 2: evaluator" \
|
||||
"lib/common-lisp/reader.sx lib/common-lisp/parser.sx lib/common-lisp/eval.sx lib/common-lisp/tests/eval.sx" \
|
||||
"cl-test-pass" "cl-test-fail" "cl-test-fails"
|
||||
|
||||
run_suite "Phase 3: condition system" \
|
||||
"lib/common-lisp/runtime.sx lib/common-lisp/tests/conditions.sx" \
|
||||
"passed" "failed" "failures"
|
||||
|
||||
run_suite "Phase 3: restart-demo" \
|
||||
"lib/common-lisp/runtime.sx lib/common-lisp/tests/programs/restart-demo.sx" \
|
||||
"demo-passed" "demo-failed" "demo-failures"
|
||||
|
||||
run_suite "Phase 3: parse-recover" \
|
||||
"lib/common-lisp/runtime.sx lib/common-lisp/tests/programs/parse-recover.sx" \
|
||||
"parse-passed" "parse-failed" "parse-failures"
|
||||
|
||||
run_suite "Phase 3: interactive-debugger" \
|
||||
"lib/common-lisp/runtime.sx lib/common-lisp/tests/programs/interactive-debugger.sx" \
|
||||
"debugger-passed" "debugger-failed" "debugger-failures"
|
||||
|
||||
run_suite "Phase 4: CLOS" \
|
||||
"lib/common-lisp/runtime.sx lib/common-lisp/clos.sx lib/common-lisp/tests/clos.sx" \
|
||||
"passed" "failed" "failures"
|
||||
|
||||
run_suite "Phase 4: geometry" \
|
||||
"lib/common-lisp/runtime.sx lib/common-lisp/clos.sx lib/common-lisp/tests/programs/geometry.sx" \
|
||||
"geo-passed" "geo-failed" "geo-failures"
|
||||
|
||||
run_suite "Phase 4: mop-trace" \
|
||||
"lib/common-lisp/runtime.sx lib/common-lisp/clos.sx lib/common-lisp/tests/programs/mop-trace.sx" \
|
||||
"mop-passed" "mop-failed" "mop-failures"
|
||||
|
||||
run_suite "Phase 5: macros+LOOP" \
|
||||
"lib/common-lisp/reader.sx lib/common-lisp/parser.sx lib/common-lisp/eval.sx lib/common-lisp/loop.sx lib/common-lisp/tests/macros.sx" \
|
||||
"macro-passed" "macro-failed" "macro-failures"
|
||||
|
||||
run_suite "Phase 6: stdlib" \
|
||||
"lib/common-lisp/reader.sx lib/common-lisp/parser.sx lib/common-lisp/eval.sx lib/common-lisp/tests/stdlib.sx" \
|
||||
"stdlib-passed" "stdlib-failed" "stdlib-failures"
|
||||
|
||||
echo ""
|
||||
echo "=== Total: $TOTAL_PASS passed, $TOTAL_FAIL failed ==="
|
||||
|
||||
# ── write scoreboard.json ─────────────────────────────────────────────────
|
||||
|
||||
SCORE_DIR="lib/common-lisp"
|
||||
JSON="$SCORE_DIR/scoreboard.json"
|
||||
{
|
||||
printf '{\n'
|
||||
printf ' "generated": "%s",\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
printf ' "total_pass": %d,\n' "$TOTAL_PASS"
|
||||
printf ' "total_fail": %d,\n' "$TOTAL_FAIL"
|
||||
printf ' "suites": [\n'
|
||||
first=true
|
||||
for i in "${!SUITE_NAMES[@]}"; do
|
||||
if [ "$first" = "true" ]; then first=false; else printf ',\n'; fi
|
||||
printf ' {"name": "%s", "pass": %d, "fail": %d}' \
|
||||
"${SUITE_NAMES[$i]}" "${SUITE_PASS[$i]}" "${SUITE_FAIL[$i]}"
|
||||
done
|
||||
printf '\n ]\n'
|
||||
printf '}\n'
|
||||
} > "$JSON"
|
||||
|
||||
# ── write scoreboard.md ───────────────────────────────────────────────────
|
||||
|
||||
MD="$SCORE_DIR/scoreboard.md"
|
||||
{
|
||||
printf '# Common Lisp on SX — Scoreboard\n\n'
|
||||
printf '_Generated: %s_\n\n' "$(date -u '+%Y-%m-%d %H:%M UTC')"
|
||||
printf '| Suite | Pass | Fail | Status |\n'
|
||||
printf '|-------|------|------|--------|\n'
|
||||
for i in "${!SUITE_NAMES[@]}"; do
|
||||
p="${SUITE_PASS[$i]}" f="${SUITE_FAIL[$i]}"
|
||||
status=""
|
||||
if [ "$f" = "0" ] && [ "${p:-0}" -gt 0 ] 2>/dev/null; then
|
||||
status="pass"
|
||||
else
|
||||
status="FAIL"
|
||||
fi
|
||||
printf '| %s | %s | %s | %s |\n' "${SUITE_NAMES[$i]}" "$p" "$f" "$status"
|
||||
done
|
||||
printf '\n**Total: %d passed, %d failed**\n' "$TOTAL_PASS" "$TOTAL_FAIL"
|
||||
} > "$MD"
|
||||
|
||||
echo ""
|
||||
echo "Scoreboard written to $JSON and $MD"
|
||||
|
||||
[ "$TOTAL_FAIL" -eq 0 ]
|
||||
1391
lib/common-lisp/eval.sx
Normal file
1391
lib/common-lisp/eval.sx
Normal file
File diff suppressed because it is too large
Load Diff
623
lib/common-lisp/loop.sx
Normal file
623
lib/common-lisp/loop.sx
Normal file
@@ -0,0 +1,623 @@
|
||||
;; lib/common-lisp/loop.sx — The LOOP macro for CL-on-SX
|
||||
;;
|
||||
;; Supported clauses:
|
||||
;; for VAR in LIST — iterate over list
|
||||
;; for VAR across VECTOR — alias for 'in'
|
||||
;; for VAR from N — numeric iteration (to/upto/below/downto/above/by)
|
||||
;; for VAR = EXPR [then EXPR] — general iteration
|
||||
;; while COND — stop when false
|
||||
;; until COND — stop when true
|
||||
;; repeat N — repeat N times
|
||||
;; collect EXPR [into VAR]
|
||||
;; append EXPR [into VAR]
|
||||
;; nconc EXPR [into VAR]
|
||||
;; sum EXPR [into VAR]
|
||||
;; count EXPR [into VAR]
|
||||
;; maximize EXPR [into VAR]
|
||||
;; minimize EXPR [into VAR]
|
||||
;; do FORM...
|
||||
;; when/if COND clause...
|
||||
;; unless COND clause...
|
||||
;; finally FORM...
|
||||
;; always COND
|
||||
;; never COND
|
||||
;; thereis COND
|
||||
;; named BLOCK-NAME
|
||||
;;
|
||||
;; Depends on: lib/common-lisp/runtime.sx, lib/common-lisp/eval.sx already loaded.
|
||||
;; Uses defmacro in the CL evaluator.
|
||||
|
||||
;; ── LOOP expansion driver ─────────────────────────────────────────────────
|
||||
|
||||
;; cl-loop-parse: analyse the flat LOOP clause list and build a Lisp form.
|
||||
;; Returns a (block NAME (let (...) (tagbody ...))) form.
|
||||
(define
|
||||
cl-loop-parse
|
||||
(fn
|
||||
(clauses)
|
||||
(define block-name nil)
|
||||
(define with-bindings (list))
|
||||
(define for-bindings (list))
|
||||
(define test-forms (list))
|
||||
(define repeat-var nil)
|
||||
(define repeat-count nil)
|
||||
(define body-forms (list))
|
||||
(define accum-vars (dict))
|
||||
(define accum-clauses (dict))
|
||||
(define result-var nil)
|
||||
(define finally-forms (list))
|
||||
(define return-expr nil)
|
||||
(define termination nil)
|
||||
(define idx 0)
|
||||
(define (lp-peek) (if (< idx (len clauses)) (nth clauses idx) nil))
|
||||
(define
|
||||
(next!)
|
||||
(let ((v (lp-peek))) (do (set! idx (+ idx 1)) v)))
|
||||
(define
|
||||
(skip-if pred)
|
||||
(if (and (not (nil? (lp-peek))) (pred (lp-peek))) (next!) nil))
|
||||
(define (upcase-str s) (if (string? s) (upcase s) s))
|
||||
(define (kw? s k) (= (upcase-str s) k))
|
||||
(define
|
||||
(make-accum-var!)
|
||||
(if
|
||||
(nil? result-var)
|
||||
(do (set! result-var "#LOOP-RESULT") result-var)
|
||||
result-var))
|
||||
(define
|
||||
(add-accum! type expr into-var)
|
||||
(let
|
||||
((v (if (nil? into-var) (make-accum-var!) into-var)))
|
||||
(if
|
||||
(not (has-key? accum-vars v))
|
||||
(do
|
||||
(set!
|
||||
accum-vars
|
||||
(assoc
|
||||
accum-vars
|
||||
v
|
||||
(cond
|
||||
((= type ":sum") 0)
|
||||
((= type ":count") 0)
|
||||
((= type ":maximize") nil)
|
||||
((= type ":minimize") nil)
|
||||
(:else (list)))))
|
||||
(set! accum-clauses (assoc accum-clauses v type))))
|
||||
(let
|
||||
((update (cond ((= type ":collect") (list "SETQ" v (list "APPEND" v (list "LIST" expr)))) ((= type ":append") (list "SETQ" v (list "APPEND" v expr))) ((= type ":nconc") (list "SETQ" v (list "NCONC" v expr))) ((= type ":sum") (list "SETQ" v (list "+" v expr))) ((= type ":count") (list "SETQ" v (list "+" v (list "IF" expr 1 0)))) ((= type ":maximize") (list "SETQ" v (list "IF" (list "OR" (list "NULL" v) (list ">" expr v)) expr v))) ((= type ":minimize") (list "SETQ" v (list "IF" (list "OR" (list "NULL" v) (list "<" expr v)) expr v))) (:else (list "SETQ" v (list "APPEND" v (list "LIST" expr)))))))
|
||||
(set! body-forms (append body-forms (list update))))))
|
||||
(define
|
||||
(parse-clause!)
|
||||
(let
|
||||
((tok (lp-peek)))
|
||||
(if
|
||||
(nil? tok)
|
||||
nil
|
||||
(do
|
||||
(let
|
||||
((u (upcase-str tok)))
|
||||
(cond
|
||||
((= u "NAMED")
|
||||
(do (next!) (set! block-name (next!)) (parse-clause!)))
|
||||
((= u "WITH")
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((var (next!)))
|
||||
(skip-if (fn (s) (kw? s "=")))
|
||||
(let
|
||||
((init (next!)))
|
||||
(set!
|
||||
with-bindings
|
||||
(append with-bindings (list (list var init))))
|
||||
(parse-clause!)))))
|
||||
((= u "FOR")
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((var (next!)))
|
||||
(let
|
||||
((kw2 (upcase-str (lp-peek))))
|
||||
(cond
|
||||
((or (= kw2 "IN") (= kw2 "ACROSS"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((lst-expr (next!))
|
||||
(tail-var (str "#TAIL-" var)))
|
||||
(set!
|
||||
for-bindings
|
||||
(append for-bindings (list {:list lst-expr :tail tail-var :type ":list" :var var})))
|
||||
(parse-clause!))))
|
||||
((= kw2 "=")
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((init-expr (next!)))
|
||||
(let
|
||||
((then-expr (if (kw? (lp-peek) "THEN") (do (next!) (next!)) init-expr)))
|
||||
(set!
|
||||
for-bindings
|
||||
(append for-bindings (list {:type ":general" :then then-expr :init init-expr :var var})))
|
||||
(parse-clause!)))))
|
||||
((or (= kw2 "FROM") (= kw2 "DOWNFROM") (= kw2 "UPFROM"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((from-expr (next!))
|
||||
(dir (if (= kw2 "DOWNFROM") ":down" ":up"))
|
||||
(limit-expr nil)
|
||||
(limit-type nil)
|
||||
(step-expr 1))
|
||||
(let
|
||||
((lkw (upcase-str (lp-peek))))
|
||||
(when
|
||||
(or
|
||||
(= lkw "TO")
|
||||
(= lkw "UPTO")
|
||||
(= lkw "BELOW")
|
||||
(= lkw "DOWNTO")
|
||||
(= lkw "ABOVE"))
|
||||
(do
|
||||
(next!)
|
||||
(set! limit-type lkw)
|
||||
(set! limit-expr (next!)))))
|
||||
(when
|
||||
(kw? (lp-peek) "BY")
|
||||
(do (next!) (set! step-expr (next!))))
|
||||
(set!
|
||||
for-bindings
|
||||
(append for-bindings (list {:dir dir :step step-expr :from from-expr :type ":numeric" :limit-type limit-type :var var :limit limit-expr})))
|
||||
(parse-clause!))))
|
||||
((or (= kw2 "TO") (= kw2 "UPTO") (= kw2 "BELOW"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((limit-expr (next!))
|
||||
(step-expr 1))
|
||||
(when
|
||||
(kw? (lp-peek) "BY")
|
||||
(do (next!) (set! step-expr (next!))))
|
||||
(set!
|
||||
for-bindings
|
||||
(append for-bindings (list {:dir ":up" :step step-expr :from 0 :type ":numeric" :limit-type kw2 :var var :limit limit-expr})))
|
||||
(parse-clause!))))
|
||||
(:else (do (parse-clause!))))))))
|
||||
((= u "WHILE")
|
||||
(do
|
||||
(next!)
|
||||
(set! test-forms (append test-forms (list {:expr (next!) :type ":while"})))
|
||||
(parse-clause!)))
|
||||
((= u "UNTIL")
|
||||
(do
|
||||
(next!)
|
||||
(set! test-forms (append test-forms (list {:expr (next!) :type ":until"})))
|
||||
(parse-clause!)))
|
||||
((= u "REPEAT")
|
||||
(do
|
||||
(next!)
|
||||
(set! repeat-count (next!))
|
||||
(set! repeat-var "#REPEAT-COUNT")
|
||||
(parse-clause!)))
|
||||
((or (= u "COLLECT") (= u "COLLECTING"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((expr (next!)) (into-var nil))
|
||||
(when
|
||||
(kw? (lp-peek) "INTO")
|
||||
(do (next!) (set! into-var (next!))))
|
||||
(add-accum! ":collect" expr into-var)
|
||||
(parse-clause!))))
|
||||
((or (= u "APPEND") (= u "APPENDING"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((expr (next!)) (into-var nil))
|
||||
(when
|
||||
(kw? (lp-peek) "INTO")
|
||||
(do (next!) (set! into-var (next!))))
|
||||
(add-accum! ":append" expr into-var)
|
||||
(parse-clause!))))
|
||||
((or (= u "NCONC") (= u "NCONCING"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((expr (next!)) (into-var nil))
|
||||
(when
|
||||
(kw? (lp-peek) "INTO")
|
||||
(do (next!) (set! into-var (next!))))
|
||||
(add-accum! ":nconc" expr into-var)
|
||||
(parse-clause!))))
|
||||
((or (= u "SUM") (= u "SUMMING"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((expr (next!)) (into-var nil))
|
||||
(when
|
||||
(kw? (lp-peek) "INTO")
|
||||
(do (next!) (set! into-var (next!))))
|
||||
(add-accum! ":sum" expr into-var)
|
||||
(parse-clause!))))
|
||||
((or (= u "COUNT") (= u "COUNTING"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((expr (next!)) (into-var nil))
|
||||
(when
|
||||
(kw? (lp-peek) "INTO")
|
||||
(do (next!) (set! into-var (next!))))
|
||||
(add-accum! ":count" expr into-var)
|
||||
(parse-clause!))))
|
||||
((or (= u "MAXIMIZE") (= u "MAXIMIZING"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((expr (next!)) (into-var nil))
|
||||
(when
|
||||
(kw? (lp-peek) "INTO")
|
||||
(do (next!) (set! into-var (next!))))
|
||||
(add-accum! ":maximize" expr into-var)
|
||||
(parse-clause!))))
|
||||
((or (= u "MINIMIZE") (= u "MINIMIZING"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((expr (next!)) (into-var nil))
|
||||
(when
|
||||
(kw? (lp-peek) "INTO")
|
||||
(do (next!) (set! into-var (next!))))
|
||||
(add-accum! ":minimize" expr into-var)
|
||||
(parse-clause!))))
|
||||
((= u "DO")
|
||||
(do
|
||||
(next!)
|
||||
(define
|
||||
(loop-kw? s)
|
||||
(let
|
||||
((us (upcase-str s)))
|
||||
(some
|
||||
(fn (k) (= us k))
|
||||
(list
|
||||
"FOR"
|
||||
"WITH"
|
||||
"WHILE"
|
||||
"UNTIL"
|
||||
"REPEAT"
|
||||
"COLLECT"
|
||||
"COLLECTING"
|
||||
"APPEND"
|
||||
"APPENDING"
|
||||
"NCONC"
|
||||
"NCONCING"
|
||||
"SUM"
|
||||
"SUMMING"
|
||||
"COUNT"
|
||||
"COUNTING"
|
||||
"MAXIMIZE"
|
||||
"MAXIMIZING"
|
||||
"MINIMIZE"
|
||||
"MINIMIZING"
|
||||
"DO"
|
||||
"WHEN"
|
||||
"IF"
|
||||
"UNLESS"
|
||||
"FINALLY"
|
||||
"ALWAYS"
|
||||
"NEVER"
|
||||
"THEREIS"
|
||||
"RETURN"
|
||||
"NAMED"))))
|
||||
(define
|
||||
(collect-do-forms!)
|
||||
(if
|
||||
(or (nil? (lp-peek)) (loop-kw? (lp-peek)))
|
||||
nil
|
||||
(do
|
||||
(set!
|
||||
body-forms
|
||||
(append body-forms (list (next!))))
|
||||
(collect-do-forms!))))
|
||||
(collect-do-forms!)
|
||||
(parse-clause!)))
|
||||
((or (= u "WHEN") (= u "IF"))
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((cond-expr (next!))
|
||||
(body-start (len body-forms)))
|
||||
(parse-clause!)
|
||||
;; wrap forms added since body-start in (WHEN cond ...)
|
||||
(when (> (len body-forms) body-start)
|
||||
(let ((added (list (nth body-forms body-start))))
|
||||
(set! body-forms
|
||||
(append
|
||||
(if (> body-start 0)
|
||||
(list (nth body-forms (- body-start 1)))
|
||||
(list))
|
||||
(list (list "WHEN" cond-expr (first added)))))
|
||||
nil)))))
|
||||
((= u "UNLESS")
|
||||
(do
|
||||
(next!)
|
||||
(let
|
||||
((cond-expr (next!))
|
||||
(body-start (len body-forms)))
|
||||
(parse-clause!)
|
||||
(when (> (len body-forms) body-start)
|
||||
(let ((added (list (nth body-forms body-start))))
|
||||
(set! body-forms
|
||||
(append
|
||||
(if (> body-start 0)
|
||||
(list (nth body-forms (- body-start 1)))
|
||||
(list))
|
||||
(list (list "UNLESS" cond-expr (first added)))))
|
||||
nil)))))
|
||||
((= u "ALWAYS")
|
||||
(do (next!) (set! termination {:expr (next!) :type ":always"}) (parse-clause!)))
|
||||
((= u "NEVER")
|
||||
(do (next!) (set! termination {:expr (next!) :type ":never"}) (parse-clause!)))
|
||||
((= u "THEREIS")
|
||||
(do (next!) (set! termination {:expr (next!) :type ":thereis"}) (parse-clause!)))
|
||||
((= u "RETURN")
|
||||
(do (next!) (set! return-expr (next!)) (parse-clause!)))
|
||||
((= u "FINALLY")
|
||||
(do
|
||||
(next!)
|
||||
(define
|
||||
(collect-finally!)
|
||||
(if
|
||||
(nil? (lp-peek))
|
||||
nil
|
||||
(do
|
||||
(set!
|
||||
finally-forms
|
||||
(append finally-forms (list (next!))))
|
||||
(collect-finally!))))
|
||||
(collect-finally!)
|
||||
(parse-clause!)))
|
||||
(:else
|
||||
(do
|
||||
(set! body-forms (append body-forms (list (next!))))
|
||||
(parse-clause!)))))))))
|
||||
(parse-clause!)
|
||||
(define let-bindings (list))
|
||||
(for-each
|
||||
(fn (wb) (set! let-bindings (append let-bindings (list wb))))
|
||||
with-bindings)
|
||||
(for-each
|
||||
(fn
|
||||
(v)
|
||||
(set!
|
||||
let-bindings
|
||||
(append let-bindings (list (list v (get accum-vars v))))))
|
||||
(keys accum-vars))
|
||||
(when
|
||||
(not (nil? repeat-var))
|
||||
(set!
|
||||
let-bindings
|
||||
(append let-bindings (list (list repeat-var repeat-count)))))
|
||||
(for-each
|
||||
(fn
|
||||
(fb)
|
||||
(let
|
||||
((type (get fb "type")))
|
||||
(cond
|
||||
((= type ":list")
|
||||
(do
|
||||
(set!
|
||||
let-bindings
|
||||
(append
|
||||
let-bindings
|
||||
(list (list (get fb "tail") (get fb "list")))
|
||||
(list
|
||||
(list
|
||||
(get fb "var")
|
||||
(list
|
||||
"IF"
|
||||
(list "CONSP" (get fb "tail"))
|
||||
(list "CAR" (get fb "tail"))
|
||||
nil)))))
|
||||
nil))
|
||||
((= type ":numeric")
|
||||
(set!
|
||||
let-bindings
|
||||
(append
|
||||
let-bindings
|
||||
(list (list (get fb "var") (get fb "from"))))))
|
||||
((= type ":general")
|
||||
(set!
|
||||
let-bindings
|
||||
(append
|
||||
let-bindings
|
||||
(list (list (get fb "var") (get fb "init"))))))
|
||||
(:else nil))))
|
||||
for-bindings)
|
||||
(define all-tests (list))
|
||||
(when
|
||||
(not (nil? repeat-var))
|
||||
(set!
|
||||
all-tests
|
||||
(append
|
||||
all-tests
|
||||
(list
|
||||
(list
|
||||
"WHEN"
|
||||
(list "<=" repeat-var 0)
|
||||
(list "RETURN-FROM" block-name (if (nil? result-var) nil result-var))))))
|
||||
(set!
|
||||
body-forms
|
||||
(append
|
||||
(list (list "SETQ" repeat-var (list "-" repeat-var 1)))
|
||||
body-forms)))
|
||||
(for-each
|
||||
(fn
|
||||
(fb)
|
||||
(when
|
||||
(= (get fb "type") ":list")
|
||||
(let
|
||||
((tvar (get fb "tail")) (var (get fb "var")))
|
||||
(set!
|
||||
all-tests
|
||||
(append
|
||||
all-tests
|
||||
(list
|
||||
(list
|
||||
"WHEN"
|
||||
(list "NULL" tvar)
|
||||
(list
|
||||
"RETURN-FROM"
|
||||
block-name
|
||||
(if (nil? result-var) nil result-var))))))
|
||||
(set!
|
||||
body-forms
|
||||
(append
|
||||
body-forms
|
||||
(list
|
||||
(list "SETQ" tvar (list "CDR" tvar))
|
||||
(list
|
||||
"SETQ"
|
||||
var
|
||||
(list "IF" (list "CONSP" tvar) (list "CAR" tvar) nil))))))))
|
||||
for-bindings)
|
||||
(for-each
|
||||
(fn
|
||||
(fb)
|
||||
(when
|
||||
(= (get fb "type") ":numeric")
|
||||
(let
|
||||
((var (get fb "var"))
|
||||
(dir (get fb "dir"))
|
||||
(lim (get fb "limit"))
|
||||
(ltype (get fb "limit-type"))
|
||||
(step (get fb "step")))
|
||||
(when
|
||||
(not (nil? lim))
|
||||
(let
|
||||
((test-op (cond ((or (= ltype "BELOW") (= ltype "ABOVE")) (if (= dir ":up") ">=" "<=")) ((or (= ltype "TO") (= ltype "UPTO")) ">") ((= ltype "DOWNTO") "<") (:else (if (= dir ":up") ">" "<")))))
|
||||
(set!
|
||||
all-tests
|
||||
(append
|
||||
all-tests
|
||||
(list
|
||||
(list
|
||||
"WHEN"
|
||||
(list test-op var lim)
|
||||
(list
|
||||
"RETURN-FROM"
|
||||
block-name
|
||||
(if (nil? result-var) nil result-var))))))))
|
||||
(let
|
||||
((step-op (if (or (= dir ":down") (= ltype "DOWNTO") (= ltype "ABOVE")) "-" "+")))
|
||||
(set!
|
||||
body-forms
|
||||
(append
|
||||
body-forms
|
||||
(list (list "SETQ" var (list step-op var step)))))))))
|
||||
for-bindings)
|
||||
(for-each
|
||||
(fn
|
||||
(fb)
|
||||
(when
|
||||
(= (get fb "type") ":general")
|
||||
(set!
|
||||
body-forms
|
||||
(append
|
||||
body-forms
|
||||
(list (list "SETQ" (get fb "var") (get fb "then")))))))
|
||||
for-bindings)
|
||||
(for-each
|
||||
(fn
|
||||
(t)
|
||||
(let
|
||||
((type (get t "type")) (expr (get t "expr")))
|
||||
(if
|
||||
(= type ":while")
|
||||
(set!
|
||||
all-tests
|
||||
(append
|
||||
all-tests
|
||||
(list
|
||||
(list
|
||||
"WHEN"
|
||||
(list "NOT" expr)
|
||||
(list
|
||||
"RETURN-FROM"
|
||||
block-name
|
||||
(if (nil? result-var) nil result-var))))))
|
||||
(set!
|
||||
all-tests
|
||||
(append
|
||||
all-tests
|
||||
(list
|
||||
(list
|
||||
"WHEN"
|
||||
expr
|
||||
(list
|
||||
"RETURN-FROM"
|
||||
block-name
|
||||
(if (nil? result-var) nil result-var)))))))))
|
||||
test-forms)
|
||||
(when
|
||||
(not (nil? termination))
|
||||
(let
|
||||
((type (get termination "type")) (expr (get termination "expr")))
|
||||
(cond
|
||||
((= type ":always")
|
||||
(set!
|
||||
body-forms
|
||||
(append
|
||||
body-forms
|
||||
(list
|
||||
(list "UNLESS" expr (list "RETURN-FROM" block-name false)))))
|
||||
(set! return-expr true))
|
||||
((= type ":never")
|
||||
(set!
|
||||
body-forms
|
||||
(append
|
||||
body-forms
|
||||
(list
|
||||
(list "WHEN" expr (list "RETURN-FROM" block-name false)))))
|
||||
(set! return-expr true))
|
||||
((= type ":thereis")
|
||||
(set!
|
||||
body-forms
|
||||
(append
|
||||
body-forms
|
||||
(list
|
||||
(list "WHEN" expr (list "RETURN-FROM" block-name expr)))))))))
|
||||
(define tag "#LOOP-START")
|
||||
(define
|
||||
inner-body
|
||||
(append (list tag) all-tests body-forms (list (list "GO" tag))))
|
||||
(define
|
||||
result-form
|
||||
(cond
|
||||
((not (nil? return-expr)) return-expr)
|
||||
((not (nil? result-var)) result-var)
|
||||
(:else nil)))
|
||||
(define
|
||||
full-body
|
||||
(if
|
||||
(= (len let-bindings) 0)
|
||||
(append
|
||||
(list "PROGN")
|
||||
(list (append (list "TAGBODY") inner-body))
|
||||
finally-forms
|
||||
(list result-form))
|
||||
(list
|
||||
"LET*"
|
||||
let-bindings
|
||||
(append (list "TAGBODY") inner-body)
|
||||
(append (list "PROGN") finally-forms (list result-form)))))
|
||||
(list "BLOCK" block-name full-body)))
|
||||
|
||||
;; ── Install LOOP as a CL macro ────────────────────────────────────────────
|
||||
;;
|
||||
;; (loop ...) — the form arrives with head "LOOP" and rest = clauses.
|
||||
;; The macro fn receives the full form.
|
||||
|
||||
(dict-set!
|
||||
cl-macro-registry
|
||||
"LOOP"
|
||||
(fn (form env) (cl-loop-parse (rest form))))
|
||||
377
lib/common-lisp/parser.sx
Normal file
377
lib/common-lisp/parser.sx
Normal file
@@ -0,0 +1,377 @@
|
||||
;; Common Lisp reader — converts token stream to CL AST forms.
|
||||
;;
|
||||
;; Depends on: lib/common-lisp/reader.sx (cl-tokenize)
|
||||
;;
|
||||
;; AST representation:
|
||||
;; integer/float → SX number (or {:cl-type "float"/:ratio ...})
|
||||
;; string "hello" → {:cl-type "string" :value "hello"}
|
||||
;; symbol FOO → SX string "FOO" (upcase)
|
||||
;; symbol NIL → nil
|
||||
;; symbol T → true
|
||||
;; :keyword → {:cl-type "keyword" :name "FOO"}
|
||||
;; #\char → {:cl-type "char" :value "a"}
|
||||
;; #:uninterned → {:cl-type "uninterned" :name "FOO"}
|
||||
;; ratio 1/3 → {:cl-type "ratio" :value "1/3"}
|
||||
;; float 3.14 → {:cl-type "float" :value "3.14"}
|
||||
;; proper list (a b c) → SX list (a b c)
|
||||
;; dotted pair (a . b) → {:cl-type "cons" :car a :cdr b}
|
||||
;; vector #(a b) → {:cl-type "vector" :elements (list a b)}
|
||||
;; 'x → ("QUOTE" x)
|
||||
;; `x → ("QUASIQUOTE" x)
|
||||
;; ,x → ("UNQUOTE" x)
|
||||
;; ,@x → ("UNQUOTE-SPLICING" x)
|
||||
;; #'x → ("FUNCTION" x)
|
||||
;;
|
||||
;; Public API:
|
||||
;; (cl-read src) — parse first form from string, return form
|
||||
;; (cl-read-all src) — parse all top-level forms, return list
|
||||
|
||||
;; ── number conversion ─────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-hex-val
|
||||
(fn
|
||||
(c)
|
||||
(let
|
||||
((o (cl-ord c)))
|
||||
(cond
|
||||
((and (>= o 48) (<= o 57)) (- o 48))
|
||||
((and (>= o 65) (<= o 70)) (+ 10 (- o 65)))
|
||||
((and (>= o 97) (<= o 102)) (+ 10 (- o 97)))
|
||||
(:else 0)))))
|
||||
|
||||
(define
|
||||
cl-parse-radix-str
|
||||
(fn
|
||||
(s radix start)
|
||||
(let
|
||||
((n (string-length s)) (i start) (acc 0))
|
||||
(define
|
||||
loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< i n)
|
||||
(do
|
||||
(set! acc (+ (* acc radix) (cl-hex-val (substring s i (+ i 1)))))
|
||||
(set! i (+ i 1))
|
||||
(loop)))))
|
||||
(loop)
|
||||
acc)))
|
||||
|
||||
(define
|
||||
cl-convert-integer
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((n (string-length s)) (neg false))
|
||||
(cond
|
||||
((and (> n 2) (= (substring s 0 1) "#"))
|
||||
(let
|
||||
((letter (downcase (substring s 1 2))))
|
||||
(cond
|
||||
((= letter "x") (cl-parse-radix-str s 16 2))
|
||||
((= letter "b") (cl-parse-radix-str s 2 2))
|
||||
((= letter "o") (cl-parse-radix-str s 8 2))
|
||||
(:else (parse-int s 0)))))
|
||||
(:else (parse-int s 0))))))
|
||||
|
||||
;; ── reader ────────────────────────────────────────────────────────
|
||||
|
||||
;; Read one form from token list.
|
||||
;; Returns {:form F :rest remaining-toks} or {:form nil :rest toks :eof true}
|
||||
(define
|
||||
cl-read-form
|
||||
(fn
|
||||
(toks)
|
||||
(if
|
||||
(not toks)
|
||||
{:form nil :rest toks :eof true}
|
||||
(let
|
||||
((tok (nth toks 0)) (nxt (rest toks)))
|
||||
(let
|
||||
((type (get tok "type")) (val (get tok "value")))
|
||||
(cond
|
||||
((= type "eof") {:form nil :rest toks :eof true})
|
||||
((= type "integer") {:form (cl-convert-integer val) :rest nxt})
|
||||
((= type "float") {:form {:cl-type "float" :value val} :rest nxt})
|
||||
((= type "ratio") {:form {:cl-type "ratio" :value val} :rest nxt})
|
||||
((= type "string") {:form {:cl-type "string" :value val} :rest nxt})
|
||||
((= type "char") {:form {:cl-type "char" :value val} :rest nxt})
|
||||
((= type "keyword") {:form {:cl-type "keyword" :name val} :rest nxt})
|
||||
((= type "uninterned") {:form {:cl-type "uninterned" :name val} :rest nxt})
|
||||
((= type "symbol")
|
||||
(cond
|
||||
((= val "NIL") {:form nil :rest nxt})
|
||||
((= val "T") {:form true :rest nxt})
|
||||
(:else {:form val :rest nxt})))
|
||||
;; list forms
|
||||
((= type "lparen") (cl-read-list nxt))
|
||||
((= type "hash-paren") (cl-read-vector nxt))
|
||||
;; reader macros that wrap the next form
|
||||
((= type "quote") (cl-read-wrap "QUOTE" nxt))
|
||||
((= type "backquote") (cl-read-wrap "QUASIQUOTE" nxt))
|
||||
((= type "comma") (cl-read-wrap "UNQUOTE" nxt))
|
||||
((= type "comma-at") (cl-read-wrap "UNQUOTE-SPLICING" nxt))
|
||||
((= type "hash-quote") (cl-read-wrap "FUNCTION" nxt))
|
||||
;; skip unrecognised tokens
|
||||
(:else (cl-read-form nxt))))))))
|
||||
|
||||
;; Wrap next form in a list: (name form)
|
||||
(define
|
||||
cl-read-wrap
|
||||
(fn
|
||||
(name toks)
|
||||
(let
|
||||
((inner (cl-read-form toks)))
|
||||
{:form (list name (get inner "form")) :rest (get inner "rest")})))
|
||||
|
||||
;; Read list forms until ')'; handles dotted pair (a . b)
|
||||
;; Called after consuming '('
|
||||
(define
|
||||
cl-read-list
|
||||
(fn
|
||||
(toks)
|
||||
(let
|
||||
((result (cl-read-list-items toks (list))))
|
||||
{:form (get result "items") :rest (get result "rest")})))
|
||||
|
||||
(define
|
||||
cl-read-list-items
|
||||
(fn
|
||||
(toks acc)
|
||||
(if
|
||||
(not toks)
|
||||
{:items acc :rest toks}
|
||||
(let
|
||||
((tok (nth toks 0)))
|
||||
(let
|
||||
((type (get tok "type")))
|
||||
(cond
|
||||
((= type "eof") {:items acc :rest toks})
|
||||
((= type "rparen") {:items acc :rest (rest toks)})
|
||||
;; dotted pair: read one more form then expect ')'
|
||||
((= type "dot")
|
||||
(let
|
||||
((cdr-result (cl-read-form (rest toks))))
|
||||
(let
|
||||
((cdr-form (get cdr-result "form"))
|
||||
(after-cdr (get cdr-result "rest")))
|
||||
;; skip the closing ')'
|
||||
(let
|
||||
((close (if after-cdr (nth after-cdr 0) nil)))
|
||||
(let
|
||||
((remaining
|
||||
(if
|
||||
(and close (= (get close "type") "rparen"))
|
||||
(rest after-cdr)
|
||||
after-cdr)))
|
||||
;; build dotted structure
|
||||
(let
|
||||
((dotted (cl-build-dotted acc cdr-form)))
|
||||
{:items dotted :rest remaining}))))))
|
||||
(:else
|
||||
(let
|
||||
((item (cl-read-form toks)))
|
||||
(cl-read-list-items
|
||||
(get item "rest")
|
||||
(concat acc (list (get item "form"))))))))))))
|
||||
|
||||
;; Build dotted form: (a b . c) → ((DOTTED a b) . c) style
|
||||
;; In CL (a b c . d) means a proper dotted structure.
|
||||
;; We represent it as {:cl-type "cons" :car a :cdr (list->dotted b c d)}
|
||||
(define
|
||||
cl-build-dotted
|
||||
(fn
|
||||
(head-items tail)
|
||||
(if
|
||||
(= (len head-items) 0)
|
||||
tail
|
||||
(if
|
||||
(= (len head-items) 1)
|
||||
{:cl-type "cons" :car (nth head-items 0) :cdr tail}
|
||||
(let
|
||||
((last-item (nth head-items (- (len head-items) 1)))
|
||||
(but-last (slice head-items 0 (- (len head-items) 1))))
|
||||
{:cl-type "cons"
|
||||
:car (cl-build-dotted but-last (list last-item))
|
||||
:cdr tail})))))
|
||||
|
||||
;; Read vector #(…) elements until ')'
|
||||
(define
|
||||
cl-read-vector
|
||||
(fn
|
||||
(toks)
|
||||
(let
|
||||
((result (cl-read-vector-items toks (list))))
|
||||
{:form {:cl-type "vector" :elements (get result "items")} :rest (get result "rest")})))
|
||||
|
||||
(define
|
||||
cl-read-vector-items
|
||||
(fn
|
||||
(toks acc)
|
||||
(if
|
||||
(not toks)
|
||||
{:items acc :rest toks}
|
||||
(let
|
||||
((tok (nth toks 0)))
|
||||
(let
|
||||
((type (get tok "type")))
|
||||
(cond
|
||||
((= type "eof") {:items acc :rest toks})
|
||||
((= type "rparen") {:items acc :rest (rest toks)})
|
||||
(:else
|
||||
(let
|
||||
((item (cl-read-form toks)))
|
||||
(cl-read-vector-items
|
||||
(get item "rest")
|
||||
(concat acc (list (get item "form"))))))))))))
|
||||
|
||||
;; ── lambda-list parser ───────────────────────────────────────────
|
||||
;;
|
||||
;; (cl-parse-lambda-list forms) — parse a list of CL forms (already read)
|
||||
;; into a structured dict:
|
||||
;; {:required (list sym ...)
|
||||
;; :optional (list {:name N :default D :supplied S} ...)
|
||||
;; :rest nil | "SYM"
|
||||
;; :key (list {:name N :keyword K :default D :supplied S} ...)
|
||||
;; :allow-other-keys false | true
|
||||
;; :aux (list {:name N :init I} ...)}
|
||||
;;
|
||||
;; Symbols arrive as SX strings (upcase). &-markers are strings like "&OPTIONAL".
|
||||
;; Key params: keyword is the upcase name string; caller uses it as :keyword.
|
||||
;; Supplied-p: nil when absent.
|
||||
|
||||
(define
|
||||
cl-parse-opt-spec
|
||||
(fn
|
||||
(spec)
|
||||
(if
|
||||
(list? spec)
|
||||
{:name (nth spec 0)
|
||||
:default (if (> (len spec) 1) (nth spec 1) nil)
|
||||
:supplied (if (> (len spec) 2) (nth spec 2) nil)}
|
||||
{:name spec :default nil :supplied nil})))
|
||||
|
||||
(define
|
||||
cl-parse-key-spec
|
||||
(fn
|
||||
(spec)
|
||||
(if
|
||||
(list? spec)
|
||||
(let
|
||||
((first (nth spec 0)))
|
||||
(if
|
||||
(list? first)
|
||||
;; ((:keyword var) default supplied-p)
|
||||
{:name (nth first 1)
|
||||
:keyword (get first "name")
|
||||
:default (if (> (len spec) 1) (nth spec 1) nil)
|
||||
:supplied (if (> (len spec) 2) (nth spec 2) nil)}
|
||||
;; (var default supplied-p)
|
||||
{:name first
|
||||
:keyword first
|
||||
:default (if (> (len spec) 1) (nth spec 1) nil)
|
||||
:supplied (if (> (len spec) 2) (nth spec 2) nil)}))
|
||||
{:name spec :keyword spec :default nil :supplied nil})))
|
||||
|
||||
(define
|
||||
cl-parse-aux-spec
|
||||
(fn
|
||||
(spec)
|
||||
(if
|
||||
(list? spec)
|
||||
{:name (nth spec 0) :init (if (> (len spec) 1) (nth spec 1) nil)}
|
||||
{:name spec :init nil})))
|
||||
|
||||
(define
|
||||
cl-parse-lambda-list
|
||||
(fn
|
||||
(forms)
|
||||
(let
|
||||
((state "required")
|
||||
(required (list))
|
||||
(optional (list))
|
||||
(rest-name nil)
|
||||
(key (list))
|
||||
(allow-other-keys false)
|
||||
(aux (list)))
|
||||
|
||||
(define
|
||||
scan
|
||||
(fn
|
||||
(items)
|
||||
(when
|
||||
(> (len items) 0)
|
||||
(let
|
||||
((item (nth items 0)) (tail (rest items)))
|
||||
(cond
|
||||
((= item "&OPTIONAL")
|
||||
(do (set! state "optional") (scan tail)))
|
||||
((= item "&REST")
|
||||
(do (set! state "rest") (scan tail)))
|
||||
((= item "&BODY")
|
||||
(do (set! state "rest") (scan tail)))
|
||||
((= item "&KEY")
|
||||
(do (set! state "key") (scan tail)))
|
||||
((= item "&AUX")
|
||||
(do (set! state "aux") (scan tail)))
|
||||
((= item "&ALLOW-OTHER-KEYS")
|
||||
(do (set! allow-other-keys true) (scan tail)))
|
||||
((= state "required")
|
||||
(do (append! required item) (scan tail)))
|
||||
((= state "optional")
|
||||
(do (append! optional (cl-parse-opt-spec item)) (scan tail)))
|
||||
((= state "rest")
|
||||
(do (set! rest-name item) (set! state "done") (scan tail)))
|
||||
((= state "key")
|
||||
(do (append! key (cl-parse-key-spec item)) (scan tail)))
|
||||
((= state "aux")
|
||||
(do (append! aux (cl-parse-aux-spec item)) (scan tail)))
|
||||
(:else (scan tail)))))))
|
||||
|
||||
(scan forms)
|
||||
{:required required
|
||||
:optional optional
|
||||
:rest rest-name
|
||||
:key key
|
||||
:allow-other-keys allow-other-keys
|
||||
:aux aux})))
|
||||
|
||||
;; Convenience: parse lambda list from a CL source string
|
||||
(define
|
||||
cl-parse-lambda-list-str
|
||||
(fn
|
||||
(src)
|
||||
(cl-parse-lambda-list (cl-read src))))
|
||||
|
||||
;; ── public API ────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-read
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((toks (cl-tokenize src)))
|
||||
(get (cl-read-form toks) "form"))))
|
||||
|
||||
(define
|
||||
cl-read-all
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((toks (cl-tokenize src)))
|
||||
(define
|
||||
loop
|
||||
(fn
|
||||
(toks acc)
|
||||
(if
|
||||
(or (not toks) (= (get (nth toks 0) "type") "eof"))
|
||||
acc
|
||||
(let
|
||||
((result (cl-read-form toks)))
|
||||
(if
|
||||
(get result "eof")
|
||||
acc
|
||||
(loop (get result "rest") (concat acc (list (get result "form")))))))))
|
||||
(loop toks (list)))))
|
||||
381
lib/common-lisp/reader.sx
Normal file
381
lib/common-lisp/reader.sx
Normal file
@@ -0,0 +1,381 @@
|
||||
;; Common Lisp tokenizer
|
||||
;;
|
||||
;; Tokens: {:type T :value V :pos P}
|
||||
;;
|
||||
;; Types:
|
||||
;; "symbol" — FOO, PKG:SYM, PKG::SYM, T, NIL (upcase)
|
||||
;; "keyword" — :foo (value is upcase name without colon)
|
||||
;; "integer" — 42, -5, #xFF, #b1010, #o17 (string)
|
||||
;; "float" — 3.14, 1.0e10 (string)
|
||||
;; "ratio" — 1/3 (string "N/D")
|
||||
;; "string" — unescaped content
|
||||
;; "char" — single-character string
|
||||
;; "lparen" "rparen" "quote" "backquote" "comma" "comma-at"
|
||||
;; "hash-quote" — #'
|
||||
;; "hash-paren" — #(
|
||||
;; "uninterned" — #:foo (upcase name)
|
||||
;; "dot" — standalone . (dotted pair separator)
|
||||
;; "eof"
|
||||
|
||||
(define cl-make-tok (fn (type value pos) {:type type :value value :pos pos}))
|
||||
|
||||
;; ── char ordinal table ────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-ord-table
|
||||
(let
|
||||
((t (dict)) (i 0))
|
||||
(define
|
||||
cl-fill
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< i 128)
|
||||
(do
|
||||
(dict-set! t (char-from-code i) i)
|
||||
(set! i (+ i 1))
|
||||
(cl-fill)))))
|
||||
(cl-fill)
|
||||
t))
|
||||
|
||||
(define cl-ord (fn (c) (or (get cl-ord-table c) 0)))
|
||||
|
||||
;; ── character predicates ──────────────────────────────────────────
|
||||
|
||||
(define cl-digit? (fn (c) (and (>= (cl-ord c) 48) (<= (cl-ord c) 57))))
|
||||
|
||||
(define
|
||||
cl-hex?
|
||||
(fn
|
||||
(c)
|
||||
(or
|
||||
(cl-digit? c)
|
||||
(and (>= (cl-ord c) 65) (<= (cl-ord c) 70))
|
||||
(and (>= (cl-ord c) 97) (<= (cl-ord c) 102)))))
|
||||
|
||||
(define cl-octal? (fn (c) (and (>= (cl-ord c) 48) (<= (cl-ord c) 55))))
|
||||
|
||||
(define cl-binary? (fn (c) (or (= c "0") (= c "1"))))
|
||||
|
||||
(define cl-ws? (fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
|
||||
|
||||
(define
|
||||
cl-alpha?
|
||||
(fn
|
||||
(c)
|
||||
(or
|
||||
(and (>= (cl-ord c) 65) (<= (cl-ord c) 90))
|
||||
(and (>= (cl-ord c) 97) (<= (cl-ord c) 122)))))
|
||||
|
||||
;; Characters that end a token (whitespace + terminating macro chars)
|
||||
(define
|
||||
cl-terminating?
|
||||
(fn
|
||||
(c)
|
||||
(or
|
||||
(cl-ws? c)
|
||||
(= c "(")
|
||||
(= c ")")
|
||||
(= c "\"")
|
||||
(= c ";")
|
||||
(= c "`")
|
||||
(= c ","))))
|
||||
|
||||
;; Symbol constituent: not terminating, not reader-special
|
||||
(define
|
||||
cl-sym-char?
|
||||
(fn
|
||||
(c)
|
||||
(not
|
||||
(or
|
||||
(cl-terminating? c)
|
||||
(= c "#")
|
||||
(= c "|")
|
||||
(= c "\\")
|
||||
(= c "'")))))
|
||||
|
||||
;; ── named character table ─────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-named-chars
|
||||
{:space " "
|
||||
:newline "\n"
|
||||
:tab "\t"
|
||||
:return "\r"
|
||||
:backspace (char-from-code 8)
|
||||
:rubout (char-from-code 127)
|
||||
:delete (char-from-code 127)
|
||||
:escape (char-from-code 27)
|
||||
:altmode (char-from-code 27)
|
||||
:null (char-from-code 0)
|
||||
:nul (char-from-code 0)
|
||||
:page (char-from-code 12)
|
||||
:formfeed (char-from-code 12)})
|
||||
|
||||
;; ── main tokenizer ────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-tokenize
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((pos 0) (n (string-length src)) (toks (list)))
|
||||
|
||||
(define at (fn () (if (< pos n) (substring src pos (+ pos 1)) nil)))
|
||||
(define peek1 (fn () (if (< (+ pos 1) n) (substring src (+ pos 1) (+ pos 2)) nil)))
|
||||
(define adv (fn () (set! pos (+ pos 1))))
|
||||
|
||||
;; Advance while predicate holds; return substring from start to end
|
||||
(define
|
||||
read-while
|
||||
(fn
|
||||
(pred)
|
||||
(let
|
||||
((start pos))
|
||||
(define
|
||||
rw-loop
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(and (at) (pred (at)))
|
||||
(do (adv) (rw-loop)))))
|
||||
(rw-loop)
|
||||
(substring src start pos))))
|
||||
|
||||
(define
|
||||
skip-line
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(and (at) (not (= (at) "\n")))
|
||||
(do (adv) (skip-line)))))
|
||||
|
||||
(define
|
||||
skip-block
|
||||
(fn
|
||||
(depth)
|
||||
(when
|
||||
(at)
|
||||
(cond
|
||||
((and (= (at) "#") (= (peek1) "|"))
|
||||
(do (adv) (adv) (skip-block (+ depth 1))))
|
||||
((and (= (at) "|") (= (peek1) "#"))
|
||||
(do
|
||||
(adv)
|
||||
(adv)
|
||||
(when (> depth 1) (skip-block (- depth 1)))))
|
||||
(:else (do (adv) (skip-block depth)))))))
|
||||
|
||||
;; Read string literal — called with pos just past opening "
|
||||
(define
|
||||
read-str
|
||||
(fn
|
||||
(acc)
|
||||
(if
|
||||
(not (at))
|
||||
acc
|
||||
(cond
|
||||
((= (at) "\"") (do (adv) acc))
|
||||
((= (at) "\\")
|
||||
(do
|
||||
(adv)
|
||||
(let
|
||||
((e (at)))
|
||||
(adv)
|
||||
(read-str
|
||||
(str
|
||||
acc
|
||||
(cond
|
||||
((= e "n") "\n")
|
||||
((= e "t") "\t")
|
||||
((= e "r") "\r")
|
||||
((= e "\"") "\"")
|
||||
((= e "\\") "\\")
|
||||
(:else e)))))))
|
||||
(:else
|
||||
(let
|
||||
((c (at)))
|
||||
(adv)
|
||||
(read-str (str acc c))))))))
|
||||
|
||||
;; Read #\ char literal — called with pos just past the backslash
|
||||
(define
|
||||
read-char-lit
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((first (at)))
|
||||
(adv)
|
||||
(let
|
||||
((rest (if (and (at) (cl-alpha? (at))) (read-while cl-alpha?) "")))
|
||||
(if
|
||||
(= rest "")
|
||||
first
|
||||
(let
|
||||
((name (downcase (str first rest))))
|
||||
(or (get cl-named-chars name) first)))))))
|
||||
|
||||
;; Number scanner — called with pos just past first digit(s).
|
||||
;; acc holds what was already consumed (first digit or sign+digit).
|
||||
(define
|
||||
scan-num
|
||||
(fn
|
||||
(p acc)
|
||||
(let
|
||||
((more (read-while cl-digit?)))
|
||||
(set! acc (str acc more))
|
||||
(cond
|
||||
;; ratio N/D
|
||||
((and (at) (= (at) "/") (peek1) (cl-digit? (peek1)))
|
||||
(do
|
||||
(adv)
|
||||
(let
|
||||
((denom (read-while cl-digit?)))
|
||||
{:type "ratio" :value (str acc "/" denom) :pos p})))
|
||||
;; float: decimal point N.M[eE]
|
||||
((and (at) (= (at) ".") (peek1) (cl-digit? (peek1)))
|
||||
(do
|
||||
(adv)
|
||||
(let
|
||||
((frac (read-while cl-digit?)))
|
||||
(set! acc (str acc "." frac))
|
||||
(when
|
||||
(and (at) (or (= (at) "e") (= (at) "E")))
|
||||
(do
|
||||
(set! acc (str acc (at)))
|
||||
(adv)
|
||||
(when
|
||||
(and (at) (or (= (at) "+") (= (at) "-")))
|
||||
(do (set! acc (str acc (at))) (adv)))
|
||||
(set! acc (str acc (read-while cl-digit?)))))
|
||||
{:type "float" :value acc :pos p})))
|
||||
;; float: exponent only NeE
|
||||
((and (at) (or (= (at) "e") (= (at) "E")))
|
||||
(do
|
||||
(set! acc (str acc (at)))
|
||||
(adv)
|
||||
(when
|
||||
(and (at) (or (= (at) "+") (= (at) "-")))
|
||||
(do (set! acc (str acc (at))) (adv)))
|
||||
(set! acc (str acc (read-while cl-digit?)))
|
||||
{:type "float" :value acc :pos p}))
|
||||
(:else {:type "integer" :value acc :pos p})))))
|
||||
|
||||
(define
|
||||
read-radix
|
||||
(fn
|
||||
(letter p)
|
||||
(let
|
||||
((pred
|
||||
(cond
|
||||
((or (= letter "x") (= letter "X")) cl-hex?)
|
||||
((or (= letter "b") (= letter "B")) cl-binary?)
|
||||
((or (= letter "o") (= letter "O")) cl-octal?)
|
||||
(:else cl-digit?))))
|
||||
{:type "integer"
|
||||
:value (str "#" letter (read-while pred))
|
||||
:pos p})))
|
||||
|
||||
(define emit (fn (tok) (append! toks tok)))
|
||||
|
||||
(define
|
||||
scan
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(< pos n)
|
||||
(let
|
||||
((c (at)) (p pos))
|
||||
(cond
|
||||
((cl-ws? c) (do (adv) (scan)))
|
||||
((= c ";") (do (adv) (skip-line) (scan)))
|
||||
((= c "(") (do (adv) (emit (cl-make-tok "lparen" "(" p)) (scan)))
|
||||
((= c ")") (do (adv) (emit (cl-make-tok "rparen" ")" p)) (scan)))
|
||||
((= c "'") (do (adv) (emit (cl-make-tok "quote" "'" p)) (scan)))
|
||||
((= c "`") (do (adv) (emit (cl-make-tok "backquote" "`" p)) (scan)))
|
||||
((= c ",")
|
||||
(do
|
||||
(adv)
|
||||
(if
|
||||
(= (at) "@")
|
||||
(do (adv) (emit (cl-make-tok "comma-at" ",@" p)))
|
||||
(emit (cl-make-tok "comma" "," p)))
|
||||
(scan)))
|
||||
((= c "\"")
|
||||
(do
|
||||
(adv)
|
||||
(emit (cl-make-tok "string" (read-str "") p))
|
||||
(scan)))
|
||||
;; :keyword
|
||||
((= c ":")
|
||||
(do
|
||||
(adv)
|
||||
(emit (cl-make-tok "keyword" (upcase (read-while cl-sym-char?)) p))
|
||||
(scan)))
|
||||
;; dispatch macro #
|
||||
((= c "#")
|
||||
(do
|
||||
(adv)
|
||||
(let
|
||||
((d (at)))
|
||||
(cond
|
||||
((= d "'") (do (adv) (emit (cl-make-tok "hash-quote" "#'" p)) (scan)))
|
||||
((= d "(") (do (adv) (emit (cl-make-tok "hash-paren" "#(" p)) (scan)))
|
||||
((= d ":")
|
||||
(do
|
||||
(adv)
|
||||
(emit
|
||||
(cl-make-tok "uninterned" (upcase (read-while cl-sym-char?)) p))
|
||||
(scan)))
|
||||
((= d "|") (do (adv) (skip-block 1) (scan)))
|
||||
((= d "\\")
|
||||
(do (adv) (emit (cl-make-tok "char" (read-char-lit) p)) (scan)))
|
||||
((or (= d "x") (= d "X"))
|
||||
(do (adv) (emit (read-radix d p)) (scan)))
|
||||
((or (= d "b") (= d "B"))
|
||||
(do (adv) (emit (read-radix d p)) (scan)))
|
||||
((or (= d "o") (= d "O"))
|
||||
(do (adv) (emit (read-radix d p)) (scan)))
|
||||
(:else (scan))))))
|
||||
;; standalone dot, float .5, or symbol starting with dots
|
||||
((= c ".")
|
||||
(do
|
||||
(adv)
|
||||
(cond
|
||||
((or (not (at)) (cl-terminating? (at)))
|
||||
(do (emit (cl-make-tok "dot" "." p)) (scan)))
|
||||
((cl-digit? (at))
|
||||
(do
|
||||
(emit
|
||||
(cl-make-tok "float" (str "0." (read-while cl-digit?)) p))
|
||||
(scan)))
|
||||
(:else
|
||||
(do
|
||||
(emit
|
||||
(cl-make-tok "symbol" (upcase (str "." (read-while cl-sym-char?))) p))
|
||||
(scan))))))
|
||||
;; sign followed by digit → number
|
||||
((and (or (= c "+") (= c "-")) (peek1) (cl-digit? (peek1)))
|
||||
(do
|
||||
(adv)
|
||||
(let
|
||||
((first-d (at)))
|
||||
(adv)
|
||||
(emit (scan-num p (str c first-d))))
|
||||
(scan)))
|
||||
;; decimal digit → number
|
||||
((cl-digit? c)
|
||||
(do
|
||||
(adv)
|
||||
(emit (scan-num p c))
|
||||
(scan)))
|
||||
;; symbol constituent (includes bare +, -, etc.)
|
||||
((cl-sym-char? c)
|
||||
(do
|
||||
(emit (cl-make-tok "symbol" (upcase (read-while cl-sym-char?)) p))
|
||||
(scan)))
|
||||
(:else (do (adv) (scan))))))))
|
||||
|
||||
(scan)
|
||||
(append! toks (cl-make-tok "eof" nil n))
|
||||
toks)))
|
||||
@@ -1,18 +1,14 @@
|
||||
;; lib/common-lisp/runtime.sx — CL built-ins using SX spec primitives
|
||||
;; lib/common-lisp/runtime.sx — CL built-ins + condition system on SX
|
||||
;;
|
||||
;; Provides CL-specific wrappers and helpers. Deliberately thin: wherever
|
||||
;; an SX spec primitive already does the job, we alias it rather than
|
||||
;; reinventing it.
|
||||
;; Section 1-9: Type predicates, arithmetic, characters, strings, gensym,
|
||||
;; multiple values, sets, radix formatting, list utilities.
|
||||
;; Section 10: Condition system (define-condition, signal/error/warn,
|
||||
;; handler-bind, handler-case, restart-case, invoke-restart).
|
||||
;;
|
||||
;; Primitives used from spec:
|
||||
;; char/char->integer/integer->char/char-upcase/char-downcase
|
||||
;; format (Phase 21 — must be loaded before this file)
|
||||
;; gensym (Phase 12)
|
||||
;; rational/rational? (Phase 16)
|
||||
;; make-set/set-member?/set-union/etc (Phase 18)
|
||||
;; open-input-string/read-char/etc (Phase 14)
|
||||
;; modulo/remainder/quotient/gcd/lcm/expt (Phase 2 / Phase 15)
|
||||
;; number->string with radix (Phase 15)
|
||||
;; format gensym rational/rational? make-set/set-member?/etc
|
||||
;; modulo/remainder/quotient/gcd/lcm/expt number->string
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 1. Type predicates
|
||||
@@ -304,3 +300,425 @@
|
||||
((or (cl-empty? plist) (cl-empty? (rest plist))) nil)
|
||||
((equal? (first plist) key) (first (rest plist)))
|
||||
(else (cl-getf (rest (rest plist)) key))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; 10. Condition system (Phase 3)
|
||||
;;
|
||||
;; Condition objects:
|
||||
;; {:cl-type "cl-condition" :class "NAME" :slots {slot-name val ...}}
|
||||
;;
|
||||
;; The built-in handler-bind / restart-case expect LITERAL handler specs in
|
||||
;; source (they operate on the raw AST), so we implement our own handler and
|
||||
;; restart stacks as mutable SX globals.
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; ── condition class registry ───────────────────────────────────────────────
|
||||
;;
|
||||
;; Populated at load time with all ANSI standard condition types.
|
||||
;; Also mutated by cl-define-condition.
|
||||
|
||||
(define
|
||||
cl-condition-classes
|
||||
(dict
|
||||
"condition"
|
||||
{:parents (list) :slots (list) :name "condition"}
|
||||
"serious-condition"
|
||||
{:parents (list "condition") :slots (list) :name "serious-condition"}
|
||||
"error"
|
||||
{:parents (list "serious-condition") :slots (list) :name "error"}
|
||||
"warning"
|
||||
{:parents (list "condition") :slots (list) :name "warning"}
|
||||
"simple-condition"
|
||||
{:parents (list "condition") :slots (list "format-control" "format-arguments") :name "simple-condition"}
|
||||
"simple-error"
|
||||
{:parents (list "error" "simple-condition") :slots (list "format-control" "format-arguments") :name "simple-error"}
|
||||
"simple-warning"
|
||||
{:parents (list "warning" "simple-condition") :slots (list "format-control" "format-arguments") :name "simple-warning"}
|
||||
"type-error"
|
||||
{:parents (list "error") :slots (list "datum" "expected-type") :name "type-error"}
|
||||
"arithmetic-error"
|
||||
{:parents (list "error") :slots (list "operation" "operands") :name "arithmetic-error"}
|
||||
"division-by-zero"
|
||||
{:parents (list "arithmetic-error") :slots (list) :name "division-by-zero"}
|
||||
"cell-error"
|
||||
{:parents (list "error") :slots (list "name") :name "cell-error"}
|
||||
"unbound-variable"
|
||||
{:parents (list "cell-error") :slots (list) :name "unbound-variable"}
|
||||
"undefined-function"
|
||||
{:parents (list "cell-error") :slots (list) :name "undefined-function"}
|
||||
"program-error"
|
||||
{:parents (list "error") :slots (list) :name "program-error"}
|
||||
"storage-condition"
|
||||
{:parents (list "serious-condition") :slots (list) :name "storage-condition"}))
|
||||
|
||||
;; ── condition predicates ───────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-condition?
|
||||
(fn (x) (and (dict? x) (= (get x "cl-type") "cl-condition"))))
|
||||
|
||||
;; cl-condition-of-type? walks the class hierarchy.
|
||||
;; We capture cl-condition-classes at define time via let to avoid
|
||||
;; free-variable scoping issues at call time.
|
||||
|
||||
(define
|
||||
cl-condition-of-type?
|
||||
(let
|
||||
((classes cl-condition-classes))
|
||||
(fn
|
||||
(c type-name)
|
||||
(if
|
||||
(not (cl-condition? c))
|
||||
false
|
||||
(let
|
||||
((class-name (get c "class")))
|
||||
(define
|
||||
check
|
||||
(fn
|
||||
(n)
|
||||
(if
|
||||
(= n type-name)
|
||||
true
|
||||
(let
|
||||
((entry (get classes n)))
|
||||
(if
|
||||
(nil? entry)
|
||||
false
|
||||
(some (fn (p) (check p)) (get entry "parents")))))))
|
||||
(check class-name))))))
|
||||
|
||||
;; ── condition constructors ─────────────────────────────────────────────────
|
||||
|
||||
;; cl-define-condition registers a new condition class.
|
||||
;; name: string (condition class name)
|
||||
;; parents: list of strings (parent class names)
|
||||
;; slot-names: list of strings
|
||||
|
||||
(define
|
||||
cl-define-condition
|
||||
(fn
|
||||
(name parents slot-names)
|
||||
(begin (dict-set! cl-condition-classes name {:parents parents :slots slot-names :name name}) name)))
|
||||
|
||||
;; cl-make-condition constructs a condition object.
|
||||
;; Keyword args (alternating slot-name/value pairs) populate the slots dict.
|
||||
|
||||
(define
|
||||
cl-make-condition
|
||||
(fn
|
||||
(name &rest kw-args)
|
||||
(let
|
||||
((slots (dict)))
|
||||
(define
|
||||
fill
|
||||
(fn
|
||||
(args)
|
||||
(when
|
||||
(>= (len args) 2)
|
||||
(begin
|
||||
(dict-set! slots (first args) (first (rest args)))
|
||||
(fill (rest (rest args)))))))
|
||||
(fill kw-args)
|
||||
{:cl-type "cl-condition" :slots slots :class name})))
|
||||
|
||||
;; ── condition accessors ────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-condition-slot
|
||||
(fn
|
||||
(c slot-name)
|
||||
(if (cl-condition? c) (get (get c "slots") slot-name) nil)))
|
||||
|
||||
(define
|
||||
cl-condition-message
|
||||
(fn
|
||||
(c)
|
||||
(if
|
||||
(not (cl-condition? c))
|
||||
(str c)
|
||||
(let
|
||||
((slots (get c "slots")))
|
||||
(or
|
||||
(get slots "message")
|
||||
(get slots "format-control")
|
||||
(str "Condition: " (get c "class")))))))
|
||||
|
||||
(define
|
||||
cl-simple-condition-format-control
|
||||
(fn (c) (cl-condition-slot c "format-control")))
|
||||
|
||||
(define
|
||||
cl-simple-condition-format-arguments
|
||||
(fn (c) (cl-condition-slot c "format-arguments")))
|
||||
|
||||
(define cl-type-error-datum (fn (c) (cl-condition-slot c "datum")))
|
||||
|
||||
(define
|
||||
cl-type-error-expected-type
|
||||
(fn (c) (cl-condition-slot c "expected-type")))
|
||||
|
||||
(define
|
||||
cl-arithmetic-error-operation
|
||||
(fn (c) (cl-condition-slot c "operation")))
|
||||
|
||||
(define
|
||||
cl-arithmetic-error-operands
|
||||
(fn (c) (cl-condition-slot c "operands")))
|
||||
|
||||
;; ── mutable handler + restart stacks ──────────────────────────────────────
|
||||
;;
|
||||
;; Handler entry: {:type "type-name" :fn (fn (condition) result)}
|
||||
;; Restart entry: {:name "restart-name" :fn (fn (&optional arg) result) :escape k}
|
||||
;;
|
||||
;; New handlers are prepended (checked first = most recent handler wins).
|
||||
|
||||
(define cl-handler-stack (list))
|
||||
(define cl-restart-stack (list))
|
||||
|
||||
(define
|
||||
cl-push-handlers
|
||||
(fn (entries) (set! cl-handler-stack (append entries cl-handler-stack))))
|
||||
|
||||
(define
|
||||
cl-pop-handlers
|
||||
(fn
|
||||
(n)
|
||||
(set! cl-handler-stack (slice cl-handler-stack n (len cl-handler-stack)))))
|
||||
|
||||
(define
|
||||
cl-push-restarts
|
||||
(fn (entries) (set! cl-restart-stack (append entries cl-restart-stack))))
|
||||
|
||||
(define
|
||||
cl-pop-restarts
|
||||
(fn
|
||||
(n)
|
||||
(set! cl-restart-stack (slice cl-restart-stack n (len cl-restart-stack)))))
|
||||
|
||||
;; ── *debugger-hook* + invoke-debugger ────────────────────────────────────
|
||||
;;
|
||||
;; cl-debugger-hook: called when an error propagates with no handler.
|
||||
;; Signature: (fn (condition hook) result). The hook arg is itself
|
||||
;; (so the hook can rebind it to nil to prevent recursion).
|
||||
;; nil = use default (re-raise as host error).
|
||||
|
||||
(define cl-debugger-hook nil)
|
||||
|
||||
(define cl-invoke-debugger
|
||||
(fn (c)
|
||||
(if (nil? cl-debugger-hook)
|
||||
(error (str "Debugger: " (cl-condition-message c)))
|
||||
(let ((hook cl-debugger-hook))
|
||||
(set! cl-debugger-hook nil)
|
||||
(let ((result (hook c hook)))
|
||||
(set! cl-debugger-hook hook)
|
||||
result)))))
|
||||
|
||||
;; ── *break-on-signals* ────────────────────────────────────────────────────
|
||||
;;
|
||||
;; When set to a type name string, cl-signal invokes the debugger hook
|
||||
;; before walking handlers if the condition is of that type.
|
||||
;; nil = disabled (ANSI default).
|
||||
|
||||
(define cl-break-on-signals nil)
|
||||
|
||||
;; ── invoke-restart-interactively ──────────────────────────────────────────
|
||||
;;
|
||||
;; Like invoke-restart but calls the restart's fn with no arguments
|
||||
;; (real CL would prompt the user for each arg via :interactive).
|
||||
|
||||
(define cl-invoke-restart-interactively
|
||||
(fn (name)
|
||||
(let ((entry (cl-find-restart-entry name cl-restart-stack)))
|
||||
(if (nil? entry)
|
||||
(error (str "No active restart: " name))
|
||||
(let ((restart-fn (get entry "fn"))
|
||||
(escape (get entry "escape")))
|
||||
(escape (restart-fn)))))))
|
||||
|
||||
;; ── cl-signal (non-unwinding) ─────────────────────────────────────────────
|
||||
;;
|
||||
;; Walks cl-handler-stack; for each matching entry, calls the handler fn.
|
||||
;; Handlers return normally — signal continues to the next matching handler.
|
||||
|
||||
(define
|
||||
cl-signal-obj
|
||||
(fn
|
||||
(obj stack)
|
||||
(if
|
||||
(empty? stack)
|
||||
nil
|
||||
(let
|
||||
((entry (first stack)))
|
||||
(if
|
||||
(cl-condition-of-type? obj (get entry "type"))
|
||||
(begin ((get entry "fn") obj) (cl-signal-obj obj (rest stack)))
|
||||
(cl-signal-obj obj (rest stack)))))))
|
||||
|
||||
(define cl-signal
|
||||
(fn (c)
|
||||
(let ((obj (if (cl-condition? c)
|
||||
c
|
||||
(cl-make-condition "simple-condition"
|
||||
"format-control" (str c)))))
|
||||
;; *break-on-signals*: invoke debugger hook when type matches
|
||||
(when (and (not (nil? cl-break-on-signals))
|
||||
(cl-condition-of-type? obj cl-break-on-signals))
|
||||
(cl-invoke-debugger obj))
|
||||
(cl-signal-obj obj cl-handler-stack))))
|
||||
|
||||
;; ── cl-error ───────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Signals an error. If no handler catches it, raises a host-level error.
|
||||
|
||||
(define
|
||||
cl-error
|
||||
(fn
|
||||
(c &rest args)
|
||||
(let
|
||||
((obj (cond ((cl-condition? c) c) ((string? c) (cl-make-condition "simple-error" "format-control" c "format-arguments" args)) (:else (cl-make-condition "simple-error" "format-control" (str c))))))
|
||||
(cl-signal-obj obj cl-handler-stack)
|
||||
(cl-invoke-debugger obj))))
|
||||
|
||||
;; ── cl-warn ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-warn
|
||||
(fn
|
||||
(c &rest args)
|
||||
(let
|
||||
((obj (cond ((cl-condition? c) c) ((string? c) (cl-make-condition "simple-warning" "format-control" c "format-arguments" args)) (:else (cl-make-condition "simple-warning" "format-control" (str c))))))
|
||||
(cl-signal-obj obj cl-handler-stack))))
|
||||
|
||||
;; ── cl-handler-bind (non-unwinding) ───────────────────────────────────────
|
||||
;;
|
||||
;; bindings: list of (type-name handler-fn) pairs
|
||||
;; thunk: (fn () body)
|
||||
|
||||
(define
|
||||
cl-handler-bind
|
||||
(fn
|
||||
(bindings thunk)
|
||||
(let
|
||||
((entries (map (fn (b) {:fn (first (rest b)) :type (first b)}) bindings)))
|
||||
(begin
|
||||
(cl-push-handlers entries)
|
||||
(let
|
||||
((result (thunk)))
|
||||
(begin (cl-pop-handlers (len entries)) result))))))
|
||||
|
||||
;; ── cl-handler-case (unwinding) ───────────────────────────────────────────
|
||||
;;
|
||||
;; thunk: (fn () body)
|
||||
;; cases: list of (type-name handler-fn) pairs
|
||||
;;
|
||||
;; Uses call/cc for the escape continuation.
|
||||
|
||||
(define
|
||||
cl-handler-case
|
||||
(fn
|
||||
(thunk &rest cases)
|
||||
(call/cc
|
||||
(fn
|
||||
(escape)
|
||||
(let
|
||||
((entries (map (fn (c) {:fn (fn (x) (escape ((first (rest c)) x))) :type (first c)}) cases)))
|
||||
(begin
|
||||
(cl-push-handlers entries)
|
||||
(let
|
||||
((result (thunk)))
|
||||
(begin (cl-pop-handlers (len entries)) result))))))))
|
||||
|
||||
;; ── cl-restart-case ────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; thunk: (fn () body)
|
||||
;; restarts: list of (name params body-fn) triples
|
||||
;; body-fn is (fn () val) or (fn (arg) val)
|
||||
|
||||
(define
|
||||
cl-restart-case
|
||||
(fn
|
||||
(thunk &rest restarts)
|
||||
(call/cc
|
||||
(fn
|
||||
(escape)
|
||||
(let
|
||||
((entries (map (fn (r) {:fn (first (rest (rest r))) :escape escape :name (first r)}) restarts)))
|
||||
(begin
|
||||
(cl-push-restarts entries)
|
||||
(let
|
||||
((result (thunk)))
|
||||
(begin (cl-pop-restarts (len entries)) result))))))))
|
||||
|
||||
;; ── cl-with-simple-restart ─────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
cl-with-simple-restart
|
||||
(fn
|
||||
(name description thunk)
|
||||
(cl-restart-case thunk (list name (list) (fn () nil)))))
|
||||
|
||||
;; ── find-restart / invoke-restart / compute-restarts ──────────────────────
|
||||
|
||||
(define
|
||||
cl-find-restart-entry
|
||||
(fn
|
||||
(name stack)
|
||||
(if
|
||||
(empty? stack)
|
||||
nil
|
||||
(let
|
||||
((entry (first stack)))
|
||||
(if
|
||||
(= (get entry "name") name)
|
||||
entry
|
||||
(cl-find-restart-entry name (rest stack)))))))
|
||||
|
||||
(define
|
||||
cl-find-restart
|
||||
(fn (name) (cl-find-restart-entry name cl-restart-stack)))
|
||||
|
||||
(define
|
||||
cl-invoke-restart
|
||||
(fn
|
||||
(name &rest args)
|
||||
(let
|
||||
((entry (cl-find-restart-entry name cl-restart-stack)))
|
||||
(if
|
||||
(nil? entry)
|
||||
(error (str "No active restart: " name))
|
||||
(let
|
||||
((restart-fn (get entry "fn")) (escape (get entry "escape")))
|
||||
(escape
|
||||
(if (empty? args) (restart-fn) (restart-fn (first args)))))))))
|
||||
|
||||
(define
|
||||
cl-compute-restarts
|
||||
(fn () (map (fn (e) (get e "name")) cl-restart-stack)))
|
||||
|
||||
;; ── with-condition-restarts (stub — association is advisory) ──────────────
|
||||
|
||||
(define cl-with-condition-restarts (fn (c restarts thunk) (thunk)))
|
||||
|
||||
;; ── cl-cerror ──────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Signals a continuable error. The "continue" restart is established;
|
||||
;; invoke-restart "continue" to proceed past the error.
|
||||
|
||||
|
||||
|
||||
;; ── cl-cerror ──────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Signals a continuable error. The "continue" restart is established;
|
||||
;; invoke-restart "continue" to proceed past the error.
|
||||
|
||||
(define cl-cerror
|
||||
(fn (continue-string c &rest args)
|
||||
(let ((obj (if (cl-condition? c)
|
||||
c
|
||||
(cl-make-condition "simple-error"
|
||||
"format-control" (str c)
|
||||
"format-arguments" args))))
|
||||
(cl-restart-case
|
||||
(fn () (cl-signal-obj obj cl-handler-stack))
|
||||
(list "continue" (list) (fn () nil))))))
|
||||
19
lib/common-lisp/scoreboard.json
Normal file
19
lib/common-lisp/scoreboard.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"generated": "2026-05-05T12:35:09Z",
|
||||
"total_pass": 518,
|
||||
"total_fail": 0,
|
||||
"suites": [
|
||||
{"name": "Phase 1: tokenizer/reader", "pass": 79, "fail": 0},
|
||||
{"name": "Phase 1: parser/lambda-lists", "pass": 31, "fail": 0},
|
||||
{"name": "Phase 2: evaluator", "pass": 182, "fail": 0},
|
||||
{"name": "Phase 3: condition system", "pass": 59, "fail": 0},
|
||||
{"name": "Phase 3: restart-demo", "pass": 7, "fail": 0},
|
||||
{"name": "Phase 3: parse-recover", "pass": 6, "fail": 0},
|
||||
{"name": "Phase 3: interactive-debugger", "pass": 7, "fail": 0},
|
||||
{"name": "Phase 4: CLOS", "pass": 41, "fail": 0},
|
||||
{"name": "Phase 4: geometry", "pass": 12, "fail": 0},
|
||||
{"name": "Phase 4: mop-trace", "pass": 13, "fail": 0},
|
||||
{"name": "Phase 5: macros+LOOP", "pass": 27, "fail": 0},
|
||||
{"name": "Phase 6: stdlib", "pass": 54, "fail": 0}
|
||||
]
|
||||
}
|
||||
20
lib/common-lisp/scoreboard.md
Normal file
20
lib/common-lisp/scoreboard.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Common Lisp on SX — Scoreboard
|
||||
|
||||
_Generated: 2026-05-05 12:35 UTC_
|
||||
|
||||
| Suite | Pass | Fail | Status |
|
||||
|-------|------|------|--------|
|
||||
| Phase 1: tokenizer/reader | 79 | 0 | pass |
|
||||
| Phase 1: parser/lambda-lists | 31 | 0 | pass |
|
||||
| Phase 2: evaluator | 182 | 0 | pass |
|
||||
| Phase 3: condition system | 59 | 0 | pass |
|
||||
| Phase 3: restart-demo | 7 | 0 | pass |
|
||||
| Phase 3: parse-recover | 6 | 0 | pass |
|
||||
| Phase 3: interactive-debugger | 7 | 0 | pass |
|
||||
| Phase 4: CLOS | 41 | 0 | pass |
|
||||
| Phase 4: geometry | 12 | 0 | pass |
|
||||
| Phase 4: mop-trace | 13 | 0 | pass |
|
||||
| Phase 5: macros+LOOP | 27 | 0 | pass |
|
||||
| Phase 6: stdlib | 54 | 0 | pass |
|
||||
|
||||
**Total: 518 passed, 0 failed**
|
||||
@@ -292,6 +292,147 @@ check 113 "cl-format-decimal 42" '"42"'
|
||||
check 114 "n->s base 16" '"1f"'
|
||||
check 115 "s->n base 16" "31"
|
||||
|
||||
# ── Phase 2: condition system unit tests ─────────────────────────────────────
|
||||
# Load runtime.sx then conditions.sx; query the passed/failed/failures globals.
|
||||
UNIT_FILE=$(mktemp); trap "rm -f $UNIT_FILE" EXIT
|
||||
cat > "$UNIT_FILE" << 'UNIT'
|
||||
(epoch 1)
|
||||
(load "spec/stdlib.sx")
|
||||
(epoch 2)
|
||||
(load "lib/common-lisp/runtime.sx")
|
||||
(epoch 3)
|
||||
(load "lib/common-lisp/tests/conditions.sx")
|
||||
(epoch 4)
|
||||
(eval "passed")
|
||||
(epoch 5)
|
||||
(eval "failed")
|
||||
(epoch 6)
|
||||
(eval "failures")
|
||||
UNIT
|
||||
|
||||
UNIT_OUT=$(timeout 30 "$SX_SERVER" < "$UNIT_FILE" 2>/dev/null)
|
||||
|
||||
# extract passed/failed counts from ok-len lines
|
||||
UNIT_PASSED=$(echo "$UNIT_OUT" | grep -A1 "^(ok-len 4 " | tail -1 || true)
|
||||
UNIT_FAILED=$(echo "$UNIT_OUT" | grep -A1 "^(ok-len 5 " | tail -1 || true)
|
||||
UNIT_ERRS=$(echo "$UNIT_OUT" | grep -A1 "^(ok-len 6 " | tail -1 || true)
|
||||
# fallback: try plain ok lines
|
||||
[ -z "$UNIT_PASSED" ] && UNIT_PASSED=$(echo "$UNIT_OUT" | grep "^(ok 4 " | awk '{print $3}' | tr -d ')' || true)
|
||||
[ -z "$UNIT_FAILED" ] && UNIT_FAILED=$(echo "$UNIT_OUT" | grep "^(ok 5 " | awk '{print $3}' | tr -d ')' || true)
|
||||
[ -z "$UNIT_PASSED" ] && UNIT_PASSED=0
|
||||
[ -z "$UNIT_FAILED" ] && UNIT_FAILED=0
|
||||
|
||||
if [ "$UNIT_FAILED" = "0" ] && [ "$UNIT_PASSED" -gt 0 ] 2>/dev/null; then
|
||||
PASS=$((PASS + UNIT_PASSED))
|
||||
[ "$VERBOSE" = "-v" ] && echo " ok condition tests ($UNIT_PASSED)"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
ERRORS+=" FAIL [condition tests] (${UNIT_PASSED} passed, ${UNIT_FAILED} failed) ${UNIT_ERRS}
|
||||
"
|
||||
fi
|
||||
|
||||
# ── Phase 3: classic program tests ───────────────────────────────────────────
|
||||
run_program_suite() {
|
||||
local prog="$1" pass_var="$2" fail_var="$3" failures_var="$4"
|
||||
local PROG_FILE=$(mktemp)
|
||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/runtime.sx")\n(epoch 3)\n(load "%s")\n(epoch 4)\n(eval "%s")\n(epoch 5)\n(eval "%s")\n(epoch 6)\n(eval "%s")\n' \
|
||||
"$prog" "$pass_var" "$fail_var" "$failures_var" > "$PROG_FILE"
|
||||
local OUT; OUT=$(timeout 20 "$SX_SERVER" < "$PROG_FILE" 2>/dev/null)
|
||||
rm -f "$PROG_FILE"
|
||||
local P F
|
||||
P=$(echo "$OUT" | grep -A1 "^(ok-len 4 " | tail -1 || true)
|
||||
F=$(echo "$OUT" | grep -A1 "^(ok-len 5 " | tail -1 || true)
|
||||
local ERRS; ERRS=$(echo "$OUT" | grep -A1 "^(ok-len 6 " | tail -1 || true)
|
||||
[ -z "$P" ] && P=0; [ -z "$F" ] && F=0
|
||||
if [ "$F" = "0" ] && [ "$P" -gt 0 ] 2>/dev/null; then
|
||||
PASS=$((PASS + P))
|
||||
[ "$VERBOSE" = "-v" ] && echo " ok $prog ($P)"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
ERRORS+=" FAIL [$prog] (${P} passed, ${F} failed) ${ERRS}
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
run_program_suite \
|
||||
"lib/common-lisp/tests/programs/restart-demo.sx" \
|
||||
"demo-passed" "demo-failed" "demo-failures"
|
||||
|
||||
run_program_suite \
|
||||
"lib/common-lisp/tests/programs/parse-recover.sx" \
|
||||
"parse-passed" "parse-failed" "parse-failures"
|
||||
|
||||
run_program_suite \
|
||||
"lib/common-lisp/tests/programs/interactive-debugger.sx" \
|
||||
"debugger-passed" "debugger-failed" "debugger-failures"
|
||||
|
||||
# ── Phase 4: CLOS unit tests ─────────────────────────────────────────────────
|
||||
CLOS_FILE=$(mktemp); trap "rm -f $CLOS_FILE" EXIT
|
||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/runtime.sx")\n(epoch 3)\n(load "lib/common-lisp/clos.sx")\n(epoch 4)\n(load "lib/common-lisp/tests/clos.sx")\n(epoch 5)\n(eval "passed")\n(epoch 6)\n(eval "failed")\n(epoch 7)\n(eval "failures")\n' > "$CLOS_FILE"
|
||||
CLOS_OUT=$(timeout 30 "$SX_SERVER" < "$CLOS_FILE" 2>/dev/null)
|
||||
rm -f "$CLOS_FILE"
|
||||
CLOS_PASSED=$(echo "$CLOS_OUT" | grep -A1 "^(ok-len 5 " | tail -1 || true)
|
||||
CLOS_FAILED=$(echo "$CLOS_OUT" | grep -A1 "^(ok-len 6 " | tail -1 || true)
|
||||
[ -z "$CLOS_PASSED" ] && CLOS_PASSED=$(echo "$CLOS_OUT" | grep "^(ok 5 " | awk '{print $3}' | tr -d ')' || true)
|
||||
[ -z "$CLOS_FAILED" ] && CLOS_FAILED=$(echo "$CLOS_OUT" | grep "^(ok 6 " | awk '{print $3}' | tr -d ')' || true)
|
||||
[ -z "$CLOS_PASSED" ] && CLOS_PASSED=0; [ -z "$CLOS_FAILED" ] && CLOS_FAILED=0
|
||||
if [ "$CLOS_FAILED" = "0" ] && [ "$CLOS_PASSED" -gt 0 ] 2>/dev/null; then
|
||||
PASS=$((PASS + CLOS_PASSED))
|
||||
[ "$VERBOSE" = "-v" ] && echo " ok CLOS unit tests ($CLOS_PASSED)"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
ERRORS+=" FAIL [CLOS unit tests] (${CLOS_PASSED} passed, ${CLOS_FAILED} failed)
|
||||
"
|
||||
fi
|
||||
|
||||
# ── Phase 4: CLOS classic programs ───────────────────────────────────────────
|
||||
run_clos_suite() {
|
||||
local prog="$1" pass_var="$2" fail_var="$3" failures_var="$4"
|
||||
local PROG_FILE=$(mktemp)
|
||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/runtime.sx")\n(epoch 3)\n(load "lib/common-lisp/clos.sx")\n(epoch 4)\n(load "%s")\n(epoch 5)\n(eval "%s")\n(epoch 6)\n(eval "%s")\n(epoch 7)\n(eval "%s")\n' \
|
||||
"$prog" "$pass_var" "$fail_var" "$failures_var" > "$PROG_FILE"
|
||||
local OUT; OUT=$(timeout 20 "$SX_SERVER" < "$PROG_FILE" 2>/dev/null)
|
||||
rm -f "$PROG_FILE"
|
||||
local P F
|
||||
P=$(echo "$OUT" | grep -A1 "^(ok-len 5 " | tail -1 || true)
|
||||
F=$(echo "$OUT" | grep -A1 "^(ok-len 6 " | tail -1 || true)
|
||||
local ERRS; ERRS=$(echo "$OUT" | grep -A1 "^(ok-len 7 " | tail -1 || true)
|
||||
[ -z "$P" ] && P=0; [ -z "$F" ] && F=0
|
||||
if [ "$F" = "0" ] && [ "$P" -gt 0 ] 2>/dev/null; then
|
||||
PASS=$((PASS + P))
|
||||
[ "$VERBOSE" = "-v" ] && echo " ok $prog ($P)"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
ERRORS+=" FAIL [$prog] (${P} passed, ${F} failed) ${ERRS}
|
||||
"
|
||||
fi
|
||||
}
|
||||
|
||||
run_clos_suite \
|
||||
"lib/common-lisp/tests/programs/geometry.sx" \
|
||||
"geo-passed" "geo-failed" "geo-failures"
|
||||
|
||||
run_clos_suite \
|
||||
"lib/common-lisp/tests/programs/mop-trace.sx" \
|
||||
"mop-passed" "mop-failed" "mop-failures"
|
||||
|
||||
# ── Phase 5: macros + LOOP ───────────────────────────────────────────────────
|
||||
MACRO_FILE=$(mktemp); trap "rm -f $MACRO_FILE" EXIT
|
||||
printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/reader.sx")\n(epoch 3)\n(load "lib/common-lisp/parser.sx")\n(epoch 4)\n(load "lib/common-lisp/eval.sx")\n(epoch 5)\n(load "lib/common-lisp/loop.sx")\n(epoch 6)\n(load "lib/common-lisp/tests/macros.sx")\n(epoch 7)\n(eval "macro-passed")\n(epoch 8)\n(eval "macro-failed")\n(epoch 9)\n(eval "macro-failures")\n' > "$MACRO_FILE"
|
||||
MACRO_OUT=$(timeout 60 "$SX_SERVER" < "$MACRO_FILE" 2>/dev/null)
|
||||
rm -f "$MACRO_FILE"
|
||||
MACRO_PASSED=$(echo "$MACRO_OUT" | grep -A1 "^(ok-len 7 " | tail -1 || true)
|
||||
MACRO_FAILED=$(echo "$MACRO_OUT" | grep -A1 "^(ok-len 8 " | tail -1 || true)
|
||||
[ -z "$MACRO_PASSED" ] && MACRO_PASSED=0; [ -z "$MACRO_FAILED" ] && MACRO_FAILED=0
|
||||
if [ "$MACRO_FAILED" = "0" ] && [ "$MACRO_PASSED" -gt 0 ] 2>/dev/null; then
|
||||
PASS=$((PASS + MACRO_PASSED))
|
||||
[ "$VERBOSE" = "-v" ] && echo " ok Phase 5 macros+LOOP ($MACRO_PASSED)"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
ERRORS+=" FAIL [Phase 5 macros+LOOP] (${MACRO_PASSED} passed, ${MACRO_FAILED} failed)
|
||||
"
|
||||
fi
|
||||
|
||||
TOTAL=$((PASS+FAIL))
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo "ok $PASS/$TOTAL lib/common-lisp tests passed"
|
||||
|
||||
334
lib/common-lisp/tests/clos.sx
Normal file
334
lib/common-lisp/tests/clos.sx
Normal file
@@ -0,0 +1,334 @@
|
||||
;; lib/common-lisp/tests/clos.sx — CLOS test suite
|
||||
;;
|
||||
;; Loaded after: spec/stdlib.sx, lib/common-lisp/runtime.sx, lib/common-lisp/clos.sx
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
assert-equal
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
(define
|
||||
assert-true
|
||||
(fn
|
||||
(label got)
|
||||
(if
|
||||
got
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str "FAIL [" label "]: expected true, got " (inspect got)))))))))
|
||||
|
||||
(define
|
||||
assert-nil
|
||||
(fn
|
||||
(label got)
|
||||
(if
|
||||
(nil? got)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list (str "FAIL [" label "]: expected nil, got " (inspect got)))))))))
|
||||
|
||||
;; ── 1. class-of for built-in types ────────────────────────────────────────
|
||||
|
||||
(assert-equal "class-of integer" (clos-class-of 42) "integer")
|
||||
(assert-equal "class-of float" (clos-class-of 3.14) "float")
|
||||
(assert-equal "class-of string" (clos-class-of "hi") "string")
|
||||
(assert-equal "class-of nil" (clos-class-of nil) "null")
|
||||
(assert-equal "class-of list" (clos-class-of (list 1)) "cons")
|
||||
(assert-equal "class-of empty" (clos-class-of (list)) "null")
|
||||
|
||||
;; ── 2. subclass-of? ───────────────────────────────────────────────────────
|
||||
|
||||
(assert-true "integer subclass-of t" (clos-subclass-of? "integer" "t"))
|
||||
(assert-true "float subclass-of t" (clos-subclass-of? "float" "t"))
|
||||
(assert-true "t subclass-of t" (clos-subclass-of? "t" "t"))
|
||||
(assert-equal
|
||||
"integer not subclass-of float"
|
||||
(clos-subclass-of? "integer" "float")
|
||||
false)
|
||||
|
||||
;; ── 3. defclass + make-instance ───────────────────────────────────────────
|
||||
|
||||
(clos-defclass "point" (list "t") (list {:initform 0 :initarg ":x" :reader nil :writer nil :accessor "point-x" :name "x"} {:initform 0 :initarg ":y" :reader nil :writer nil :accessor "point-y" :name "y"}))
|
||||
|
||||
(let
|
||||
((p (clos-make-instance "point" ":x" 3 ":y" 4)))
|
||||
(begin
|
||||
(assert-equal "make-instance slot x" (clos-slot-value p "x") 3)
|
||||
(assert-equal "make-instance slot y" (clos-slot-value p "y") 4)
|
||||
(assert-equal "class-of instance" (clos-class-of p) "point")
|
||||
(assert-true "instance-of? point" (clos-instance-of? p "point"))
|
||||
(assert-true "instance-of? t" (clos-instance-of? p "t"))
|
||||
(assert-equal "instance-of? string" (clos-instance-of? p "string") false)))
|
||||
|
||||
;; initform defaults
|
||||
(let
|
||||
((p0 (clos-make-instance "point")))
|
||||
(begin
|
||||
(assert-equal "initform default x=0" (clos-slot-value p0 "x") 0)
|
||||
(assert-equal "initform default y=0" (clos-slot-value p0 "y") 0)))
|
||||
|
||||
;; ── 4. slot-value / set-slot-value! ──────────────────────────────────────
|
||||
|
||||
(let
|
||||
((p (clos-make-instance "point" ":x" 10 ":y" 20)))
|
||||
(begin
|
||||
(clos-set-slot-value! p "x" 99)
|
||||
(assert-equal "set-slot-value! x" (clos-slot-value p "x") 99)
|
||||
(assert-equal "slot-value y unchanged" (clos-slot-value p "y") 20)))
|
||||
|
||||
;; ── 5. slot-boundp ────────────────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((p (clos-make-instance "point" ":x" 5)))
|
||||
(begin
|
||||
(assert-true "slot-boundp x" (clos-slot-boundp p "x"))
|
||||
(assert-true "slot-boundp y (initform 0)" (clos-slot-boundp p "y"))))
|
||||
|
||||
;; ── 6. find-class ─────────────────────────────────────────────────────────
|
||||
|
||||
(assert-equal
|
||||
"find-class point"
|
||||
(get (clos-find-class "point") "name")
|
||||
"point")
|
||||
(assert-nil "find-class missing" (clos-find-class "no-such-class"))
|
||||
|
||||
;; ── 7. inheritance ────────────────────────────────────────────────────────
|
||||
|
||||
(clos-defclass "colored-point" (list "point") (list {:initform "white" :initarg ":color" :reader nil :writer nil :accessor nil :name "color"}))
|
||||
|
||||
(let
|
||||
((cp (clos-make-instance "colored-point" ":x" 1 ":y" 2 ":color" "red")))
|
||||
(begin
|
||||
(assert-equal "inherited slot x" (clos-slot-value cp "x") 1)
|
||||
(assert-equal "inherited slot y" (clos-slot-value cp "y") 2)
|
||||
(assert-equal "own slot color" (clos-slot-value cp "color") "red")
|
||||
(assert-true
|
||||
"instance-of? colored-point"
|
||||
(clos-instance-of? cp "colored-point"))
|
||||
(assert-true "instance-of? point (parent)" (clos-instance-of? cp "point"))
|
||||
(assert-true "instance-of? t (root)" (clos-instance-of? cp "t"))))
|
||||
|
||||
;; ── 8. defgeneric + primary method ───────────────────────────────────────
|
||||
|
||||
(clos-defgeneric "describe-obj" {})
|
||||
|
||||
(clos-defmethod
|
||||
"describe-obj"
|
||||
(list)
|
||||
(list "point")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((p (first args)))
|
||||
(str "(" (clos-slot-value p "x") "," (clos-slot-value p "y") ")"))))
|
||||
|
||||
(clos-defmethod
|
||||
"describe-obj"
|
||||
(list)
|
||||
(list "t")
|
||||
(fn (args next-fn) (str "object:" (inspect (first args)))))
|
||||
|
||||
(let
|
||||
((p (clos-make-instance "point" ":x" 3 ":y" 4)))
|
||||
(begin
|
||||
(assert-equal
|
||||
"primary method for point"
|
||||
(clos-call-generic "describe-obj" (list p))
|
||||
"(3,4)")
|
||||
(assert-equal
|
||||
"fallback t method"
|
||||
(clos-call-generic "describe-obj" (list 42))
|
||||
"object:42")))
|
||||
|
||||
;; ── 9. method inheritance + specificity ───────────────────────────────────
|
||||
|
||||
(clos-defmethod
|
||||
"describe-obj"
|
||||
(list)
|
||||
(list "colored-point")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((cp (first args)))
|
||||
(str
|
||||
(clos-slot-value cp "color")
|
||||
"@("
|
||||
(clos-slot-value cp "x")
|
||||
","
|
||||
(clos-slot-value cp "y")
|
||||
")"))))
|
||||
|
||||
(let
|
||||
((cp (clos-make-instance "colored-point" ":x" 5 ":y" 6 ":color" "blue")))
|
||||
(assert-equal
|
||||
"most specific method wins"
|
||||
(clos-call-generic "describe-obj" (list cp))
|
||||
"blue@(5,6)"))
|
||||
|
||||
;; ── 10. :before / :after / :around qualifiers ─────────────────────────────
|
||||
|
||||
(clos-defgeneric "logged-action" {})
|
||||
|
||||
(clos-defmethod
|
||||
"logged-action"
|
||||
(list "before")
|
||||
(list "t")
|
||||
(fn (args next-fn) (set! action-log (append action-log (list "before")))))
|
||||
|
||||
(clos-defmethod
|
||||
"logged-action"
|
||||
(list)
|
||||
(list "t")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(set! action-log (append action-log (list "primary")))
|
||||
"result"))
|
||||
|
||||
(clos-defmethod
|
||||
"logged-action"
|
||||
(list "after")
|
||||
(list "t")
|
||||
(fn (args next-fn) (set! action-log (append action-log (list "after")))))
|
||||
|
||||
(define action-log (list))
|
||||
(clos-call-generic "logged-action" (list 1))
|
||||
(assert-equal
|
||||
":before/:after order"
|
||||
action-log
|
||||
(list "before" "primary" "after"))
|
||||
|
||||
;; :around
|
||||
(define around-log (list))
|
||||
|
||||
(clos-defgeneric "wrapped-action" {})
|
||||
|
||||
(clos-defmethod
|
||||
"wrapped-action"
|
||||
(list "around")
|
||||
(list "t")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(set! around-log (append around-log (list "around-enter")))
|
||||
(let
|
||||
((r (next-fn)))
|
||||
(set! around-log (append around-log (list "around-exit")))
|
||||
r)))
|
||||
|
||||
(clos-defmethod
|
||||
"wrapped-action"
|
||||
(list)
|
||||
(list "t")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(set! around-log (append around-log (list "primary")))
|
||||
42))
|
||||
|
||||
(let
|
||||
((r (clos-call-generic "wrapped-action" (list nil))))
|
||||
(begin
|
||||
(assert-equal ":around result" r 42)
|
||||
(assert-equal
|
||||
":around log"
|
||||
around-log
|
||||
(list "around-enter" "primary" "around-exit"))))
|
||||
|
||||
;; ── 11. call-next-method ─────────────────────────────────────────────────
|
||||
|
||||
(clos-defgeneric "chain-test" {})
|
||||
|
||||
(clos-defmethod
|
||||
"chain-test"
|
||||
(list)
|
||||
(list "colored-point")
|
||||
(fn (args next-fn) (str "colored:" (clos-call-next-method next-fn))))
|
||||
|
||||
(clos-defmethod
|
||||
"chain-test"
|
||||
(list)
|
||||
(list "point")
|
||||
(fn (args next-fn) "point-base"))
|
||||
|
||||
(let
|
||||
((cp (clos-make-instance "colored-point" ":x" 0 ":y" 0 ":color" "green")))
|
||||
(assert-equal
|
||||
"call-next-method chains"
|
||||
(clos-call-generic "chain-test" (list cp))
|
||||
"colored:point-base"))
|
||||
|
||||
;; ── 12. accessor methods ──────────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((p (clos-make-instance "point" ":x" 7 ":y" 8)))
|
||||
(begin
|
||||
(assert-equal
|
||||
"accessor point-x"
|
||||
(clos-call-generic "point-x" (list p))
|
||||
7)
|
||||
(assert-equal
|
||||
"accessor point-y"
|
||||
(clos-call-generic "point-y" (list p))
|
||||
8)))
|
||||
|
||||
;; ── 13. with-slots ────────────────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((p (clos-make-instance "point" ":x" 3 ":y" 4)))
|
||||
(assert-equal
|
||||
"with-slots"
|
||||
(clos-with-slots p (list "x" "y") (fn (x y) (* x y)))
|
||||
12))
|
||||
|
||||
;; ── 14. change-class ─────────────────────────────────────────────────────
|
||||
|
||||
(clos-defclass "special-point" (list "point") (list {:initform "" :initarg ":label" :reader nil :writer nil :accessor nil :name "label"}))
|
||||
|
||||
(let
|
||||
((p (clos-make-instance "point" ":x" 1 ":y" 2)))
|
||||
(begin
|
||||
(clos-change-class! p "special-point")
|
||||
(assert-equal
|
||||
"change-class updates class"
|
||||
(clos-class-of p)
|
||||
"special-point")))
|
||||
|
||||
;; ── summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
(if
|
||||
(= failed 0)
|
||||
(print (str "ok " passed "/" (+ passed failed) " CLOS tests passed"))
|
||||
(begin
|
||||
(for-each (fn (f) (print f)) failures)
|
||||
(print
|
||||
(str "FAIL " passed "/" (+ passed failed) " passed, " failed " failed"))))
|
||||
478
lib/common-lisp/tests/conditions.sx
Normal file
478
lib/common-lisp/tests/conditions.sx
Normal file
@@ -0,0 +1,478 @@
|
||||
;; lib/common-lisp/tests/conditions.sx — Phase 3 condition system tests
|
||||
;;
|
||||
;; Loaded by lib/common-lisp/test.sh after:
|
||||
;; (load "spec/stdlib.sx")
|
||||
;; (load "lib/common-lisp/runtime.sx")
|
||||
;;
|
||||
;; Each test resets the handler/restart stacks to ensure isolation.
|
||||
|
||||
(define
|
||||
reset-stacks!
|
||||
(fn () (set! cl-handler-stack (list)) (set! cl-restart-stack (list))))
|
||||
|
||||
;; ── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
assert-equal
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
(define
|
||||
assert-true
|
||||
(fn
|
||||
(label got)
|
||||
(if
|
||||
got
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str "FAIL [" label "]: expected true, got " (inspect got)))))))))
|
||||
|
||||
(define
|
||||
assert-nil
|
||||
(fn
|
||||
(label got)
|
||||
(if
|
||||
(nil? got)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list (str "FAIL [" label "]: expected nil, got " (inspect got)))))))))
|
||||
|
||||
;; ── 1. condition predicates ────────────────────────────────────────────────
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
(let
|
||||
((c (cl-make-condition "simple-error" "format-control" "oops")))
|
||||
(begin
|
||||
(assert-true "cl-condition? on condition" (cl-condition? c))
|
||||
(assert-equal "cl-condition? on string" (cl-condition? "hello") false)
|
||||
(assert-equal "cl-condition? on number" (cl-condition? 42) false)
|
||||
(assert-equal "cl-condition? on nil" (cl-condition? nil) false)))
|
||||
|
||||
;; ── 2. cl-make-condition + slot access ────────────────────────────────────
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
(let
|
||||
((c (cl-make-condition "simple-error" "format-control" "msg" "format-arguments" (list 1 2))))
|
||||
(begin
|
||||
(assert-equal "class field" (get c "class") "simple-error")
|
||||
(assert-equal "cl-type field" (get c "cl-type") "cl-condition")
|
||||
(assert-equal
|
||||
"format-control slot"
|
||||
(cl-condition-slot c "format-control")
|
||||
"msg")
|
||||
(assert-equal
|
||||
"format-arguments slot"
|
||||
(cl-condition-slot c "format-arguments")
|
||||
(list 1 2))
|
||||
(assert-nil "missing slot is nil" (cl-condition-slot c "no-such-slot"))
|
||||
(assert-equal "condition-message" (cl-condition-message c) "msg")))
|
||||
|
||||
;; ── 3. cl-condition-of-type? — hierarchy walking ─────────────────────────
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
(let
|
||||
((se (cl-make-condition "simple-error" "format-control" "x"))
|
||||
(w (cl-make-condition "simple-warning" "format-control" "y"))
|
||||
(te
|
||||
(cl-make-condition
|
||||
"type-error"
|
||||
"datum"
|
||||
5
|
||||
"expected-type"
|
||||
"string"))
|
||||
(dz (cl-make-condition "division-by-zero")))
|
||||
(begin
|
||||
(assert-true
|
||||
"se isa simple-error"
|
||||
(cl-condition-of-type? se "simple-error"))
|
||||
(assert-true "se isa error" (cl-condition-of-type? se "error"))
|
||||
(assert-true
|
||||
"se isa serious-condition"
|
||||
(cl-condition-of-type? se "serious-condition"))
|
||||
(assert-true "se isa condition" (cl-condition-of-type? se "condition"))
|
||||
(assert-equal
|
||||
"se not isa warning"
|
||||
(cl-condition-of-type? se "warning")
|
||||
false)
|
||||
(assert-true
|
||||
"w isa simple-warning"
|
||||
(cl-condition-of-type? w "simple-warning"))
|
||||
(assert-true "w isa warning" (cl-condition-of-type? w "warning"))
|
||||
(assert-true "w isa condition" (cl-condition-of-type? w "condition"))
|
||||
(assert-equal "w not isa error" (cl-condition-of-type? w "error") false)
|
||||
(assert-true "te isa type-error" (cl-condition-of-type? te "type-error"))
|
||||
(assert-true "te isa error" (cl-condition-of-type? te "error"))
|
||||
(assert-true
|
||||
"dz isa division-by-zero"
|
||||
(cl-condition-of-type? dz "division-by-zero"))
|
||||
(assert-true
|
||||
"dz isa arithmetic-error"
|
||||
(cl-condition-of-type? dz "arithmetic-error"))
|
||||
(assert-true "dz isa error" (cl-condition-of-type? dz "error"))
|
||||
(assert-equal
|
||||
"non-condition not isa anything"
|
||||
(cl-condition-of-type? 42 "error")
|
||||
false)))
|
||||
|
||||
;; ── 4. cl-define-condition ────────────────────────────────────────────────
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
(begin
|
||||
(cl-define-condition "my-app-error" (list "error") (list "code" "detail"))
|
||||
(let
|
||||
((c (cl-make-condition "my-app-error" "code" 404 "detail" "not found")))
|
||||
(begin
|
||||
(assert-true "user condition: cl-condition?" (cl-condition? c))
|
||||
(assert-true
|
||||
"user condition isa my-app-error"
|
||||
(cl-condition-of-type? c "my-app-error"))
|
||||
(assert-true
|
||||
"user condition isa error"
|
||||
(cl-condition-of-type? c "error"))
|
||||
(assert-true
|
||||
"user condition isa condition"
|
||||
(cl-condition-of-type? c "condition"))
|
||||
(assert-equal
|
||||
"user condition slot code"
|
||||
(cl-condition-slot c "code")
|
||||
404)
|
||||
(assert-equal
|
||||
"user condition slot detail"
|
||||
(cl-condition-slot c "detail")
|
||||
"not found"))))
|
||||
|
||||
;; ── 5. cl-handler-bind (non-unwinding) ───────────────────────────────────
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
(let
|
||||
((log (list)))
|
||||
(begin
|
||||
(cl-handler-bind
|
||||
(list
|
||||
(list
|
||||
"error"
|
||||
(fn (c) (set! log (append log (list (cl-condition-message c)))))))
|
||||
(fn
|
||||
()
|
||||
(cl-signal (cl-make-condition "simple-error" "format-control" "oops"))))
|
||||
(assert-equal "handler-bind: handler fired" log (list "oops"))))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; Non-unwinding: body continues after signal
|
||||
(let
|
||||
((body-ran false))
|
||||
(begin
|
||||
(cl-handler-bind
|
||||
(list (list "error" (fn (c) nil)))
|
||||
(fn
|
||||
()
|
||||
(cl-signal (cl-make-condition "simple-error" "format-control" "x"))
|
||||
(set! body-ran true)))
|
||||
(assert-true "handler-bind: body continues after signal" body-ran)))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; Type filtering: warning handler does not fire for error
|
||||
(let
|
||||
((w-fired false))
|
||||
(begin
|
||||
(cl-handler-bind
|
||||
(list (list "warning" (fn (c) (set! w-fired true))))
|
||||
(fn
|
||||
()
|
||||
(cl-signal (cl-make-condition "simple-error" "format-control" "e"))))
|
||||
(assert-equal
|
||||
"handler-bind: type filter (warning ignores error)"
|
||||
w-fired
|
||||
false)))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; Multiple handlers: both matching handlers fire
|
||||
(let
|
||||
((log (list)))
|
||||
(begin
|
||||
(cl-handler-bind
|
||||
(list
|
||||
(list "error" (fn (c) (set! log (append log (list "e1")))))
|
||||
(list "condition" (fn (c) (set! log (append log (list "e2"))))))
|
||||
(fn
|
||||
()
|
||||
(cl-signal (cl-make-condition "simple-error" "format-control" "x"))))
|
||||
(assert-equal "handler-bind: both handlers fire" log (list "e1" "e2"))))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 6. cl-handler-case (unwinding) ───────────────────────────────────────
|
||||
|
||||
;; Catches error, returns handler result
|
||||
(let
|
||||
((result (cl-handler-case (fn () (cl-error "boom") 99) (list "error" (fn (c) (str "caught: " (cl-condition-message c)))))))
|
||||
(assert-equal "handler-case: catches error" result "caught: boom"))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; Returns body result when no signal
|
||||
(let
|
||||
((result (cl-handler-case (fn () 42) (list "error" (fn (c) -1)))))
|
||||
(assert-equal "handler-case: body result" result 42))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; Only first matching handler runs (unwinding)
|
||||
(let
|
||||
((result (cl-handler-case (fn () (cl-error "x")) (list "simple-error" (fn (c) "simple")) (list "error" (fn (c) "error")))))
|
||||
(assert-equal "handler-case: most specific wins" result "simple"))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 7. cl-warn ────────────────────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((warned false))
|
||||
(begin
|
||||
(cl-handler-bind
|
||||
(list (list "warning" (fn (c) (set! warned true))))
|
||||
(fn () (cl-warn "be careful")))
|
||||
(assert-true "cl-warn: fires warning handler" warned)))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; Warn with condition object
|
||||
(let
|
||||
((msg ""))
|
||||
(begin
|
||||
(cl-handler-bind
|
||||
(list (list "warning" (fn (c) (set! msg (cl-condition-message c)))))
|
||||
(fn
|
||||
()
|
||||
(cl-warn
|
||||
(cl-make-condition "simple-warning" "format-control" "take care"))))
|
||||
(assert-equal "cl-warn: condition object" msg "take care")))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 8. cl-restart-case + cl-invoke-restart ───────────────────────────────
|
||||
|
||||
;; Basic restart invocation
|
||||
(let
|
||||
((result (cl-restart-case (fn () (cl-invoke-restart "use-zero")) (list "use-zero" (list) (fn () 0)))))
|
||||
(assert-equal "restart-case: invoke-restart use-zero" result 0))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; Restart with argument
|
||||
(let
|
||||
((result (cl-restart-case (fn () (cl-invoke-restart "use-value" 77)) (list "use-value" (list "v") (fn (v) v)))))
|
||||
(assert-equal "restart-case: invoke-restart with arg" result 77))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; Body returns normally when restart not invoked
|
||||
(let
|
||||
((result (cl-restart-case (fn () 42) (list "never-used" (list) (fn () -1)))))
|
||||
(assert-equal "restart-case: body result" result 42))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 9. cl-with-simple-restart ─────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((result (cl-with-simple-restart "skip" "Skip this step" (fn () (cl-invoke-restart "skip") 99))))
|
||||
(assert-nil "with-simple-restart: invoke returns nil" result))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 10. cl-find-restart ───────────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((found (cl-restart-case (fn () (cl-find-restart "retry")) (list "retry" (list) (fn () nil)))))
|
||||
(assert-true "find-restart: finds active restart" (not (nil? found))))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
(let
|
||||
((not-found (cl-restart-case (fn () (cl-find-restart "nonexistent")) (list "retry" (list) (fn () nil)))))
|
||||
(assert-nil "find-restart: nil for inactive restart" not-found))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 11. cl-compute-restarts ───────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((names (cl-restart-case (fn () (cl-restart-case (fn () (cl-compute-restarts)) (list "inner" (list) (fn () nil)))) (list "outer" (list) (fn () nil)))))
|
||||
(assert-equal
|
||||
"compute-restarts: both restarts"
|
||||
names
|
||||
(list "inner" "outer")))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 12. handler-bind + restart-case interop ───────────────────────────────
|
||||
|
||||
;; Classic CL pattern: error handler invokes a restart
|
||||
(let
|
||||
((result (cl-restart-case (fn () (cl-handler-bind (list (list "error" (fn (c) (cl-invoke-restart "use-zero")))) (fn () (cl-error "divide by zero")))) (list "use-zero" (list) (fn () 0)))))
|
||||
(assert-equal "interop: handler invokes restart" result 0))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 13. cl-cerror ─────────────────────────────────────────────────────────
|
||||
|
||||
;; When "continue" restart is invoked, cerror returns nil
|
||||
(let
|
||||
((result (cl-restart-case (fn () (cl-cerror "continue anyway" "something bad") 42) (list "continue" (list) (fn () "resumed")))))
|
||||
(assert-true
|
||||
"cerror: returns"
|
||||
(or (nil? result) (= result 42) (= result "resumed"))))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 14. slot accessor helpers ─────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((c (cl-make-condition "simple-error" "format-control" "msg" "format-arguments" (list 1 2))))
|
||||
(begin
|
||||
(assert-equal
|
||||
"simple-condition-format-control"
|
||||
(cl-simple-condition-format-control c)
|
||||
"msg")
|
||||
(assert-equal
|
||||
"simple-condition-format-arguments"
|
||||
(cl-simple-condition-format-arguments c)
|
||||
(list 1 2))))
|
||||
|
||||
(let
|
||||
((c (cl-make-condition "type-error" "datum" 42 "expected-type" "string")))
|
||||
(begin
|
||||
(assert-equal "type-error-datum" (cl-type-error-datum c) 42)
|
||||
(assert-equal
|
||||
"type-error-expected-type"
|
||||
(cl-type-error-expected-type c)
|
||||
"string")))
|
||||
|
||||
(let
|
||||
((c (cl-make-condition "arithmetic-error" "operation" "/" "operands" (list 1 0))))
|
||||
(begin
|
||||
(assert-equal
|
||||
"arithmetic-error-operation"
|
||||
(cl-arithmetic-error-operation c)
|
||||
"/")
|
||||
(assert-equal
|
||||
"arithmetic-error-operands"
|
||||
(cl-arithmetic-error-operands c)
|
||||
(list 1 0))))
|
||||
|
||||
|
||||
;; ── 15. *debugger-hook* ───────────────────────────────────────────────────
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
(let ((received nil))
|
||||
(begin
|
||||
(set! cl-debugger-hook
|
||||
(fn (c h)
|
||||
(set! received (cl-condition-message c))
|
||||
(cl-invoke-restart "escape")))
|
||||
(cl-restart-case
|
||||
(fn () (cl-error "debugger test"))
|
||||
(list "escape" (list) (fn () nil)))
|
||||
(set! cl-debugger-hook nil)
|
||||
(assert-equal "debugger-hook receives condition" received "debugger test")))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 16. *break-on-signals* ────────────────────────────────────────────────
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
(let ((triggered false))
|
||||
(begin
|
||||
(set! cl-break-on-signals "error")
|
||||
(set! cl-debugger-hook
|
||||
(fn (c h)
|
||||
(set! triggered true)
|
||||
(cl-invoke-restart "abort")))
|
||||
(cl-restart-case
|
||||
(fn ()
|
||||
(cl-signal (cl-make-condition "simple-error" "format-control" "x")))
|
||||
(list "abort" (list) (fn () nil)))
|
||||
(set! cl-break-on-signals nil)
|
||||
(set! cl-debugger-hook nil)
|
||||
(assert-true "break-on-signals fires hook" triggered)))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; break-on-signals: non-matching type does NOT fire hook
|
||||
(let ((triggered false))
|
||||
(begin
|
||||
(set! cl-break-on-signals "error")
|
||||
(set! cl-debugger-hook
|
||||
(fn (c h) (set! triggered true) nil))
|
||||
(cl-handler-bind
|
||||
(list (list "warning" (fn (c) nil)))
|
||||
(fn ()
|
||||
(cl-signal (cl-make-condition "simple-warning" "format-control" "w"))))
|
||||
(set! cl-break-on-signals nil)
|
||||
(set! cl-debugger-hook nil)
|
||||
(assert-equal "break-on-signals: type mismatch not triggered" triggered false)))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── 17. cl-invoke-restart-interactively ──────────────────────────────────
|
||||
|
||||
(let ((result
|
||||
(cl-restart-case
|
||||
(fn () (cl-invoke-restart-interactively "use-default"))
|
||||
(list "use-default" (list) (fn () 99)))))
|
||||
(assert-equal "invoke-restart-interactively: returns restart value" result 99))
|
||||
|
||||
(reset-stacks!)
|
||||
|
||||
;; ── summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
(if
|
||||
(= failed 0)
|
||||
(print (str "ok " passed "/" (+ passed failed) " condition tests passed"))
|
||||
(begin
|
||||
(for-each (fn (f) (print f)) failures)
|
||||
(print
|
||||
(str "FAIL " passed "/" (+ passed failed) " passed, " failed " failed"))))
|
||||
466
lib/common-lisp/tests/eval.sx
Normal file
466
lib/common-lisp/tests/eval.sx
Normal file
@@ -0,0 +1,466 @@
|
||||
;; CL evaluator tests
|
||||
|
||||
(define cl-test-pass 0)
|
||||
(define cl-test-fail 0)
|
||||
(define cl-test-fails (list))
|
||||
|
||||
(define
|
||||
cl-deep=
|
||||
(fn
|
||||
(a b)
|
||||
(cond
|
||||
((= a b) true)
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((ak (keys a)) (bk (keys b)))
|
||||
(if
|
||||
(not (= (len ak) (len bk)))
|
||||
false
|
||||
(every?
|
||||
(fn (k) (and (has-key? b k) (cl-deep= (get a k) (get b k))))
|
||||
ak))))
|
||||
((and (list? a) (list? b))
|
||||
(if
|
||||
(not (= (len a) (len b)))
|
||||
false
|
||||
(let
|
||||
((i 0) (ok true))
|
||||
(define
|
||||
chk
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(and ok (< i (len a)))
|
||||
(do
|
||||
(when
|
||||
(not (cl-deep= (nth a i) (nth b i)))
|
||||
(set! ok false))
|
||||
(set! i (+ i 1))
|
||||
(chk)))))
|
||||
(chk)
|
||||
ok)))
|
||||
(:else false))))
|
||||
|
||||
(define
|
||||
cl-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(cl-deep= actual expected)
|
||||
(set! cl-test-pass (+ cl-test-pass 1))
|
||||
(do
|
||||
(set! cl-test-fail (+ cl-test-fail 1))
|
||||
(append! cl-test-fails {:name name :expected expected :actual actual})))))
|
||||
|
||||
;; Convenience: evaluate CL string with fresh env each time
|
||||
(define ev (fn (src) (cl-eval-str src (cl-make-env))))
|
||||
(define evall (fn (src) (cl-eval-all-str src (cl-make-env))))
|
||||
|
||||
;; ── self-evaluating literals ──────────────────────────────────────
|
||||
|
||||
(cl-test "lit: nil" (ev "nil") nil)
|
||||
(cl-test "lit: t" (ev "t") true)
|
||||
(cl-test "lit: integer" (ev "42") 42)
|
||||
(cl-test "lit: negative" (ev "-7") -7)
|
||||
(cl-test "lit: zero" (ev "0") 0)
|
||||
(cl-test "lit: string" (ev "\"hello\"") "hello")
|
||||
(cl-test "lit: empty string" (ev "\"\"") "")
|
||||
(cl-test "lit: keyword type" (get (ev ":foo") "cl-type") "keyword")
|
||||
(cl-test "lit: keyword name" (get (ev ":foo") "name") "FOO")
|
||||
(cl-test "lit: float type" (get (ev "3.14") "cl-type") "float")
|
||||
|
||||
;; ── QUOTE ─────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "quote: symbol" (ev "'x") "X")
|
||||
(cl-test "quote: list" (ev "'(a b c)") (list "A" "B" "C"))
|
||||
(cl-test "quote: nil" (ev "'nil") nil)
|
||||
(cl-test "quote: integer" (ev "'42") 42)
|
||||
(cl-test "quote: nested" (ev "'(a (b c))") (list "A" (list "B" "C")))
|
||||
|
||||
;; ── IF ────────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "if: true branch" (ev "(if t 1 2)") 1)
|
||||
(cl-test "if: false branch" (ev "(if nil 1 2)") 2)
|
||||
(cl-test "if: no else nil" (ev "(if nil 99)") nil)
|
||||
(cl-test "if: number truthy" (ev "(if 0 'yes 'no)") "YES")
|
||||
(cl-test "if: empty string truthy" (ev "(if \"\" 'yes 'no)") "YES")
|
||||
(cl-test "if: nested" (ev "(if t (if nil 1 2) 3)") 2)
|
||||
|
||||
;; ── PROGN ────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "progn: single" (ev "(progn 42)") 42)
|
||||
(cl-test "progn: multiple" (ev "(progn 1 2 3)") 3)
|
||||
(cl-test "progn: nil last" (ev "(progn 1 nil)") nil)
|
||||
|
||||
;; ── AND / OR ─────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "and: empty" (ev "(and)") true)
|
||||
(cl-test "and: all true" (ev "(and 1 2 3)") 3)
|
||||
(cl-test "and: short-circuit" (ev "(and nil 99)") nil)
|
||||
(cl-test "and: returns last" (ev "(and 1 2)") 2)
|
||||
(cl-test "or: empty" (ev "(or)") nil)
|
||||
(cl-test "or: first truthy" (ev "(or 1 2)") 1)
|
||||
(cl-test "or: all nil" (ev "(or nil nil)") nil)
|
||||
(cl-test "or: short-circuit" (ev "(or nil 42)") 42)
|
||||
|
||||
;; ── COND ─────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "cond: first match" (ev "(cond (t 1) (t 2))") 1)
|
||||
(cl-test "cond: second match" (ev "(cond (nil 1) (t 2))") 2)
|
||||
(cl-test "cond: no match" (ev "(cond (nil 1) (nil 2))") nil)
|
||||
(cl-test "cond: returns test value" (ev "(cond (42))") 42)
|
||||
|
||||
;; ── WHEN / UNLESS ─────────────────────────────────────────────────
|
||||
|
||||
(cl-test "when: true" (ev "(when t 1 2 3)") 3)
|
||||
(cl-test "when: nil" (ev "(when nil 99)") nil)
|
||||
(cl-test "unless: nil runs" (ev "(unless nil 42)") 42)
|
||||
(cl-test "unless: true skips" (ev "(unless t 99)") nil)
|
||||
|
||||
;; ── LET ──────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "let: empty bindings" (ev "(let () 42)") 42)
|
||||
(cl-test "let: single binding" (ev "(let ((x 5)) x)") 5)
|
||||
(cl-test "let: two bindings" (ev "(let ((x 3) (y 4)) (+ x y))") 7)
|
||||
(cl-test "let: parallel" (ev "(let ((x 1)) (let ((x 2) (y x)) y))") 1)
|
||||
(cl-test "let: nested" (ev "(let ((x 1)) (let ((y 2)) (+ x y)))") 3)
|
||||
(cl-test "let: progn body" (ev "(let ((x 5)) (+ x 1) (* x 2))") 10)
|
||||
(cl-test "let: bare name nil" (ev "(let (x) x)") nil)
|
||||
|
||||
;; ── LET* ─────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "let*: sequential" (ev "(let* ((x 1) (y (+ x 1))) y)") 2)
|
||||
(cl-test "let*: chain" (ev "(let* ((a 2) (b (* a 3)) (c (+ b 1))) c)") 7)
|
||||
(cl-test "let*: shadow" (ev "(let ((x 1)) (let* ((x 2) (y x)) y))") 2)
|
||||
|
||||
;; ── SETQ / SETF ──────────────────────────────────────────────────
|
||||
|
||||
(cl-test "setq: basic" (ev "(let ((x 0)) (setq x 5) x)") 5)
|
||||
(cl-test "setq: returns value" (ev "(let ((x 0)) (setq x 99))") 99)
|
||||
(cl-test "setf: basic" (ev "(let ((x 0)) (setf x 7) x)") 7)
|
||||
|
||||
;; ── LAMBDA ────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "lambda: call" (ev "((lambda (x) x) 42)") 42)
|
||||
(cl-test "lambda: multi-arg" (ev "((lambda (x y) (+ x y)) 3 4)") 7)
|
||||
(cl-test "lambda: closure" (ev "(let ((n 10)) ((lambda (x) (+ x n)) 5))") 15)
|
||||
(cl-test "lambda: rest arg"
|
||||
(ev "((lambda (x &rest xs) (cons x xs)) 1 2 3)")
|
||||
{:cl-type "cons" :car 1 :cdr (list 2 3)})
|
||||
(cl-test "lambda: optional no default"
|
||||
(ev "((lambda (&optional x) x))")
|
||||
nil)
|
||||
(cl-test "lambda: optional with arg"
|
||||
(ev "((lambda (&optional (x 99)) x) 42)")
|
||||
42)
|
||||
(cl-test "lambda: optional default used"
|
||||
(ev "((lambda (&optional (x 7)) x))")
|
||||
7)
|
||||
|
||||
;; ── FUNCTION ─────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "function: lambda" (get (ev "(function (lambda (x) x))") "cl-type") "function")
|
||||
|
||||
;; ── DEFUN ────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "defun: returns name" (evall "(defun sq (x) (* x x))") "SQ")
|
||||
(cl-test "defun: call" (evall "(defun sq (x) (* x x)) (sq 5)") 25)
|
||||
(cl-test "defun: multi-arg" (evall "(defun add (x y) (+ x y)) (add 3 4)") 7)
|
||||
(cl-test "defun: recursive factorial"
|
||||
(evall "(defun fact (n) (if (<= n 1) 1 (* n (fact (- n 1))))) (fact 5)")
|
||||
120)
|
||||
(cl-test "defun: multiple calls"
|
||||
(evall "(defun double (x) (* x 2)) (+ (double 3) (double 5))")
|
||||
16)
|
||||
|
||||
;; ── FLET ─────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "flet: basic"
|
||||
(ev "(flet ((double (x) (* x 2))) (double 5))")
|
||||
10)
|
||||
(cl-test "flet: sees outer vars"
|
||||
(ev "(let ((n 3)) (flet ((add-n (x) (+ x n))) (add-n 7)))")
|
||||
10)
|
||||
(cl-test "flet: non-recursive"
|
||||
(ev "(flet ((f (x) (+ x 1))) (flet ((f (x) (f (f x)))) (f 5)))")
|
||||
7)
|
||||
|
||||
;; ── LABELS ────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "labels: basic"
|
||||
(ev "(labels ((greet (x) x)) (greet 42))")
|
||||
42)
|
||||
(cl-test "labels: recursive"
|
||||
(ev "(labels ((count (n) (if (<= n 0) 0 (+ 1 (count (- n 1)))))) (count 5))")
|
||||
5)
|
||||
(cl-test "labels: mutual recursion"
|
||||
(ev "(labels
|
||||
((even? (n) (if (= n 0) t (odd? (- n 1))))
|
||||
(odd? (n) (if (= n 0) nil (even? (- n 1)))))
|
||||
(list (even? 4) (odd? 3)))")
|
||||
(list true true))
|
||||
|
||||
;; ── THE / LOCALLY / EVAL-WHEN ────────────────────────────────────
|
||||
|
||||
(cl-test "the: passthrough" (ev "(the integer 42)") 42)
|
||||
(cl-test "the: string" (ev "(the string \"hi\")") "hi")
|
||||
(cl-test "locally: body" (ev "(locally 1 2 3)") 3)
|
||||
(cl-test "eval-when: execute" (ev "(eval-when (:execute) 99)") 99)
|
||||
(cl-test "eval-when: no execute" (ev "(eval-when (:compile-toplevel) 99)") nil)
|
||||
|
||||
;; ── DEFVAR / DEFPARAMETER ────────────────────────────────────────
|
||||
|
||||
(cl-test "defvar: returns name" (evall "(defvar *x* 10)") "*X*")
|
||||
(cl-test "defparameter: sets value" (evall "(defparameter *y* 42) *y*") 42)
|
||||
(cl-test "defvar: no reinit" (evall "(defvar *z* 1) (defvar *z* 99) *z*") 1)
|
||||
|
||||
;; ── built-in arithmetic ───────────────────────────────────────────
|
||||
|
||||
(cl-test "arith: +" (ev "(+ 1 2 3)") 6)
|
||||
(cl-test "arith: + zero" (ev "(+)") 0)
|
||||
(cl-test "arith: -" (ev "(- 10 3 2)") 5)
|
||||
(cl-test "arith: - negate" (ev "(- 5)") -5)
|
||||
(cl-test "arith: *" (ev "(* 2 3 4)") 24)
|
||||
(cl-test "arith: * one" (ev "(*)") 1)
|
||||
(cl-test "arith: /" (ev "(/ 12 3)") 4)
|
||||
(cl-test "arith: max" (ev "(max 3 1 4 1 5)") 5)
|
||||
(cl-test "arith: min" (ev "(min 3 1 4 1 5)") 1)
|
||||
(cl-test "arith: abs neg" (ev "(abs -7)") 7)
|
||||
(cl-test "arith: abs pos" (ev "(abs 7)") 7)
|
||||
|
||||
;; ── built-in comparisons ──────────────────────────────────────────
|
||||
|
||||
(cl-test "cmp: = true" (ev "(= 3 3)") true)
|
||||
(cl-test "cmp: = false" (ev "(= 3 4)") nil)
|
||||
(cl-test "cmp: /=" (ev "(/= 3 4)") true)
|
||||
(cl-test "cmp: <" (ev "(< 1 2)") true)
|
||||
(cl-test "cmp: > false" (ev "(> 1 2)") nil)
|
||||
(cl-test "cmp: <=" (ev "(<= 2 2)") true)
|
||||
|
||||
;; ── built-in predicates ───────────────────────────────────────────
|
||||
|
||||
(cl-test "pred: null nil" (ev "(null nil)") true)
|
||||
(cl-test "pred: null non-nil" (ev "(null 5)") nil)
|
||||
(cl-test "pred: not nil" (ev "(not nil)") true)
|
||||
(cl-test "pred: not truthy" (ev "(not 5)") nil)
|
||||
(cl-test "pred: numberp" (ev "(numberp 5)") true)
|
||||
(cl-test "pred: numberp str" (ev "(numberp \"x\")") nil)
|
||||
(cl-test "pred: stringp" (ev "(stringp \"hello\")") true)
|
||||
(cl-test "pred: listp list" (ev "(listp '(1))") true)
|
||||
(cl-test "pred: listp nil" (ev "(listp nil)") true)
|
||||
(cl-test "pred: zerop" (ev "(zerop 0)") true)
|
||||
(cl-test "pred: plusp" (ev "(plusp 3)") true)
|
||||
(cl-test "pred: evenp" (ev "(evenp 4)") true)
|
||||
(cl-test "pred: oddp" (ev "(oddp 3)") true)
|
||||
|
||||
;; ── built-in list ops ─────────────────────────────────────────────
|
||||
|
||||
(cl-test "list: car" (ev "(car '(1 2 3))") 1)
|
||||
(cl-test "list: cdr" (ev "(cdr '(1 2 3))") (list 2 3))
|
||||
(cl-test "list: cons" (get (ev "(cons 1 2)") "car") 1)
|
||||
(cl-test "list: list fn" (ev "(list 1 2 3)") (list 1 2 3))
|
||||
(cl-test "list: length" (ev "(length '(a b c))") 3)
|
||||
(cl-test "list: length nil" (ev "(length nil)") 0)
|
||||
(cl-test "list: append" (ev "(append '(1 2) '(3 4))") (list 1 2 3 4))
|
||||
(cl-test "list: first" (ev "(first '(10 20 30))") 10)
|
||||
(cl-test "list: second" (ev "(second '(10 20 30))") 20)
|
||||
(cl-test "list: third" (ev "(third '(10 20 30))") 30)
|
||||
(cl-test "list: rest" (ev "(rest '(1 2 3))") (list 2 3))
|
||||
(cl-test "list: nth" (ev "(nth 1 '(a b c))") "B")
|
||||
(cl-test "list: reverse" (ev "(reverse '(1 2 3))") (list 3 2 1))
|
||||
|
||||
;; ── FUNCALL / APPLY / MAPCAR ─────────────────────────────────────
|
||||
|
||||
(cl-test "funcall: lambda"
|
||||
(ev "(funcall (lambda (x) (* x x)) 5)")
|
||||
25)
|
||||
(cl-test "apply: basic"
|
||||
(ev "(apply #'+ '(1 2 3))")
|
||||
6)
|
||||
(cl-test "apply: leading args"
|
||||
(ev "(apply #'+ 1 2 '(3 4))")
|
||||
10)
|
||||
(cl-test "mapcar: basic"
|
||||
(ev "(mapcar (lambda (x) (* x 2)) '(1 2 3))")
|
||||
(list 2 4 6))
|
||||
|
||||
;; ── BLOCK / RETURN-FROM / RETURN ─────────────────────────────────
|
||||
|
||||
(cl-test "block: last form value"
|
||||
(ev "(block done 1 2 3)")
|
||||
3)
|
||||
(cl-test "block: empty body"
|
||||
(ev "(block done)")
|
||||
nil)
|
||||
(cl-test "block: single form"
|
||||
(ev "(block foo 42)")
|
||||
42)
|
||||
(cl-test "block: return-from"
|
||||
(ev "(block done 1 (return-from done 99) 2)")
|
||||
99)
|
||||
(cl-test "block: return-from nil block"
|
||||
(ev "(block nil 1 (return-from nil 42) 3)")
|
||||
42)
|
||||
(cl-test "block: return-from no value"
|
||||
(ev "(block done (return-from done))")
|
||||
nil)
|
||||
(cl-test "block: nested inner return stays inner"
|
||||
(ev "(block outer (block inner (return-from inner 1) 2) 3)")
|
||||
3)
|
||||
(cl-test "block: nested outer return"
|
||||
(ev "(block outer (block inner 1 2) (return-from outer 99) 3)")
|
||||
99)
|
||||
(cl-test "return: shorthand for nil block"
|
||||
(ev "(block nil (return 77))")
|
||||
77)
|
||||
(cl-test "return: no value"
|
||||
(ev "(block nil 1 (return) 2)")
|
||||
nil)
|
||||
(cl-test "block: return-from inside let"
|
||||
(ev "(block done (let ((x 5)) (when (> x 3) (return-from done x))) 0)")
|
||||
5)
|
||||
(cl-test "block: return-from inside progn"
|
||||
(ev "(block done (progn (return-from done 7) 99))")
|
||||
7)
|
||||
(cl-test "block: return-from through function"
|
||||
(ev "(block done (flet ((f () (return-from done 42))) (f)) nil)")
|
||||
42)
|
||||
|
||||
;; ── TAGBODY / GO ─────────────────────────────────────────────────
|
||||
|
||||
(cl-test "tagbody: empty returns nil"
|
||||
(ev "(tagbody)")
|
||||
nil)
|
||||
(cl-test "tagbody: forms only, returns nil"
|
||||
(ev "(let ((x 0)) (tagbody (setq x 1) (setq x 2)) x)")
|
||||
2)
|
||||
(cl-test "tagbody: tag only, returns nil"
|
||||
(ev "(tagbody done)")
|
||||
nil)
|
||||
(cl-test "tagbody: go skips forms"
|
||||
(ev "(let ((x 0)) (tagbody (go done) (setq x 99) done) x)")
|
||||
0)
|
||||
(cl-test "tagbody: go to later tag"
|
||||
(ev "(let ((x 0)) (tagbody start (setq x (+ x 1)) (go done) (setq x 99) done) x)")
|
||||
1)
|
||||
(cl-test "tagbody: loop with counter"
|
||||
(ev "(let ((n 0)) (tagbody loop (when (>= n 3) (go done)) (setq n (+ n 1)) (go loop) done) n)")
|
||||
3)
|
||||
(cl-test "tagbody: go inside when"
|
||||
(ev "(let ((x 0)) (tagbody (setq x 1) (when t (go done)) (setq x 99) done) x)")
|
||||
1)
|
||||
(cl-test "tagbody: go inside progn"
|
||||
(ev "(let ((x 0)) (tagbody (progn (setq x 1) (go done)) (setq x 99) done) x)")
|
||||
1)
|
||||
(cl-test "tagbody: go inside let"
|
||||
(ev "(let ((acc 0)) (tagbody (let ((y 5)) (when (> y 3) (go done))) (setq acc 99) done) acc)")
|
||||
0)
|
||||
(cl-test "tagbody: integer tags"
|
||||
(ev "(let ((x 0)) (tagbody (go 2) 1 (setq x 1) (go 3) 2 (setq x 2) (go 3) 3) x)")
|
||||
2)
|
||||
(cl-test "tagbody: block-return propagates out"
|
||||
(ev "(block done (tagbody (return-from done 42)) nil)")
|
||||
42)
|
||||
|
||||
;; ── UNWIND-PROTECT ───────────────────────────────────────────────
|
||||
|
||||
(cl-test "unwind-protect: normal returns protected"
|
||||
(ev "(unwind-protect 42 nil)")
|
||||
42)
|
||||
(cl-test "unwind-protect: cleanup runs"
|
||||
(ev "(let ((x 0)) (unwind-protect 1 (setq x 99)) x)")
|
||||
99)
|
||||
(cl-test "unwind-protect: cleanup result ignored"
|
||||
(ev "(unwind-protect 42 777)")
|
||||
42)
|
||||
(cl-test "unwind-protect: multiple cleanup forms"
|
||||
(ev "(let ((x 0)) (unwind-protect 1 (setq x (+ x 1)) (setq x (+ x 1))) x)")
|
||||
2)
|
||||
(cl-test "unwind-protect: cleanup on return-from"
|
||||
(ev "(let ((x 0)) (block done (unwind-protect (return-from done 7) (setq x 99))) x)")
|
||||
99)
|
||||
(cl-test "unwind-protect: return-from still propagates"
|
||||
(ev "(block done (unwind-protect (return-from done 42) nil))")
|
||||
42)
|
||||
(cl-test "unwind-protect: cleanup on go"
|
||||
(ev "(let ((x 0)) (tagbody (unwind-protect (go done) (setq x 1)) done) x)")
|
||||
1)
|
||||
(cl-test "unwind-protect: nested, inner cleanup first"
|
||||
(ev "(let ((n 0)) (unwind-protect (unwind-protect 1 (setq n (+ n 10))) (setq n (+ n 1))) n)")
|
||||
11)
|
||||
|
||||
;; ── VALUES / MULTIPLE-VALUE-BIND / NTH-VALUE ────────────────────
|
||||
|
||||
(cl-test "values: single returns plain"
|
||||
(ev "(values 42)")
|
||||
42)
|
||||
(cl-test "values: zero returns nil"
|
||||
(ev "(values)")
|
||||
nil)
|
||||
(cl-test "values: multi — primary via funcall"
|
||||
(ev "(car (list (values 1 2)))")
|
||||
1)
|
||||
(cl-test "multiple-value-bind: basic"
|
||||
(ev "(multiple-value-bind (a b) (values 1 2) (+ a b))")
|
||||
3)
|
||||
(cl-test "multiple-value-bind: extra vars get nil"
|
||||
(ev "(multiple-value-bind (a b c) (values 10 20) (list a b c))")
|
||||
(list 10 20 nil))
|
||||
(cl-test "multiple-value-bind: extra values ignored"
|
||||
(ev "(multiple-value-bind (a) (values 1 2 3) a)")
|
||||
1)
|
||||
(cl-test "multiple-value-bind: single value source"
|
||||
(ev "(multiple-value-bind (a b) 42 (list a b))")
|
||||
(list 42 nil))
|
||||
(cl-test "nth-value: 0"
|
||||
(ev "(nth-value 0 (values 10 20 30))")
|
||||
10)
|
||||
(cl-test "nth-value: 1"
|
||||
(ev "(nth-value 1 (values 10 20 30))")
|
||||
20)
|
||||
(cl-test "nth-value: out of range"
|
||||
(ev "(nth-value 5 (values 10 20))")
|
||||
nil)
|
||||
(cl-test "multiple-value-call: basic"
|
||||
(ev "(multiple-value-call #'+ (values 1 2) (values 3 4))")
|
||||
10)
|
||||
(cl-test "multiple-value-prog1: returns first"
|
||||
(ev "(multiple-value-prog1 1 2 3)")
|
||||
1)
|
||||
(cl-test "multiple-value-prog1: side effects run"
|
||||
(ev "(let ((x 0)) (multiple-value-prog1 99 (setq x 7)) x)")
|
||||
7)
|
||||
(cl-test "values: nil primary in if"
|
||||
(ev "(if (values nil t) 'yes 'no)")
|
||||
"NO")
|
||||
(cl-test "values: truthy primary in if"
|
||||
(ev "(if (values 42 nil) 'yes 'no)")
|
||||
"YES")
|
||||
|
||||
;; --- Dynamic variables ---
|
||||
(cl-test "defvar marks special"
|
||||
(do (ev "(defvar *dv* 10)")
|
||||
(cl-special? "*DV*"))
|
||||
true)
|
||||
(cl-test "defvar: let rebinds dynamically"
|
||||
(ev "(progn (defvar *x* 1) (defun get-x () *x*) (let ((*x* 99)) (get-x)))")
|
||||
99)
|
||||
(cl-test "defvar: binding restores after let"
|
||||
(ev "(progn (defvar *yrst* 5) (let ((*yrst* 42)) *yrst*) *yrst*)")
|
||||
5)
|
||||
(cl-test "defparameter marks special"
|
||||
(do (ev "(defparameter *dp* 0)")
|
||||
(cl-special? "*DP*"))
|
||||
true)
|
||||
(cl-test "defparameter: let rebinds dynamically"
|
||||
(ev "(progn (defparameter *z* 10) (defun get-z () *z*) (let ((*z* 77)) (get-z)))")
|
||||
77)
|
||||
(cl-test "defparameter: always assigns"
|
||||
(ev "(progn (defparameter *p* 1) (defparameter *p* 2) *p*)")
|
||||
2)
|
||||
(cl-test "dynamic binding: nested lets"
|
||||
(ev "(progn (defvar *n* 0) (let ((*n* 1)) (let ((*n* 2)) *n*)))")
|
||||
2)
|
||||
(cl-test "dynamic binding: restores across nesting"
|
||||
(ev "(progn (defvar *m* 10) (let ((*m* 20)) (let ((*m* 30)) nil)) *m*)")
|
||||
10)
|
||||
204
lib/common-lisp/tests/lambda.sx
Normal file
204
lib/common-lisp/tests/lambda.sx
Normal file
@@ -0,0 +1,204 @@
|
||||
;; Lambda list parser tests
|
||||
|
||||
(define cl-test-pass 0)
|
||||
(define cl-test-fail 0)
|
||||
(define cl-test-fails (list))
|
||||
|
||||
;; Deep structural equality for dicts and lists
|
||||
(define
|
||||
cl-deep=
|
||||
(fn
|
||||
(a b)
|
||||
(cond
|
||||
((= a b) true)
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((ak (keys a)) (bk (keys b)))
|
||||
(if
|
||||
(not (= (len ak) (len bk)))
|
||||
false
|
||||
(every?
|
||||
(fn (k) (and (has-key? b k) (cl-deep= (get a k) (get b k))))
|
||||
ak))))
|
||||
((and (list? a) (list? b))
|
||||
(if
|
||||
(not (= (len a) (len b)))
|
||||
false
|
||||
(let
|
||||
((i 0) (ok true))
|
||||
(define
|
||||
chk
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(and ok (< i (len a)))
|
||||
(do
|
||||
(when
|
||||
(not (cl-deep= (nth a i) (nth b i)))
|
||||
(set! ok false))
|
||||
(set! i (+ i 1))
|
||||
(chk)))))
|
||||
(chk)
|
||||
ok)))
|
||||
(:else false))))
|
||||
|
||||
(define
|
||||
cl-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(cl-deep= actual expected)
|
||||
(set! cl-test-pass (+ cl-test-pass 1))
|
||||
(do
|
||||
(set! cl-test-fail (+ cl-test-fail 1))
|
||||
(append! cl-test-fails {:name name :expected expected :actual actual})))))
|
||||
|
||||
;; Helper: parse lambda list from string "(x y ...)"
|
||||
(define ll (fn (src) (cl-parse-lambda-list-str src)))
|
||||
(define ll-req (fn (src) (get (ll src) "required")))
|
||||
(define ll-opt (fn (src) (get (ll src) "optional")))
|
||||
(define ll-rest (fn (src) (get (ll src) "rest")))
|
||||
(define ll-key (fn (src) (get (ll src) "key")))
|
||||
(define ll-aok (fn (src) (get (ll src) "allow-other-keys")))
|
||||
(define ll-aux (fn (src) (get (ll src) "aux")))
|
||||
|
||||
;; ── required parameters ───────────────────────────────────────────
|
||||
|
||||
(cl-test "required: empty" (ll-req "()") (list))
|
||||
(cl-test "required: one" (ll-req "(x)") (list "X"))
|
||||
(cl-test "required: two" (ll-req "(x y)") (list "X" "Y"))
|
||||
(cl-test "required: three" (ll-req "(a b c)") (list "A" "B" "C"))
|
||||
(cl-test "required: upcased" (ll-req "(foo bar)") (list "FOO" "BAR"))
|
||||
|
||||
;; ── &optional ─────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "optional: none" (ll-opt "(x)") (list))
|
||||
|
||||
(cl-test
|
||||
"optional: bare symbol"
|
||||
(ll-opt "(x &optional z)")
|
||||
(list {:name "Z" :default nil :supplied nil}))
|
||||
|
||||
(cl-test
|
||||
"optional: with default"
|
||||
(ll-opt "(x &optional (z 0))")
|
||||
(list {:name "Z" :default 0 :supplied nil}))
|
||||
|
||||
(cl-test
|
||||
"optional: with supplied-p"
|
||||
(ll-opt "(x &optional (z 0 z-p))")
|
||||
(list {:name "Z" :default 0 :supplied "Z-P"}))
|
||||
|
||||
(cl-test
|
||||
"optional: two params"
|
||||
(ll-opt "(&optional a (b 1))")
|
||||
(list {:name "A" :default nil :supplied nil} {:name "B" :default 1 :supplied nil}))
|
||||
|
||||
(cl-test
|
||||
"optional: string default"
|
||||
(ll-opt "(&optional (name \"world\"))")
|
||||
(list {:name "NAME" :default {:cl-type "string" :value "world"} :supplied nil}))
|
||||
|
||||
;; ── &rest ─────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "rest: none" (ll-rest "(x)") nil)
|
||||
(cl-test "rest: present" (ll-rest "(x &rest args)") "ARGS")
|
||||
(cl-test "rest: with required" (ll-rest "(a b &rest tail)") "TAIL")
|
||||
|
||||
;; &body is an alias for &rest
|
||||
(cl-test "body: alias for rest" (ll-rest "(&body forms)") "FORMS")
|
||||
|
||||
;; rest doesn't consume required params
|
||||
(cl-test "rest: required still there" (ll-req "(a b &rest rest)") (list "A" "B"))
|
||||
|
||||
;; ── &key ──────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "key: none" (ll-key "(x)") (list))
|
||||
|
||||
(cl-test
|
||||
"key: bare symbol"
|
||||
(ll-key "(&key x)")
|
||||
(list {:name "X" :keyword "X" :default nil :supplied nil}))
|
||||
|
||||
(cl-test
|
||||
"key: with default"
|
||||
(ll-key "(&key (x 42))")
|
||||
(list {:name "X" :keyword "X" :default 42 :supplied nil}))
|
||||
|
||||
(cl-test
|
||||
"key: with supplied-p"
|
||||
(ll-key "(&key (x 42 x-p))")
|
||||
(list {:name "X" :keyword "X" :default 42 :supplied "X-P"}))
|
||||
|
||||
(cl-test
|
||||
"key: two params"
|
||||
(ll-key "(&key a b)")
|
||||
(list
|
||||
{:name "A" :keyword "A" :default nil :supplied nil}
|
||||
{:name "B" :keyword "B" :default nil :supplied nil}))
|
||||
|
||||
;; ── &allow-other-keys ─────────────────────────────────────────────
|
||||
|
||||
(cl-test "aok: absent" (ll-aok "(x)") false)
|
||||
(cl-test "aok: present" (ll-aok "(&key x &allow-other-keys)") true)
|
||||
|
||||
;; ── &aux ──────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "aux: none" (ll-aux "(x)") (list))
|
||||
|
||||
(cl-test
|
||||
"aux: bare symbol"
|
||||
(ll-aux "(&aux temp)")
|
||||
(list {:name "TEMP" :init nil}))
|
||||
|
||||
(cl-test
|
||||
"aux: with init"
|
||||
(ll-aux "(&aux (count 0))")
|
||||
(list {:name "COUNT" :init 0}))
|
||||
|
||||
(cl-test
|
||||
"aux: two vars"
|
||||
(ll-aux "(&aux a (b 1))")
|
||||
(list {:name "A" :init nil} {:name "B" :init 1}))
|
||||
|
||||
;; ── combined ──────────────────────────────────────────────────────
|
||||
|
||||
(cl-test
|
||||
"combined: full lambda list"
|
||||
(let
|
||||
((parsed (ll "(x y &optional (z 0 z-p) &rest args &key a (b nil b-p) &aux temp)")))
|
||||
(list
|
||||
(get parsed "required")
|
||||
(get (nth (get parsed "optional") 0) "name")
|
||||
(get (nth (get parsed "optional") 0) "default")
|
||||
(get (nth (get parsed "optional") 0) "supplied")
|
||||
(get parsed "rest")
|
||||
(get (nth (get parsed "key") 0) "name")
|
||||
(get (nth (get parsed "key") 1) "supplied")
|
||||
(get (nth (get parsed "aux") 0) "name")))
|
||||
(list
|
||||
(list "X" "Y")
|
||||
"Z"
|
||||
0
|
||||
"Z-P"
|
||||
"ARGS"
|
||||
"A"
|
||||
"B-P"
|
||||
"TEMP"))
|
||||
|
||||
(cl-test
|
||||
"combined: required only stops before &"
|
||||
(ll-req "(a b &optional c)")
|
||||
(list "A" "B"))
|
||||
|
||||
(cl-test
|
||||
"combined: required only with &key"
|
||||
(ll-req "(x &key y)")
|
||||
(list "X"))
|
||||
|
||||
(cl-test
|
||||
"combined: &rest and &key together"
|
||||
(let
|
||||
((parsed (ll "(&rest args &key verbose)")))
|
||||
(list (get parsed "rest") (get (nth (get parsed "key") 0) "name")))
|
||||
(list "ARGS" "VERBOSE"))
|
||||
204
lib/common-lisp/tests/macros.sx
Normal file
204
lib/common-lisp/tests/macros.sx
Normal file
@@ -0,0 +1,204 @@
|
||||
;; lib/common-lisp/tests/macros.sx — Phase 5: defmacro, gensym, LOOP tests
|
||||
;;
|
||||
;; Depends on: runtime.sx, eval.sx, loop.sx already loaded.
|
||||
;; Tests via (ev "...") using the CL evaluator.
|
||||
|
||||
(define ev (fn (src) (cl-eval-str src (cl-make-env))))
|
||||
(define evall (fn (src) (cl-eval-all-str src (cl-make-env))))
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
check
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
;; ── defmacro basics ──────────────────────────────────────────────────────────
|
||||
|
||||
(check
|
||||
"defmacro returns name"
|
||||
(ev "(defmacro my-or (a b) (list 'if a a b))")
|
||||
"MY-OR")
|
||||
|
||||
(check
|
||||
"defmacro expansion works"
|
||||
(ev "(progn (defmacro my-inc (x) (list '+ x 1)) (my-inc 5))")
|
||||
6)
|
||||
|
||||
(check
|
||||
"defmacro with &rest"
|
||||
(ev "(progn (defmacro my-list (&rest xs) (cons 'list xs)) (my-list 1 2 3))")
|
||||
(list 1 2 3))
|
||||
|
||||
(check
|
||||
"nested macro expansion"
|
||||
(ev "(progn (defmacro sq (x) (list '* x x)) (sq 7))")
|
||||
49)
|
||||
|
||||
(check
|
||||
"macro in conditional"
|
||||
(ev
|
||||
"(progn (defmacro my-when (c &rest body) (list 'if c (cons 'progn body) nil)) (my-when t 10 20))")
|
||||
20)
|
||||
|
||||
(check
|
||||
"macro returns nil branch"
|
||||
(ev
|
||||
"(progn (defmacro my-when (c &rest body) (list 'if c (cons 'progn body) nil)) (my-when nil 42))")
|
||||
nil)
|
||||
|
||||
;; ── macroexpand ───────────────────────────────────────────────────────────────
|
||||
|
||||
(check
|
||||
"macroexpand returns expanded form"
|
||||
(ev "(progn (defmacro double (x) (list '+ x x)) (macroexpand '(double 5)))")
|
||||
(list "+" 5 5))
|
||||
|
||||
;; ── gensym ────────────────────────────────────────────────────────────────────
|
||||
|
||||
(check "gensym returns string" (ev "(stringp (gensym))") true)
|
||||
|
||||
(check
|
||||
"gensym prefix"
|
||||
(ev "(let ((g (gensym \"MY\"))) (not (= g nil)))")
|
||||
true)
|
||||
|
||||
(check "gensyms are unique" (ev "(not (= (gensym) (gensym)))") true)
|
||||
|
||||
;; ── swap! macro with gensym ───────────────────────────────────────────────────
|
||||
|
||||
(check
|
||||
"swap! macro"
|
||||
(evall
|
||||
"(defmacro swap! (a b) (let ((tmp (gensym))) (list 'let (list (list tmp a)) (list 'setq a b) (list 'setq b tmp)))) (defvar *a* 10) (defvar *b* 20) (swap! *a* *b*) (list *a* *b*)")
|
||||
(list 20 10))
|
||||
|
||||
;; ── LOOP: basic repeat and collect ────────────────────────────────────────────
|
||||
|
||||
(check
|
||||
"loop repeat collect"
|
||||
(ev "(loop repeat 3 collect 99)")
|
||||
(list 99 99 99))
|
||||
|
||||
(check
|
||||
"loop for-in collect"
|
||||
(ev "(loop for x in '(1 2 3) collect (* x x))")
|
||||
(list 1 4 9))
|
||||
|
||||
(check
|
||||
"loop for-from-to collect"
|
||||
(ev "(loop for i from 1 to 5 collect i)")
|
||||
(list 1 2 3 4 5))
|
||||
|
||||
(check
|
||||
"loop for-from-below collect"
|
||||
(ev "(loop for i from 0 below 4 collect i)")
|
||||
(list 0 1 2 3))
|
||||
|
||||
(check
|
||||
"loop for-downto collect"
|
||||
(ev "(loop for i from 5 downto 1 collect i)")
|
||||
(list 5 4 3 2 1))
|
||||
|
||||
(check
|
||||
"loop for-by collect"
|
||||
(ev "(loop for i from 0 to 10 by 2 collect i)")
|
||||
(list 0 2 4 6 8 10))
|
||||
|
||||
;; ── LOOP: sum, count, maximize, minimize ─────────────────────────────────────
|
||||
|
||||
(check "loop sum" (ev "(loop for i from 1 to 5 sum i)") 15)
|
||||
|
||||
(check
|
||||
"loop count"
|
||||
(ev "(loop for x in '(1 2 3 4 5) count (> x 3))")
|
||||
2)
|
||||
|
||||
(check
|
||||
"loop maximize"
|
||||
(ev "(loop for x in '(3 1 4 1 5 9 2 6) maximize x)")
|
||||
9)
|
||||
|
||||
(check
|
||||
"loop minimize"
|
||||
(ev "(loop for x in '(3 1 4 1 5 9 2 6) minimize x)")
|
||||
1)
|
||||
|
||||
;; ── LOOP: while and until ─────────────────────────────────────────────────────
|
||||
|
||||
(check
|
||||
"loop while"
|
||||
(ev "(loop for i from 1 to 10 while (< i 5) collect i)")
|
||||
(list 1 2 3 4))
|
||||
|
||||
(check
|
||||
"loop until"
|
||||
(ev "(loop for i from 1 to 10 until (= i 5) collect i)")
|
||||
(list 1 2 3 4))
|
||||
|
||||
;; ── LOOP: when / unless ───────────────────────────────────────────────────────
|
||||
|
||||
(check
|
||||
"loop when filter"
|
||||
(ev "(loop for i from 0 below 8 when (evenp i) collect i)")
|
||||
(list 0 2 4 6))
|
||||
|
||||
(check
|
||||
"loop unless filter"
|
||||
(ev "(loop for i from 0 below 8 unless (evenp i) collect i)")
|
||||
(list 1 3 5 7))
|
||||
|
||||
;; ── LOOP: append ─────────────────────────────────────────────────────────────
|
||||
|
||||
(check
|
||||
"loop append"
|
||||
(ev "(loop for x in '((1 2) (3 4) (5 6)) append x)")
|
||||
(list 1 2 3 4 5 6))
|
||||
|
||||
;; ── LOOP: always, never, thereis ─────────────────────────────────────────────
|
||||
|
||||
(check
|
||||
"loop always true"
|
||||
(ev "(loop for x in '(2 4 6) always (evenp x))")
|
||||
true)
|
||||
|
||||
(check
|
||||
"loop always false"
|
||||
(ev "(loop for x in '(2 3 6) always (evenp x))")
|
||||
false)
|
||||
|
||||
(check "loop never" (ev "(loop for x in '(1 3 5) never (evenp x))") true)
|
||||
|
||||
(check "loop thereis" (ev "(loop for x in '(1 2 3) thereis (> x 2))") true)
|
||||
|
||||
;; ── LOOP: for = then (general iteration) ─────────────────────────────────────
|
||||
|
||||
(check
|
||||
"loop for = then doubling"
|
||||
(ev "(loop repeat 5 for x = 1 then (* x 2) collect x)")
|
||||
(list 1 2 4 8 16))
|
||||
|
||||
;; ── summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define macro-passed passed)
|
||||
(define macro-failed failed)
|
||||
(define macro-failures failures)
|
||||
160
lib/common-lisp/tests/parse.sx
Normal file
160
lib/common-lisp/tests/parse.sx
Normal file
@@ -0,0 +1,160 @@
|
||||
;; Common Lisp reader/parser tests
|
||||
|
||||
(define cl-test-pass 0)
|
||||
(define cl-test-fail 0)
|
||||
(define cl-test-fails (list))
|
||||
|
||||
(define
|
||||
cl-deep=
|
||||
(fn
|
||||
(a b)
|
||||
(cond
|
||||
((= a b) true)
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((ak (keys a)) (bk (keys b)))
|
||||
(if
|
||||
(not (= (len ak) (len bk)))
|
||||
false
|
||||
(every?
|
||||
(fn (k) (and (has-key? b k) (cl-deep= (get a k) (get b k))))
|
||||
ak))))
|
||||
((and (list? a) (list? b))
|
||||
(if
|
||||
(not (= (len a) (len b)))
|
||||
false
|
||||
(let
|
||||
((i 0) (ok true))
|
||||
(define
|
||||
chk
|
||||
(fn
|
||||
()
|
||||
(when
|
||||
(and ok (< i (len a)))
|
||||
(do
|
||||
(when
|
||||
(not (cl-deep= (nth a i) (nth b i)))
|
||||
(set! ok false))
|
||||
(set! i (+ i 1))
|
||||
(chk)))))
|
||||
(chk)
|
||||
ok)))
|
||||
(:else false))))
|
||||
|
||||
(define
|
||||
cl-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(cl-deep= actual expected)
|
||||
(set! cl-test-pass (+ cl-test-pass 1))
|
||||
(do
|
||||
(set! cl-test-fail (+ cl-test-fail 1))
|
||||
(append! cl-test-fails {:name name :expected expected :actual actual})))))
|
||||
|
||||
;; ── atoms ─────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "integer: 42" (cl-read "42") 42)
|
||||
(cl-test "integer: 0" (cl-read "0") 0)
|
||||
(cl-test "integer: negative" (cl-read "-5") -5)
|
||||
(cl-test "integer: positive sign" (cl-read "+3") 3)
|
||||
(cl-test "integer: hex #xFF" (cl-read "#xFF") 255)
|
||||
(cl-test "integer: hex #xAB" (cl-read "#xAB") 171)
|
||||
(cl-test "integer: binary #b1010" (cl-read "#b1010") 10)
|
||||
(cl-test "integer: octal #o17" (cl-read "#o17") 15)
|
||||
|
||||
(cl-test "float: type" (get (cl-read "3.14") "cl-type") "float")
|
||||
(cl-test "float: value" (get (cl-read "3.14") "value") "3.14")
|
||||
(cl-test "float: neg" (get (cl-read "-2.5") "value") "-2.5")
|
||||
(cl-test "float: exp" (get (cl-read "1.0e10") "value") "1.0e10")
|
||||
|
||||
(cl-test "ratio: type" (get (cl-read "1/3") "cl-type") "ratio")
|
||||
(cl-test "ratio: value" (get (cl-read "1/3") "value") "1/3")
|
||||
(cl-test "ratio: 22/7" (get (cl-read "22/7") "value") "22/7")
|
||||
|
||||
(cl-test "string: basic" (cl-read "\"hello\"") {:cl-type "string" :value "hello"})
|
||||
(cl-test "string: empty" (cl-read "\"\"") {:cl-type "string" :value ""})
|
||||
(cl-test "string: with escape" (cl-read "\"a\\nb\"") {:cl-type "string" :value "a\nb"})
|
||||
|
||||
(cl-test "symbol: foo" (cl-read "foo") "FOO")
|
||||
(cl-test "symbol: BAR" (cl-read "BAR") "BAR")
|
||||
(cl-test "symbol: pkg:sym" (cl-read "cl:car") "CL:CAR")
|
||||
(cl-test "symbol: pkg::sym" (cl-read "pkg::foo") "PKG::FOO")
|
||||
|
||||
(cl-test "nil: symbol" (cl-read "nil") nil)
|
||||
(cl-test "nil: uppercase" (cl-read "NIL") nil)
|
||||
(cl-test "t: symbol" (cl-read "t") true)
|
||||
(cl-test "t: uppercase" (cl-read "T") true)
|
||||
|
||||
(cl-test "keyword: type" (get (cl-read ":foo") "cl-type") "keyword")
|
||||
(cl-test "keyword: name" (get (cl-read ":foo") "name") "FOO")
|
||||
(cl-test "keyword: :test" (get (cl-read ":test") "name") "TEST")
|
||||
|
||||
(cl-test "char: type" (get (cl-read "#\\a") "cl-type") "char")
|
||||
(cl-test "char: value" (get (cl-read "#\\a") "value") "a")
|
||||
(cl-test "char: Space" (get (cl-read "#\\Space") "value") " ")
|
||||
(cl-test "char: Newline" (get (cl-read "#\\Newline") "value") "\n")
|
||||
|
||||
(cl-test "uninterned: type" (get (cl-read "#:foo") "cl-type") "uninterned")
|
||||
(cl-test "uninterned: name" (get (cl-read "#:foo") "name") "FOO")
|
||||
|
||||
;; ── lists ─────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "list: empty" (cl-read "()") (list))
|
||||
(cl-test "list: one element" (cl-read "(foo)") (list "FOO"))
|
||||
(cl-test "list: two elements" (cl-read "(foo bar)") (list "FOO" "BAR"))
|
||||
(cl-test "list: nested" (cl-read "((a b) c)") (list (list "A" "B") "C"))
|
||||
(cl-test "list: with integer" (cl-read "(+ 1 2)") (list "+" 1 2))
|
||||
(cl-test "list: with string" (cl-read "(print \"hi\")") (list "PRINT" {:cl-type "string" :value "hi"}))
|
||||
(cl-test "list: nil element" (cl-read "(a nil b)") (list "A" nil "B"))
|
||||
(cl-test "list: t element" (cl-read "(a t b)") (list "A" true "B"))
|
||||
|
||||
;; ── dotted pairs ──────────────────────────────────────────────<E29480><E29480>──
|
||||
|
||||
(cl-test "dotted: type" (get (cl-read "(a . b)") "cl-type") "cons")
|
||||
(cl-test "dotted: car" (get (cl-read "(a . b)") "car") "A")
|
||||
(cl-test "dotted: cdr" (get (cl-read "(a . b)") "cdr") "B")
|
||||
(cl-test "dotted: number cdr" (get (cl-read "(x . 42)") "cdr") 42)
|
||||
|
||||
;; ── reader macros ────────────────────────────────────────────────<E29480><E29480>
|
||||
|
||||
(cl-test "quote: form" (cl-read "'x") (list "QUOTE" "X"))
|
||||
(cl-test "quote: list" (cl-read "'(a b)") (list "QUOTE" (list "A" "B")))
|
||||
(cl-test "backquote: form" (cl-read "`x") (list "QUASIQUOTE" "X"))
|
||||
(cl-test "unquote: form" (cl-read ",x") (list "UNQUOTE" "X"))
|
||||
(cl-test "comma-at: form" (cl-read ",@x") (list "UNQUOTE-SPLICING" "X"))
|
||||
(cl-test "function: form" (cl-read "#'foo") (list "FUNCTION" "FOO"))
|
||||
|
||||
;; ── vector ────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "vector: type" (get (cl-read "#(1 2 3)") "cl-type") "vector")
|
||||
(cl-test "vector: elements" (get (cl-read "#(1 2 3)") "elements") (list 1 2 3))
|
||||
(cl-test "vector: empty" (get (cl-read "#()") "elements") (list))
|
||||
(cl-test "vector: mixed" (get (cl-read "#(a 1 \"s\")") "elements") (list "A" 1 {:cl-type "string" :value "s"}))
|
||||
|
||||
;; ── cl-read-all ───────────────────────────────────────────────────
|
||||
|
||||
(cl-test
|
||||
"read-all: empty"
|
||||
(cl-read-all "")
|
||||
(list))
|
||||
|
||||
(cl-test
|
||||
"read-all: two forms"
|
||||
(cl-read-all "42 foo")
|
||||
(list 42 "FOO"))
|
||||
|
||||
(cl-test
|
||||
"read-all: three forms"
|
||||
(cl-read-all "(+ 1 2) (+ 3 4) hello")
|
||||
(list (list "+" 1 2) (list "+" 3 4) "HELLO"))
|
||||
|
||||
(cl-test
|
||||
"read-all: with comments"
|
||||
(cl-read-all "; this is a comment\n42 ; inline\nfoo")
|
||||
(list 42 "FOO"))
|
||||
|
||||
(cl-test
|
||||
"read-all: defun form"
|
||||
(nth (cl-read-all "(defun square (x) (* x x))") 0)
|
||||
(list "DEFUN" "SQUARE" (list "X") (list "*" "X" "X")))
|
||||
291
lib/common-lisp/tests/programs/geometry.sx
Normal file
291
lib/common-lisp/tests/programs/geometry.sx
Normal file
@@ -0,0 +1,291 @@
|
||||
;; geometry.sx — Multiple dispatch with CLOS
|
||||
;;
|
||||
;; Demonstrates generic functions dispatching on combinations of
|
||||
;; geometric types: point, line, plane.
|
||||
;;
|
||||
;; Depends on: lib/common-lisp/runtime.sx, lib/common-lisp/clos.sx
|
||||
|
||||
;; ── geometric classes ──────────────────────────────────────────────────────
|
||||
|
||||
(clos-defclass "geo-point" (list "t") (list {:initform 0 :initarg ":px" :reader nil :writer nil :accessor nil :name "px"} {:initform 0 :initarg ":py" :reader nil :writer nil :accessor nil :name "py"}))
|
||||
|
||||
(clos-defclass "geo-line" (list "t") (list {:initform nil :initarg ":p1" :reader nil :writer nil :accessor nil :name "p1"} {:initform nil :initarg ":p2" :reader nil :writer nil :accessor nil :name "p2"}))
|
||||
|
||||
(clos-defclass "geo-plane" (list "t") (list {:initform nil :initarg ":normal" :reader nil :writer nil :accessor nil :name "normal"} {:initform 0 :initarg ":d" :reader nil :writer nil :accessor nil :name "d"}))
|
||||
|
||||
;; ── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define geo-point-x (fn (p) (clos-slot-value p "px")))
|
||||
(define geo-point-y (fn (p) (clos-slot-value p "py")))
|
||||
|
||||
(define
|
||||
geo-make-point
|
||||
(fn (x y) (clos-make-instance "geo-point" ":px" x ":py" y)))
|
||||
|
||||
(define
|
||||
geo-make-line
|
||||
(fn (p1 p2) (clos-make-instance "geo-line" ":p1" p1 ":p2" p2)))
|
||||
|
||||
(define
|
||||
geo-make-plane
|
||||
(fn
|
||||
(nx ny d)
|
||||
(clos-make-instance "geo-plane" ":normal" (list nx ny) ":d" d)))
|
||||
|
||||
;; ── describe generic ───────────────────────────────────────────────────────
|
||||
|
||||
(clos-defgeneric "geo-describe" {})
|
||||
|
||||
(clos-defmethod
|
||||
"geo-describe"
|
||||
(list)
|
||||
(list "geo-point")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((p (first args)))
|
||||
(str "P(" (geo-point-x p) "," (geo-point-y p) ")"))))
|
||||
|
||||
(clos-defmethod
|
||||
"geo-describe"
|
||||
(list)
|
||||
(list "geo-line")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((l (first args)))
|
||||
(str
|
||||
"L["
|
||||
(clos-call-generic "geo-describe" (list (clos-slot-value l "p1")))
|
||||
"-"
|
||||
(clos-call-generic "geo-describe" (list (clos-slot-value l "p2")))
|
||||
"]"))))
|
||||
|
||||
(clos-defmethod
|
||||
"geo-describe"
|
||||
(list)
|
||||
(list "geo-plane")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((pl (first args)))
|
||||
(str "Plane(d=" (clos-slot-value pl "d") ")"))))
|
||||
|
||||
;; ── intersect: multi-dispatch generic ─────────────────────────────────────
|
||||
;;
|
||||
;; Returns a string description of the intersection result.
|
||||
|
||||
(clos-defgeneric "intersect" {})
|
||||
|
||||
;; point ∩ point: same if coordinates match
|
||||
(clos-defmethod
|
||||
"intersect"
|
||||
(list)
|
||||
(list "geo-point" "geo-point")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((p1 (first args)) (p2 (first (rest args))))
|
||||
(if
|
||||
(and
|
||||
(= (geo-point-x p1) (geo-point-x p2))
|
||||
(= (geo-point-y p1) (geo-point-y p2)))
|
||||
"point"
|
||||
"empty"))))
|
||||
|
||||
;; point ∩ line: check if point lies on line (cross product = 0)
|
||||
(clos-defmethod
|
||||
"intersect"
|
||||
(list)
|
||||
(list "geo-point" "geo-line")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((pt (first args)) (ln (first (rest args))))
|
||||
(let
|
||||
((lp1 (clos-slot-value ln "p1")) (lp2 (clos-slot-value ln "p2")))
|
||||
(let
|
||||
((dx (- (geo-point-x lp2) (geo-point-x lp1)))
|
||||
(dy (- (geo-point-y lp2) (geo-point-y lp1)))
|
||||
(ex (- (geo-point-x pt) (geo-point-x lp1)))
|
||||
(ey (- (geo-point-y pt) (geo-point-y lp1))))
|
||||
(if (= (- (* dx ey) (* dy ex)) 0) "point" "empty"))))))
|
||||
|
||||
;; line ∩ line: parallel (same slope = empty) or point
|
||||
(clos-defmethod
|
||||
"intersect"
|
||||
(list)
|
||||
(list "geo-line" "geo-line")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((l1 (first args)) (l2 (first (rest args))))
|
||||
(let
|
||||
((p1 (clos-slot-value l1 "p1"))
|
||||
(p2 (clos-slot-value l1 "p2"))
|
||||
(p3 (clos-slot-value l2 "p1"))
|
||||
(p4 (clos-slot-value l2 "p2")))
|
||||
(let
|
||||
((dx1 (- (geo-point-x p2) (geo-point-x p1)))
|
||||
(dy1 (- (geo-point-y p2) (geo-point-y p1)))
|
||||
(dx2 (- (geo-point-x p4) (geo-point-x p3)))
|
||||
(dy2 (- (geo-point-y p4) (geo-point-y p3))))
|
||||
(let
|
||||
((cross (- (* dx1 dy2) (* dy1 dx2))))
|
||||
(if (= cross 0) "parallel" "point")))))))
|
||||
|
||||
;; line ∩ plane: general case = point (or parallel if line ⊥ normal)
|
||||
(clos-defmethod
|
||||
"intersect"
|
||||
(list)
|
||||
(list "geo-line" "geo-plane")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((ln (first args)) (pl (first (rest args))))
|
||||
(let
|
||||
((p1 (clos-slot-value ln "p1"))
|
||||
(p2 (clos-slot-value ln "p2"))
|
||||
(n (clos-slot-value pl "normal")))
|
||||
(let
|
||||
((dx (- (geo-point-x p2) (geo-point-x p1)))
|
||||
(dy (- (geo-point-y p2) (geo-point-y p1)))
|
||||
(nx (first n))
|
||||
(ny (first (rest n))))
|
||||
(let
|
||||
((dot (+ (* dx nx) (* dy ny))))
|
||||
(if (= dot 0) "parallel" "point")))))))
|
||||
|
||||
;; ── tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
check
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
;; describe
|
||||
(check
|
||||
"describe point"
|
||||
(clos-call-generic
|
||||
"geo-describe"
|
||||
(list (geo-make-point 3 4)))
|
||||
"P(3,4)")
|
||||
(check
|
||||
"describe line"
|
||||
(clos-call-generic
|
||||
"geo-describe"
|
||||
(list
|
||||
(geo-make-line
|
||||
(geo-make-point 0 0)
|
||||
(geo-make-point 1 1))))
|
||||
"L[P(0,0)-P(1,1)]")
|
||||
(check
|
||||
"describe plane"
|
||||
(clos-call-generic
|
||||
"geo-describe"
|
||||
(list (geo-make-plane 0 1 5)))
|
||||
"Plane(d=5)")
|
||||
|
||||
;; intersect point×point
|
||||
(check
|
||||
"P∩P same"
|
||||
(clos-call-generic
|
||||
"intersect"
|
||||
(list
|
||||
(geo-make-point 2 3)
|
||||
(geo-make-point 2 3)))
|
||||
"point")
|
||||
(check
|
||||
"P∩P diff"
|
||||
(clos-call-generic
|
||||
"intersect"
|
||||
(list
|
||||
(geo-make-point 1 2)
|
||||
(geo-make-point 3 4)))
|
||||
"empty")
|
||||
|
||||
;; intersect point×line
|
||||
(let
|
||||
((origin (geo-make-point 0 0))
|
||||
(p10 (geo-make-point 10 0))
|
||||
(p55 (geo-make-point 5 5))
|
||||
(l-x
|
||||
(geo-make-line
|
||||
(geo-make-point 0 0)
|
||||
(geo-make-point 10 0))))
|
||||
(begin
|
||||
(check
|
||||
"P∩L on line"
|
||||
(clos-call-generic "intersect" (list p10 l-x))
|
||||
"point")
|
||||
(check
|
||||
"P∩L on x-axis"
|
||||
(clos-call-generic "intersect" (list origin l-x))
|
||||
"point")
|
||||
(check
|
||||
"P∩L off line"
|
||||
(clos-call-generic "intersect" (list p55 l-x))
|
||||
"empty")))
|
||||
|
||||
;; intersect line×line
|
||||
(let
|
||||
((horiz (geo-make-line (geo-make-point 0 0) (geo-make-point 10 0)))
|
||||
(vert
|
||||
(geo-make-line
|
||||
(geo-make-point 5 -5)
|
||||
(geo-make-point 5 5)))
|
||||
(horiz2
|
||||
(geo-make-line
|
||||
(geo-make-point 0 3)
|
||||
(geo-make-point 10 3))))
|
||||
(begin
|
||||
(check
|
||||
"L∩L crossing"
|
||||
(clos-call-generic "intersect" (list horiz vert))
|
||||
"point")
|
||||
(check
|
||||
"L∩L parallel"
|
||||
(clos-call-generic "intersect" (list horiz horiz2))
|
||||
"parallel")))
|
||||
|
||||
;; intersect line×plane
|
||||
(let
|
||||
((diag (geo-make-line (geo-make-point 0 0) (geo-make-point 1 1)))
|
||||
(vert-plane (geo-make-plane 1 0 5))
|
||||
(diag-plane (geo-make-plane -1 1 0)))
|
||||
(begin
|
||||
(check
|
||||
"L∩Plane cross"
|
||||
(clos-call-generic "intersect" (list diag vert-plane))
|
||||
"point")
|
||||
(check
|
||||
"L∩Plane parallel"
|
||||
(clos-call-generic "intersect" (list diag diag-plane))
|
||||
"parallel")))
|
||||
|
||||
;; ── summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define geo-passed passed)
|
||||
(define geo-failed failed)
|
||||
(define geo-failures failures)
|
||||
196
lib/common-lisp/tests/programs/interactive-debugger.sx
Normal file
196
lib/common-lisp/tests/programs/interactive-debugger.sx
Normal file
@@ -0,0 +1,196 @@
|
||||
;; interactive-debugger.sx — Condition debugger using *debugger-hook*
|
||||
;;
|
||||
;; Demonstrates the classic CL debugger pattern:
|
||||
;; - *debugger-hook* is invoked when an unhandled error reaches the top level
|
||||
;; - The hook receives the condition and a reference to itself
|
||||
;; - It can offer restarts interactively (here simulated with a policy fn)
|
||||
;;
|
||||
;; In real CL the debugger reads from the terminal. Here we simulate
|
||||
;; the "user input" via a policy function passed in at call time.
|
||||
;;
|
||||
;; Depends on: lib/common-lisp/runtime.sx already loaded.
|
||||
|
||||
;; ── *debugger-hook* global ────────────────────────────────────────────────
|
||||
;;
|
||||
;; CL: when error is unhandled, invoke *debugger-hook* with (condition hook).
|
||||
;; A nil hook means use the system default (which we simulate as re-raise).
|
||||
|
||||
(define cl-debugger-hook nil)
|
||||
|
||||
;; ── invoke-debugger ────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Called when cl-error finds no handler. Tries cl-debugger-hook first;
|
||||
;; falls back to a simple error report.
|
||||
|
||||
(define
|
||||
cl-invoke-debugger
|
||||
(fn
|
||||
(c)
|
||||
(if
|
||||
(nil? cl-debugger-hook)
|
||||
(error (str "Debugger: " (cl-condition-message c)))
|
||||
(begin
|
||||
(let
|
||||
((hook cl-debugger-hook))
|
||||
(set! cl-debugger-hook nil)
|
||||
(let
|
||||
((result (hook c hook)))
|
||||
(set! cl-debugger-hook hook)
|
||||
result))))))
|
||||
|
||||
;; ── cl-error/debugger — error that routes through invoke-debugger ─────────
|
||||
|
||||
(define
|
||||
cl-error-with-debugger
|
||||
(fn
|
||||
(c &rest args)
|
||||
(let
|
||||
((obj (cond ((cl-condition? c) c) ((string? c) (cl-make-condition "simple-error" "format-control" c "format-arguments" args)) (:else (cl-make-condition "simple-error" "format-control" (str c))))))
|
||||
(cl-signal-obj obj cl-handler-stack)
|
||||
(cl-invoke-debugger obj))))
|
||||
|
||||
;; ── simulated debugger session ────────────────────────────────────────────
|
||||
;;
|
||||
;; A debugger hook takes (condition hook) and "reads" user commands.
|
||||
;; We simulate this with a policy function: (fn (c restarts) restart-name)
|
||||
;; that picks a restart given the condition and available restarts.
|
||||
|
||||
(define
|
||||
make-policy-debugger
|
||||
(fn
|
||||
(policy)
|
||||
(fn
|
||||
(c hook)
|
||||
(let
|
||||
((available (cl-compute-restarts)))
|
||||
(let
|
||||
((choice (policy c available)))
|
||||
(if
|
||||
(and choice (not (nil? (cl-find-restart choice))))
|
||||
(cl-invoke-restart choice)
|
||||
(error
|
||||
(str
|
||||
"Debugger: no restart chosen for: "
|
||||
(cl-condition-message c)))))))))
|
||||
|
||||
;; ── tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
check
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
(define
|
||||
reset-stacks!
|
||||
(fn
|
||||
()
|
||||
(set! cl-handler-stack (list))
|
||||
(set! cl-restart-stack (list))
|
||||
(set! cl-debugger-hook nil)))
|
||||
|
||||
;; Test 1: debugger hook receives condition
|
||||
(reset-stacks!)
|
||||
(let
|
||||
((received-msg ""))
|
||||
(begin
|
||||
(set!
|
||||
cl-debugger-hook
|
||||
(fn (c hook) (set! received-msg (cl-condition-message c)) nil))
|
||||
(cl-restart-case
|
||||
(fn () (cl-error-with-debugger "something broke"))
|
||||
(list "abort" (list) (fn () nil)))
|
||||
(check "debugger hook receives condition" received-msg "something broke")))
|
||||
|
||||
;; Test 2: policy-driven restart selection (use-zero)
|
||||
(reset-stacks!)
|
||||
(let
|
||||
((result (begin (set! cl-debugger-hook (make-policy-debugger (fn (c restarts) "use-zero"))) (cl-restart-case (fn () (cl-error-with-debugger (cl-make-condition "division-by-zero")) 999) (list "use-zero" (list) (fn () 0))))))
|
||||
(check "policy debugger: use-zero restart" result 0))
|
||||
|
||||
;; Test 3: policy selects abort
|
||||
(reset-stacks!)
|
||||
(let
|
||||
((result (begin (set! cl-debugger-hook (make-policy-debugger (fn (c restarts) "abort"))) (cl-restart-case (fn () (cl-error-with-debugger "aborting error") 999) (list "abort" (list) (fn () "aborted"))))))
|
||||
(check "policy debugger: abort restart" result "aborted"))
|
||||
|
||||
;; Test 4: compute-restarts inside debugger hook
|
||||
(reset-stacks!)
|
||||
(let
|
||||
((seen-restarts (list)))
|
||||
(begin
|
||||
(set!
|
||||
cl-debugger-hook
|
||||
(fn
|
||||
(c hook)
|
||||
(set! seen-restarts (cl-compute-restarts))
|
||||
(cl-invoke-restart "continue")))
|
||||
(cl-restart-case
|
||||
(fn () (cl-error-with-debugger "test") 42)
|
||||
(list "continue" (list) (fn () "ok"))
|
||||
(list "abort" (list) (fn () "no")))
|
||||
(check
|
||||
"debugger: compute-restarts visible"
|
||||
(= (len seen-restarts) 2)
|
||||
true)))
|
||||
|
||||
;; Test 5: hook not invoked when handler catches first
|
||||
(reset-stacks!)
|
||||
(let
|
||||
((hook-called false)
|
||||
(result
|
||||
(begin
|
||||
(set! cl-debugger-hook (fn (c hook) (set! hook-called true) nil))
|
||||
(cl-handler-case
|
||||
(fn () (cl-error-with-debugger "handled"))
|
||||
(list "error" (fn (c) "handler-won"))))))
|
||||
(check "handler wins; hook not called" hook-called false)
|
||||
(check "handler result returned" result "handler-won"))
|
||||
|
||||
;; Test 6: debugger-hook nil after re-raise guard
|
||||
(reset-stacks!)
|
||||
(let
|
||||
((hook-calls 0))
|
||||
(begin
|
||||
(set!
|
||||
cl-debugger-hook
|
||||
(fn
|
||||
(c hook)
|
||||
(set! hook-calls (+ hook-calls 1))
|
||||
(if
|
||||
(> hook-calls 1)
|
||||
(error "infinite loop guard")
|
||||
(cl-invoke-restart "escape"))))
|
||||
(cl-restart-case
|
||||
(fn () (cl-error-with-debugger "once"))
|
||||
(list "escape" (list) (fn () nil)))
|
||||
(check
|
||||
"hook called exactly once (no infinite recursion)"
|
||||
hook-calls
|
||||
1)))
|
||||
|
||||
;; ── summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define debugger-passed passed)
|
||||
(define debugger-failed failed)
|
||||
(define debugger-failures failures)
|
||||
228
lib/common-lisp/tests/programs/mop-trace.sx
Normal file
228
lib/common-lisp/tests/programs/mop-trace.sx
Normal file
@@ -0,0 +1,228 @@
|
||||
;; mop-trace.sx — :before/:after method tracing with CLOS
|
||||
;;
|
||||
;; Classic CLOS pattern: instrument generic functions with :before and :after
|
||||
;; qualifiers to print call/return traces without modifying the primary method.
|
||||
;;
|
||||
;; Depends on: lib/common-lisp/runtime.sx, lib/common-lisp/clos.sx
|
||||
|
||||
;; ── trace log (mutable accumulator) ───────────────────────────────────────
|
||||
|
||||
(define trace-log (list))
|
||||
|
||||
(define
|
||||
trace-push
|
||||
(fn (msg) (set! trace-log (append trace-log (list msg)))))
|
||||
|
||||
(define trace-clear (fn () (set! trace-log (list))))
|
||||
|
||||
;; ── domain classes ─────────────────────────────────────────────────────────
|
||||
|
||||
(clos-defclass "shape" (list "t") (list {:initform "white" :initarg ":color" :reader nil :writer nil :accessor nil :name "color"}))
|
||||
|
||||
(clos-defclass "circle" (list "shape") (list {:initform 1 :initarg ":radius" :reader nil :writer nil :accessor nil :name "radius"}))
|
||||
|
||||
(clos-defclass "rect" (list "shape") (list {:initform 1 :initarg ":width" :reader nil :writer nil :accessor nil :name "width"} {:initform 1 :initarg ":height" :reader nil :writer nil :accessor nil :name "height"}))
|
||||
|
||||
;; ── generic function: area ─────────────────────────────────────────────────
|
||||
|
||||
(clos-defgeneric "area" {})
|
||||
|
||||
;; primary methods
|
||||
(clos-defmethod
|
||||
"area"
|
||||
(list)
|
||||
(list "circle")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((c (first args)))
|
||||
(let ((r (clos-slot-value c "radius"))) (* r r)))))
|
||||
|
||||
(clos-defmethod
|
||||
"area"
|
||||
(list)
|
||||
(list "rect")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((r (first args)))
|
||||
(* (clos-slot-value r "width") (clos-slot-value r "height")))))
|
||||
|
||||
;; :before tracing
|
||||
(clos-defmethod
|
||||
"area"
|
||||
(list "before")
|
||||
(list "shape")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(trace-push (str "BEFORE area(" (clos-class-of (first args)) ")"))))
|
||||
|
||||
;; :after tracing
|
||||
(clos-defmethod
|
||||
"area"
|
||||
(list "after")
|
||||
(list "shape")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(trace-push (str "AFTER area(" (clos-class-of (first args)) ")"))))
|
||||
|
||||
;; ── generic function: describe-shape ──────────────────────────────────────
|
||||
|
||||
(clos-defgeneric "describe-shape" {})
|
||||
|
||||
(clos-defmethod
|
||||
"describe-shape"
|
||||
(list)
|
||||
(list "shape")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((s (first args)))
|
||||
(str "shape[" (clos-slot-value s "color") "]"))))
|
||||
|
||||
(clos-defmethod
|
||||
"describe-shape"
|
||||
(list)
|
||||
(list "circle")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((c (first args)))
|
||||
(str
|
||||
"circle[r="
|
||||
(clos-slot-value c "radius")
|
||||
" "
|
||||
(clos-call-next-method next-fn)
|
||||
"]"))))
|
||||
|
||||
(clos-defmethod
|
||||
"describe-shape"
|
||||
(list)
|
||||
(list "rect")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(let
|
||||
((r (first args)))
|
||||
(str
|
||||
"rect["
|
||||
(clos-slot-value r "width")
|
||||
"x"
|
||||
(clos-slot-value r "height")
|
||||
" "
|
||||
(clos-call-next-method next-fn)
|
||||
"]"))))
|
||||
|
||||
;; :before on base shape (fires for all subclasses too)
|
||||
(clos-defmethod
|
||||
"describe-shape"
|
||||
(list "before")
|
||||
(list "shape")
|
||||
(fn
|
||||
(args next-fn)
|
||||
(trace-push
|
||||
(str "BEFORE describe-shape(" (clos-class-of (first args)) ")"))))
|
||||
|
||||
;; ── tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
check
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
;; ── area tests ────────────────────────────────────────────────────────────
|
||||
|
||||
;; circle area = r*r (no pi — integer arithmetic for predictability)
|
||||
(let
|
||||
((c (clos-make-instance "circle" ":radius" 5 ":color" "red")))
|
||||
(do
|
||||
(trace-clear)
|
||||
(check "circle area" (clos-call-generic "area" (list c)) 25)
|
||||
(check
|
||||
":before fired for circle"
|
||||
(= (first trace-log) "BEFORE area(circle)")
|
||||
true)
|
||||
(check
|
||||
":after fired for circle"
|
||||
(= (first (rest trace-log)) "AFTER area(circle)")
|
||||
true)
|
||||
(check "trace length 2" (len trace-log) 2)))
|
||||
|
||||
;; rect area = w*h
|
||||
(let
|
||||
((r (clos-make-instance "rect" ":width" 4 ":height" 6 ":color" "blue")))
|
||||
(do
|
||||
(trace-clear)
|
||||
(check "rect area" (clos-call-generic "area" (list r)) 24)
|
||||
(check
|
||||
":before fired for rect"
|
||||
(= (first trace-log) "BEFORE area(rect)")
|
||||
true)
|
||||
(check
|
||||
":after fired for rect"
|
||||
(= (first (rest trace-log)) "AFTER area(rect)")
|
||||
true)
|
||||
(check "trace length 2 (rect)" (len trace-log) 2)))
|
||||
|
||||
;; ── describe-shape tests ───────────────────────────────────────────────────
|
||||
|
||||
(let
|
||||
((c (clos-make-instance "circle" ":radius" 3 ":color" "green")))
|
||||
(do
|
||||
(trace-clear)
|
||||
(check
|
||||
"circle describe"
|
||||
(clos-call-generic "describe-shape" (list c))
|
||||
"circle[r=3 shape[green]]")
|
||||
(check
|
||||
":before fired for describe circle"
|
||||
(= (first trace-log) "BEFORE describe-shape(circle)")
|
||||
true)))
|
||||
|
||||
(let
|
||||
((r (clos-make-instance "rect" ":width" 2 ":height" 7 ":color" "black")))
|
||||
(do
|
||||
(trace-clear)
|
||||
(check
|
||||
"rect describe"
|
||||
(clos-call-generic "describe-shape" (list r))
|
||||
"rect[2x7 shape[black]]")
|
||||
(check
|
||||
":before fired for describe rect"
|
||||
(= (first trace-log) "BEFORE describe-shape(rect)")
|
||||
true)))
|
||||
|
||||
;; ── call-next-method: circle -> shape ─────────────────────────────────────
|
||||
|
||||
(let
|
||||
((c (clos-make-instance "circle" ":radius" 1 ":color" "purple")))
|
||||
(check
|
||||
"call-next-method result in describe"
|
||||
(clos-call-generic "describe-shape" (list c))
|
||||
"circle[r=1 shape[purple]]"))
|
||||
|
||||
;; ── summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define mop-passed passed)
|
||||
(define mop-failed failed)
|
||||
(define mop-failures failures)
|
||||
163
lib/common-lisp/tests/programs/parse-recover.sx
Normal file
163
lib/common-lisp/tests/programs/parse-recover.sx
Normal file
@@ -0,0 +1,163 @@
|
||||
;; parse-recover.sx — Parser with skipped-token restart
|
||||
;;
|
||||
;; Classic CL pattern: a simple token parser that signals a condition
|
||||
;; when it encounters an unexpected token. The :skip-token restart
|
||||
;; allows the parser to continue past the offending token.
|
||||
;;
|
||||
;; Depends on: lib/common-lisp/runtime.sx already loaded.
|
||||
|
||||
;; ── condition type ─────────────────────────────────────────────────────────
|
||||
|
||||
(cl-define-condition "parse-error" (list "error") (list "token" "position"))
|
||||
|
||||
;; ── simple token parser ────────────────────────────────────────────────────
|
||||
;;
|
||||
;; parse-numbers: given a list of tokens (strings), parse integers.
|
||||
;; Non-integer tokens signal parse-error with two restarts:
|
||||
;; skip-token — skip the bad token and continue
|
||||
;; use-zero — use 0 in place of the bad token
|
||||
|
||||
(define
|
||||
parse-numbers
|
||||
(fn
|
||||
(tokens)
|
||||
(define result (list))
|
||||
(define
|
||||
process
|
||||
(fn
|
||||
(toks)
|
||||
(if
|
||||
(empty? toks)
|
||||
result
|
||||
(let
|
||||
((tok (first toks)) (rest-toks (rest toks)))
|
||||
(let
|
||||
((n (string->number tok 10)))
|
||||
(if
|
||||
n
|
||||
(begin
|
||||
(set! result (append result (list n)))
|
||||
(process rest-toks))
|
||||
(cl-restart-case
|
||||
(fn
|
||||
()
|
||||
(cl-signal
|
||||
(cl-make-condition
|
||||
"parse-error"
|
||||
"token"
|
||||
tok
|
||||
"position"
|
||||
(len result)))
|
||||
(set! result (append result (list 0)))
|
||||
(process rest-toks))
|
||||
(list "skip-token" (list) (fn () (process rest-toks)))
|
||||
(list
|
||||
"use-zero"
|
||||
(list)
|
||||
(fn
|
||||
()
|
||||
(begin
|
||||
(set! result (append result (list 0)))
|
||||
(process rest-toks)))))))))))
|
||||
(process tokens)
|
||||
result))
|
||||
|
||||
;; ── tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
check
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
(define
|
||||
reset-stacks!
|
||||
(fn () (set! cl-handler-stack (list)) (set! cl-restart-stack (list))))
|
||||
|
||||
;; All valid tokens
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"all valid: 1 2 3"
|
||||
(cl-handler-bind
|
||||
(list (list "parse-error" (fn (c) (cl-invoke-restart "skip-token"))))
|
||||
(fn () (parse-numbers (list "1" "2" "3"))))
|
||||
(list 1 2 3))
|
||||
|
||||
;; Skip bad token
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"skip bad token: 1 x 3 -> (1 3)"
|
||||
(cl-handler-bind
|
||||
(list (list "parse-error" (fn (c) (cl-invoke-restart "skip-token"))))
|
||||
(fn () (parse-numbers (list "1" "x" "3"))))
|
||||
(list 1 3))
|
||||
|
||||
;; Use zero for bad token
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"use-zero for bad: 1 x 3 -> (1 0 3)"
|
||||
(cl-handler-bind
|
||||
(list (list "parse-error" (fn (c) (cl-invoke-restart "use-zero"))))
|
||||
(fn () (parse-numbers (list "1" "x" "3"))))
|
||||
(list 1 0 3))
|
||||
|
||||
;; Multiple bad tokens, all skipped
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"skip multiple bad: a 2 b 4 -> (2 4)"
|
||||
(cl-handler-bind
|
||||
(list (list "parse-error" (fn (c) (cl-invoke-restart "skip-token"))))
|
||||
(fn () (parse-numbers (list "a" "2" "b" "4"))))
|
||||
(list 2 4))
|
||||
|
||||
;; handler-case: abort on first bad token
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"handler-case: abort on first bad"
|
||||
(cl-handler-case
|
||||
(fn () (parse-numbers (list "1" "bad" "3")))
|
||||
(list
|
||||
"parse-error"
|
||||
(fn
|
||||
(c)
|
||||
(str
|
||||
"parse error at position "
|
||||
(cl-condition-slot c "position")
|
||||
": "
|
||||
(cl-condition-slot c "token")))))
|
||||
"parse error at position 1: bad")
|
||||
|
||||
;; Verify condition type hierarchy
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"parse-error isa error"
|
||||
(cl-condition-of-type?
|
||||
(cl-make-condition "parse-error" "token" "x" "position" 0)
|
||||
"error")
|
||||
true)
|
||||
|
||||
;; ── summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define parse-passed passed)
|
||||
(define parse-failed failed)
|
||||
(define parse-failures failures)
|
||||
141
lib/common-lisp/tests/programs/restart-demo.sx
Normal file
141
lib/common-lisp/tests/programs/restart-demo.sx
Normal file
@@ -0,0 +1,141 @@
|
||||
;; restart-demo.sx — Classic CL condition system demo
|
||||
;;
|
||||
;; Demonstrates resumable exceptions via restarts.
|
||||
;; The `safe-divide` function signals a division-by-zero condition
|
||||
;; and offers two restarts:
|
||||
;; :use-zero — return 0 as the result
|
||||
;; :retry — call safe-divide again with a corrected divisor
|
||||
;;
|
||||
;; Depends on: lib/common-lisp/runtime.sx already loaded.
|
||||
|
||||
;; ── safe-divide ────────────────────────────────────────────────────────────
|
||||
;;
|
||||
;; Divides numerator by denominator.
|
||||
;; When denominator is 0, signals division-by-zero with two restarts.
|
||||
|
||||
(define
|
||||
safe-divide
|
||||
(fn
|
||||
(n d)
|
||||
(if
|
||||
(= d 0)
|
||||
(cl-restart-case
|
||||
(fn
|
||||
()
|
||||
(cl-signal
|
||||
(cl-make-condition
|
||||
"division-by-zero"
|
||||
"operation"
|
||||
"/"
|
||||
"operands"
|
||||
(list n d)))
|
||||
(error "division by zero — no restart invoked"))
|
||||
(list "use-zero" (list) (fn () 0))
|
||||
(list "retry" (list "d") (fn (d2) (safe-divide n d2))))
|
||||
(/ n d))))
|
||||
|
||||
;; ── tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
check
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
(define
|
||||
reset-stacks!
|
||||
(fn () (set! cl-handler-stack (list)) (set! cl-restart-stack (list))))
|
||||
|
||||
;; Normal division
|
||||
(reset-stacks!)
|
||||
(check "10 / 2 = 5" (safe-divide 10 2) 5)
|
||||
|
||||
;; Invoke use-zero restart
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"10 / 0 -> use-zero"
|
||||
(cl-handler-bind
|
||||
(list
|
||||
(list "division-by-zero" (fn (c) (cl-invoke-restart "use-zero"))))
|
||||
(fn () (safe-divide 10 0)))
|
||||
0)
|
||||
|
||||
;; Invoke retry restart with a corrected denominator
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"10 / 0 -> retry with 2"
|
||||
(cl-handler-bind
|
||||
(list
|
||||
(list
|
||||
"division-by-zero"
|
||||
(fn (c) (cl-invoke-restart "retry" 2))))
|
||||
(fn () (safe-divide 10 0)))
|
||||
5)
|
||||
|
||||
;; Nested calls: outer handles the inner divide-by-zero
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"nested: 20 / (0->4) = 5"
|
||||
(cl-handler-bind
|
||||
(list
|
||||
(list
|
||||
"division-by-zero"
|
||||
(fn (c) (cl-invoke-restart "retry" 4))))
|
||||
(fn () (let ((r1 (safe-divide 20 0))) r1)))
|
||||
5)
|
||||
|
||||
;; handler-case — unwinding version
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"handler-case: catches division-by-zero"
|
||||
(cl-handler-case
|
||||
(fn () (safe-divide 9 0))
|
||||
(list "division-by-zero" (fn (c) "caught!")))
|
||||
"caught!")
|
||||
|
||||
;; Verify use-zero is idempotent (two uses)
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"two use-zero invocations"
|
||||
(cl-handler-bind
|
||||
(list
|
||||
(list "division-by-zero" (fn (c) (cl-invoke-restart "use-zero"))))
|
||||
(fn
|
||||
()
|
||||
(+
|
||||
(safe-divide 10 0)
|
||||
(safe-divide 3 0))))
|
||||
0)
|
||||
|
||||
;; No restart needed for normal division
|
||||
(reset-stacks!)
|
||||
(check
|
||||
"no restart needed for 8/4"
|
||||
(safe-divide 8 4)
|
||||
2)
|
||||
|
||||
;; ── summary ────────────────────────────────────────────────────────────────
|
||||
|
||||
(define demo-passed passed)
|
||||
(define demo-failed failed)
|
||||
(define demo-failures failures)
|
||||
180
lib/common-lisp/tests/read.sx
Normal file
180
lib/common-lisp/tests/read.sx
Normal file
@@ -0,0 +1,180 @@
|
||||
;; Common Lisp tokenizer tests
|
||||
|
||||
(define cl-test-pass 0)
|
||||
(define cl-test-fail 0)
|
||||
(define cl-test-fails (list))
|
||||
|
||||
(define
|
||||
cl-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! cl-test-pass (+ cl-test-pass 1))
|
||||
(do
|
||||
(set! cl-test-fail (+ cl-test-fail 1))
|
||||
(append! cl-test-fails {:name name :expected expected :actual actual})))))
|
||||
|
||||
;; Helpers: extract types and values from token stream (drops eof)
|
||||
(define
|
||||
cl-tok-types
|
||||
(fn
|
||||
(src)
|
||||
(map
|
||||
(fn (t) (get t "type"))
|
||||
(filter (fn (t) (not (= (get t "type") "eof"))) (cl-tokenize src)))))
|
||||
|
||||
(define
|
||||
cl-tok-values
|
||||
(fn
|
||||
(src)
|
||||
(map
|
||||
(fn (t) (get t "value"))
|
||||
(filter (fn (t) (not (= (get t "type") "eof"))) (cl-tokenize src)))))
|
||||
|
||||
(define
|
||||
cl-tok-first
|
||||
(fn (src) (nth (cl-tokenize src) 0)))
|
||||
|
||||
;; ── symbols ───────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "symbol: bare lowercase" (cl-tok-values "foo") (list "FOO"))
|
||||
(cl-test "symbol: uppercase" (cl-tok-values "BAR") (list "BAR"))
|
||||
(cl-test "symbol: mixed case folded" (cl-tok-values "FooBar") (list "FOOBAR"))
|
||||
(cl-test "symbol: with hyphen" (cl-tok-values "foo-bar") (list "FOO-BAR"))
|
||||
(cl-test "symbol: with star" (cl-tok-values "*special*") (list "*SPECIAL*"))
|
||||
(cl-test "symbol: with question" (cl-tok-values "null?") (list "NULL?"))
|
||||
(cl-test "symbol: with exclamation" (cl-tok-values "set!") (list "SET!"))
|
||||
(cl-test "symbol: plus sign alone" (cl-tok-values "+") (list "+"))
|
||||
(cl-test "symbol: minus sign alone" (cl-tok-values "-") (list "-"))
|
||||
(cl-test "symbol: type is symbol" (cl-tok-types "foo") (list "symbol"))
|
||||
|
||||
;; ── package-qualified symbols ─────────────────────────────────────
|
||||
|
||||
(cl-test "symbol: pkg:sym external" (cl-tok-values "cl:car") (list "CL:CAR"))
|
||||
(cl-test "symbol: pkg::sym internal" (cl-tok-values "pkg::foo") (list "PKG::FOO"))
|
||||
(cl-test "symbol: cl:car type" (cl-tok-types "cl:car") (list "symbol"))
|
||||
|
||||
;; ── keywords ──────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "keyword: basic" (cl-tok-values ":foo") (list "FOO"))
|
||||
(cl-test "keyword: type" (cl-tok-types ":foo") (list "keyword"))
|
||||
(cl-test "keyword: upcase" (cl-tok-values ":hello-world") (list "HELLO-WORLD"))
|
||||
(cl-test "keyword: multiple" (cl-tok-types ":a :b :c") (list "keyword" "keyword" "keyword"))
|
||||
|
||||
;; ── integers ──────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "integer: zero" (cl-tok-values "0") (list "0"))
|
||||
(cl-test "integer: positive" (cl-tok-values "42") (list "42"))
|
||||
(cl-test "integer: negative" (cl-tok-values "-5") (list "-5"))
|
||||
(cl-test "integer: positive-sign" (cl-tok-values "+3") (list "+3"))
|
||||
(cl-test "integer: type" (cl-tok-types "42") (list "integer"))
|
||||
(cl-test "integer: multi-digit" (cl-tok-values "12345678") (list "12345678"))
|
||||
|
||||
;; ── hex, binary, octal ───────────────────────────────────────────
|
||||
|
||||
(cl-test "hex: lowercase x" (cl-tok-values "#xFF") (list "#xFF"))
|
||||
(cl-test "hex: uppercase X" (cl-tok-values "#XFF") (list "#XFF"))
|
||||
(cl-test "hex: type" (cl-tok-types "#xFF") (list "integer"))
|
||||
(cl-test "hex: zero" (cl-tok-values "#x0") (list "#x0"))
|
||||
(cl-test "binary: #b" (cl-tok-values "#b1010") (list "#b1010"))
|
||||
(cl-test "binary: type" (cl-tok-types "#b1010") (list "integer"))
|
||||
(cl-test "octal: #o" (cl-tok-values "#o17") (list "#o17"))
|
||||
(cl-test "octal: type" (cl-tok-types "#o17") (list "integer"))
|
||||
|
||||
;; ── floats ────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "float: basic" (cl-tok-values "3.14") (list "3.14"))
|
||||
(cl-test "float: type" (cl-tok-types "3.14") (list "float"))
|
||||
(cl-test "float: negative" (cl-tok-values "-2.5") (list "-2.5"))
|
||||
(cl-test "float: exponent" (cl-tok-values "1.0e10") (list "1.0e10"))
|
||||
(cl-test "float: neg exponent" (cl-tok-values "1.5e-3") (list "1.5e-3"))
|
||||
(cl-test "float: leading dot" (cl-tok-values ".5") (list "0.5"))
|
||||
(cl-test "float: exp only" (cl-tok-values "1e5") (list "1e5"))
|
||||
|
||||
;; ── ratios ────────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "ratio: 1/3" (cl-tok-values "1/3") (list "1/3"))
|
||||
(cl-test "ratio: type" (cl-tok-types "1/3") (list "ratio"))
|
||||
(cl-test "ratio: 22/7" (cl-tok-values "22/7") (list "22/7"))
|
||||
(cl-test "ratio: negative" (cl-tok-values "-1/2") (list "-1/2"))
|
||||
|
||||
;; ── strings ───────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "string: empty" (cl-tok-values "\"\"") (list ""))
|
||||
(cl-test "string: basic" (cl-tok-values "\"hello\"") (list "hello"))
|
||||
(cl-test "string: type" (cl-tok-types "\"hello\"") (list "string"))
|
||||
(cl-test "string: with space" (cl-tok-values "\"hello world\"") (list "hello world"))
|
||||
(cl-test "string: escaped quote" (cl-tok-values "\"say \\\"hi\\\"\"") (list "say \"hi\""))
|
||||
(cl-test "string: escaped backslash" (cl-tok-values "\"a\\\\b\"") (list "a\\b"))
|
||||
(cl-test "string: newline escape" (cl-tok-values "\"a\\nb\"") (list "a\nb"))
|
||||
(cl-test "string: tab escape" (cl-tok-values "\"a\\tb\"") (list "a\tb"))
|
||||
|
||||
;; ── characters ────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "char: lowercase a" (cl-tok-values "#\\a") (list "a"))
|
||||
(cl-test "char: uppercase A" (cl-tok-values "#\\A") (list "A"))
|
||||
(cl-test "char: digit" (cl-tok-values "#\\1") (list "1"))
|
||||
(cl-test "char: type" (cl-tok-types "#\\a") (list "char"))
|
||||
(cl-test "char: Space" (cl-tok-values "#\\Space") (list " "))
|
||||
(cl-test "char: Newline" (cl-tok-values "#\\Newline") (list "\n"))
|
||||
(cl-test "char: Tab" (cl-tok-values "#\\Tab") (list "\t"))
|
||||
(cl-test "char: Return" (cl-tok-values "#\\Return") (list "\r"))
|
||||
|
||||
;; ── reader macros ─────────────────────────────────────────────────
|
||||
|
||||
(cl-test "quote: type" (cl-tok-types "'x") (list "quote" "symbol"))
|
||||
(cl-test "backquote: type" (cl-tok-types "`x") (list "backquote" "symbol"))
|
||||
(cl-test "comma: type" (cl-tok-types ",x") (list "comma" "symbol"))
|
||||
(cl-test "comma-at: type" (cl-tok-types ",@x") (list "comma-at" "symbol"))
|
||||
(cl-test "hash-quote: type" (cl-tok-types "#'foo") (list "hash-quote" "symbol"))
|
||||
(cl-test "hash-paren: type" (cl-tok-types "#(1 2)") (list "hash-paren" "integer" "integer" "rparen"))
|
||||
|
||||
;; ── uninterned ────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "uninterned: type" (cl-tok-types "#:foo") (list "uninterned"))
|
||||
(cl-test "uninterned: value upcase" (cl-tok-values "#:foo") (list "FOO"))
|
||||
(cl-test "uninterned: compound" (cl-tok-values "#:my-sym") (list "MY-SYM"))
|
||||
|
||||
;; ── parens and structure ──────────────────────────────────────────
|
||||
|
||||
(cl-test "paren: empty list" (cl-tok-types "()") (list "lparen" "rparen"))
|
||||
(cl-test "paren: nested" (cl-tok-types "((a))") (list "lparen" "lparen" "symbol" "rparen" "rparen"))
|
||||
(cl-test "dot: standalone" (cl-tok-types "(a . b)") (list "lparen" "symbol" "dot" "symbol" "rparen"))
|
||||
|
||||
;; ── comments ──────────────────────────────────────────────────────
|
||||
|
||||
(cl-test "comment: line" (cl-tok-types "; comment\nfoo") (list "symbol"))
|
||||
(cl-test "comment: inline" (cl-tok-values "foo ; bar\nbaz") (list "FOO" "BAZ"))
|
||||
(cl-test "block-comment: basic" (cl-tok-types "#| hello |# foo") (list "symbol"))
|
||||
(cl-test "block-comment: nested" (cl-tok-types "#| a #| b |# c |# x") (list "symbol"))
|
||||
|
||||
;; ── combined ──────────────────────────────────────────────────────
|
||||
|
||||
(cl-test
|
||||
"combined: defun skeleton"
|
||||
(cl-tok-types "(defun foo (x) x)")
|
||||
(list "lparen" "symbol" "symbol" "lparen" "symbol" "rparen" "symbol" "rparen"))
|
||||
|
||||
(cl-test
|
||||
"combined: let form"
|
||||
(cl-tok-types "(let ((x 1)) x)")
|
||||
(list
|
||||
"lparen"
|
||||
"symbol"
|
||||
"lparen"
|
||||
"lparen"
|
||||
"symbol"
|
||||
"integer"
|
||||
"rparen"
|
||||
"rparen"
|
||||
"symbol"
|
||||
"rparen"))
|
||||
|
||||
(cl-test
|
||||
"combined: whitespace skip"
|
||||
(cl-tok-values " foo bar baz ")
|
||||
(list "FOO" "BAR" "BAZ"))
|
||||
|
||||
(cl-test "eof: present" (get (nth (cl-tokenize "") 0) "type") "eof")
|
||||
(cl-test "eof: at end of tokens" (get (nth (cl-tokenize "x") 1) "type") "eof")
|
||||
207
lib/common-lisp/tests/runtime.sx
Normal file
207
lib/common-lisp/tests/runtime.sx
Normal file
@@ -0,0 +1,207 @@
|
||||
;; lib/common-lisp/tests/runtime.sx — tests for CL runtime layer
|
||||
|
||||
(load "lib/common-lisp/runtime.sx")
|
||||
|
||||
(defsuite
|
||||
"cl-types"
|
||||
(deftest "cl-null? nil" (assert= true (cl-null? nil)))
|
||||
(deftest "cl-null? false" (assert= false (cl-null? false)))
|
||||
(deftest
|
||||
"cl-consp? pair"
|
||||
(assert= true (cl-consp? (list 1 2))))
|
||||
(deftest "cl-consp? nil" (assert= false (cl-consp? nil)))
|
||||
(deftest "cl-listp? nil" (assert= true (cl-listp? nil)))
|
||||
(deftest
|
||||
"cl-listp? list"
|
||||
(assert= true (cl-listp? (list 1 2))))
|
||||
(deftest "cl-atom? nil" (assert= true (cl-atom? nil)))
|
||||
(deftest "cl-atom? pair" (assert= false (cl-atom? (list 1))))
|
||||
(deftest "cl-integerp?" (assert= true (cl-integerp? 42)))
|
||||
(deftest "cl-floatp?" (assert= true (cl-floatp? 3.14)))
|
||||
(deftest
|
||||
"cl-characterp?"
|
||||
(assert= true (cl-characterp? (integer->char 65))))
|
||||
(deftest "cl-stringp?" (assert= true (cl-stringp? "hello")))
|
||||
(deftest "cl-symbolp?" (assert= true (cl-symbolp? (quote foo)))))
|
||||
|
||||
(defsuite
|
||||
"cl-arithmetic"
|
||||
(deftest "cl-mod" (assert= 1 (cl-mod 10 3)))
|
||||
(deftest "cl-rem" (assert= 1 (cl-rem 10 3)))
|
||||
(deftest
|
||||
"cl-quotient"
|
||||
(assert= 3 (cl-quotient 10 3)))
|
||||
(deftest "cl-gcd" (assert= 4 (cl-gcd 12 8)))
|
||||
(deftest "cl-lcm" (assert= 12 (cl-lcm 4 6)))
|
||||
(deftest "cl-abs pos" (assert= 5 (cl-abs 5)))
|
||||
(deftest "cl-abs neg" (assert= 5 (cl-abs -5)))
|
||||
(deftest "cl-min" (assert= 2 (cl-min 2 7)))
|
||||
(deftest "cl-max" (assert= 7 (cl-max 2 7)))
|
||||
(deftest "cl-evenp? t" (assert= true (cl-evenp? 4)))
|
||||
(deftest "cl-evenp? f" (assert= false (cl-evenp? 3)))
|
||||
(deftest "cl-oddp? t" (assert= true (cl-oddp? 7)))
|
||||
(deftest "cl-zerop?" (assert= true (cl-zerop? 0)))
|
||||
(deftest "cl-plusp?" (assert= true (cl-plusp? 1)))
|
||||
(deftest "cl-minusp?" (assert= true (cl-minusp? -1)))
|
||||
(deftest "cl-signum pos" (assert= 1 (cl-signum 42)))
|
||||
(deftest "cl-signum neg" (assert= -1 (cl-signum -7)))
|
||||
(deftest "cl-signum zero" (assert= 0 (cl-signum 0))))
|
||||
|
||||
(defsuite
|
||||
"cl-chars"
|
||||
(deftest
|
||||
"cl-char-code"
|
||||
(assert= 65 (cl-char-code (integer->char 65))))
|
||||
(deftest "cl-code-char" (assert= true (char? (cl-code-char 65))))
|
||||
(deftest
|
||||
"cl-char-upcase"
|
||||
(assert=
|
||||
(integer->char 65)
|
||||
(cl-char-upcase (integer->char 97))))
|
||||
(deftest
|
||||
"cl-char-downcase"
|
||||
(assert=
|
||||
(integer->char 97)
|
||||
(cl-char-downcase (integer->char 65))))
|
||||
(deftest
|
||||
"cl-alpha-char-p"
|
||||
(assert= true (cl-alpha-char-p (integer->char 65))))
|
||||
(deftest
|
||||
"cl-digit-char-p"
|
||||
(assert= true (cl-digit-char-p (integer->char 48))))
|
||||
(deftest
|
||||
"cl-char=?"
|
||||
(assert=
|
||||
true
|
||||
(cl-char=? (integer->char 65) (integer->char 65))))
|
||||
(deftest
|
||||
"cl-char<?"
|
||||
(assert=
|
||||
true
|
||||
(cl-char<? (integer->char 65) (integer->char 90))))
|
||||
(deftest
|
||||
"cl-char space"
|
||||
(assert= (integer->char 32) cl-char-space))
|
||||
(deftest
|
||||
"cl-char newline"
|
||||
(assert= (integer->char 10) cl-char-newline)))
|
||||
|
||||
(defsuite
|
||||
"cl-format"
|
||||
(deftest
|
||||
"cl-format nil basic"
|
||||
(assert= "hello" (cl-format nil "~a" "hello")))
|
||||
(deftest
|
||||
"cl-format nil number"
|
||||
(assert= "42" (cl-format nil "~d" 42)))
|
||||
(deftest
|
||||
"cl-format nil hex"
|
||||
(assert= "ff" (cl-format nil "~x" 255)))
|
||||
(deftest
|
||||
"cl-format nil template"
|
||||
(assert= "x=3 y=4" (cl-format nil "x=~d y=~d" 3 4)))
|
||||
(deftest "cl-format nil tilde" (assert= "a~b" (cl-format nil "a~~b"))))
|
||||
|
||||
(defsuite
|
||||
"cl-gensym"
|
||||
(deftest
|
||||
"cl-gensym returns symbol"
|
||||
(assert= "symbol" (type-of (cl-gensym))))
|
||||
(deftest "cl-gensym unique" (assert= false (= (cl-gensym) (cl-gensym)))))
|
||||
|
||||
(defsuite
|
||||
"cl-sets"
|
||||
(deftest "cl-make-set empty" (assert= true (cl-set? (cl-make-set))))
|
||||
(deftest
|
||||
"cl-set-add/member"
|
||||
(let
|
||||
((s (cl-make-set)))
|
||||
(do
|
||||
(cl-set-add s 1)
|
||||
(assert= true (cl-set-memberp s 1)))))
|
||||
(deftest
|
||||
"cl-set-memberp false"
|
||||
(assert= false (cl-set-memberp (cl-make-set) 42)))
|
||||
(deftest
|
||||
"cl-list->set"
|
||||
(let
|
||||
((s (cl-list->set (list 1 2 3))))
|
||||
(assert= true (cl-set-memberp s 2)))))
|
||||
|
||||
(defsuite
|
||||
"cl-lists"
|
||||
(deftest
|
||||
"cl-nth 0"
|
||||
(assert=
|
||||
1
|
||||
(cl-nth 0 (list 1 2 3))))
|
||||
(deftest
|
||||
"cl-nth 2"
|
||||
(assert=
|
||||
3
|
||||
(cl-nth 2 (list 1 2 3))))
|
||||
(deftest
|
||||
"cl-last"
|
||||
(assert=
|
||||
(list 3)
|
||||
(cl-last (list 1 2 3))))
|
||||
(deftest
|
||||
"cl-butlast"
|
||||
(assert=
|
||||
(list 1 2)
|
||||
(cl-butlast (list 1 2 3))))
|
||||
(deftest
|
||||
"cl-nthcdr 1"
|
||||
(assert=
|
||||
(list 2 3)
|
||||
(cl-nthcdr 1 (list 1 2 3))))
|
||||
(deftest
|
||||
"cl-assoc hit"
|
||||
(assert=
|
||||
(list "b" 2)
|
||||
(cl-assoc "b" (list (list "a" 1) (list "b" 2)))))
|
||||
(deftest
|
||||
"cl-assoc miss"
|
||||
(assert= nil (cl-assoc "z" (list (list "a" 1)))))
|
||||
(deftest
|
||||
"cl-getf hit"
|
||||
(assert= 42 (cl-getf (list "x" 42 "y" 99) "x")))
|
||||
(deftest "cl-getf miss" (assert= nil (cl-getf (list "x" 42) "z")))
|
||||
(deftest
|
||||
"cl-adjoin new"
|
||||
(assert=
|
||||
(list 0 1 2)
|
||||
(cl-adjoin 0 (list 1 2))))
|
||||
(deftest
|
||||
"cl-adjoin dup"
|
||||
(assert=
|
||||
(list 1 2)
|
||||
(cl-adjoin 1 (list 1 2))))
|
||||
(deftest
|
||||
"cl-flatten"
|
||||
(assert=
|
||||
(list 1 2 3 4)
|
||||
(cl-flatten (list 1 (list 2 3) 4))))
|
||||
(deftest
|
||||
"cl-member hit"
|
||||
(assert=
|
||||
(list 2 3)
|
||||
(cl-member 2 (list 1 2 3))))
|
||||
(deftest
|
||||
"cl-member miss"
|
||||
(assert=
|
||||
nil
|
||||
(cl-member 9 (list 1 2 3)))))
|
||||
|
||||
(defsuite
|
||||
"cl-radix"
|
||||
(deftest "binary" (assert= "1010" (cl-format-binary 10)))
|
||||
(deftest "octal" (assert= "17" (cl-format-octal 15)))
|
||||
(deftest "hex" (assert= "ff" (cl-format-hex 255)))
|
||||
(deftest "decimal" (assert= "42" (cl-format-decimal 42)))
|
||||
(deftest
|
||||
"n->s r16"
|
||||
(assert= "1f" (cl-integer-to-string 31 16)))
|
||||
(deftest
|
||||
"s->n r16"
|
||||
(assert= 31 (cl-string-to-integer "1f" 16))))
|
||||
285
lib/common-lisp/tests/stdlib.sx
Normal file
285
lib/common-lisp/tests/stdlib.sx
Normal file
@@ -0,0 +1,285 @@
|
||||
;; lib/common-lisp/tests/stdlib.sx — Phase 6: sequence, list, string functions
|
||||
|
||||
(define ev (fn (src) (cl-eval-str src (cl-make-env))))
|
||||
|
||||
(define passed 0)
|
||||
(define failed 0)
|
||||
(define failures (list))
|
||||
|
||||
(define
|
||||
check
|
||||
(fn
|
||||
(label got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! passed (+ passed 1))
|
||||
(begin
|
||||
(set! failed (+ failed 1))
|
||||
(set!
|
||||
failures
|
||||
(append
|
||||
failures
|
||||
(list
|
||||
(str
|
||||
"FAIL ["
|
||||
label
|
||||
"]: got="
|
||||
(inspect got)
|
||||
" expected="
|
||||
(inspect expected)))))))))
|
||||
|
||||
;; ── mapc ─────────────────────────────────────────────────────────
|
||||
|
||||
(check "mapc returns list"
|
||||
(ev "(mapc #'1+ '(1 2 3))")
|
||||
(list 1 2 3))
|
||||
|
||||
;; ── mapcan ───────────────────────────────────────────────────────
|
||||
|
||||
(check "mapcan basic"
|
||||
(ev "(mapcan (lambda (x) (list x (* x x))) '(1 2 3))")
|
||||
(list 1 1 2 4 3 9))
|
||||
|
||||
(check "mapcan filter-like"
|
||||
(ev "(mapcan (lambda (x) (if (evenp x) (list x) nil)) '(1 2 3 4 5 6))")
|
||||
(list 2 4 6))
|
||||
|
||||
;; ── reduce ───────────────────────────────────────────────────────
|
||||
|
||||
(check "reduce sum"
|
||||
(ev "(reduce #'+ '(1 2 3 4 5))")
|
||||
15)
|
||||
|
||||
(check "reduce with initial-value"
|
||||
(ev "(reduce #'+ '(1 2 3) :initial-value 10)")
|
||||
16)
|
||||
|
||||
(check "reduce max"
|
||||
(ev "(reduce (lambda (a b) (if (> a b) a b)) '(3 1 4 1 5 9 2 6))")
|
||||
9)
|
||||
|
||||
;; ── find ─────────────────────────────────────────────────────────
|
||||
|
||||
(check "find present"
|
||||
(ev "(find 3 '(1 2 3 4 5))")
|
||||
3)
|
||||
|
||||
(check "find absent"
|
||||
(ev "(find 9 '(1 2 3))")
|
||||
nil)
|
||||
|
||||
(check "find-if present"
|
||||
(ev "(find-if #'evenp '(1 3 4 7))")
|
||||
4)
|
||||
|
||||
(check "find-if absent"
|
||||
(ev "(find-if #'evenp '(1 3 5))")
|
||||
nil)
|
||||
|
||||
(check "find-if-not"
|
||||
(ev "(find-if-not #'evenp '(2 4 5 6))")
|
||||
5)
|
||||
|
||||
;; ── position ─────────────────────────────────────────────────────
|
||||
|
||||
(check "position found"
|
||||
(ev "(position 3 '(1 2 3 4 5))")
|
||||
2)
|
||||
|
||||
(check "position not found"
|
||||
(ev "(position 9 '(1 2 3))")
|
||||
nil)
|
||||
|
||||
(check "position-if"
|
||||
(ev "(position-if #'evenp '(1 3 4 8))")
|
||||
2)
|
||||
|
||||
;; ── count ────────────────────────────────────────────────────────
|
||||
|
||||
(check "count"
|
||||
(ev "(count 2 '(1 2 3 2 4 2))")
|
||||
3)
|
||||
|
||||
(check "count-if"
|
||||
(ev "(count-if #'evenp '(1 2 3 4 5 6))")
|
||||
3)
|
||||
|
||||
;; ── every / some / notany / notevery ─────────────────────────────
|
||||
|
||||
(check "every true"
|
||||
(ev "(every #'evenp '(2 4 6))")
|
||||
true)
|
||||
|
||||
(check "every false"
|
||||
(ev "(every #'evenp '(2 3 6))")
|
||||
nil)
|
||||
|
||||
(check "every empty"
|
||||
(ev "(every #'evenp '())")
|
||||
true)
|
||||
|
||||
(check "some truthy"
|
||||
(ev "(some #'evenp '(1 3 4))")
|
||||
true)
|
||||
|
||||
(check "some nil"
|
||||
(ev "(some #'evenp '(1 3 5))")
|
||||
nil)
|
||||
|
||||
(check "notany true"
|
||||
(ev "(notany #'evenp '(1 3 5))")
|
||||
true)
|
||||
|
||||
(check "notany false"
|
||||
(ev "(notany #'evenp '(1 2 5))")
|
||||
nil)
|
||||
|
||||
(check "notevery false"
|
||||
(ev "(notevery #'evenp '(2 4 6))")
|
||||
nil)
|
||||
|
||||
(check "notevery true"
|
||||
(ev "(notevery #'evenp '(2 3 6))")
|
||||
true)
|
||||
|
||||
;; ── remove ───────────────────────────────────────────────────────
|
||||
|
||||
(check "remove"
|
||||
(ev "(remove 3 '(1 2 3 4 3 5))")
|
||||
(list 1 2 4 5))
|
||||
|
||||
(check "remove-if"
|
||||
(ev "(remove-if #'evenp '(1 2 3 4 5 6))")
|
||||
(list 1 3 5))
|
||||
|
||||
(check "remove-if-not"
|
||||
(ev "(remove-if-not #'evenp '(1 2 3 4 5 6))")
|
||||
(list 2 4 6))
|
||||
|
||||
;; ── member ───────────────────────────────────────────────────────
|
||||
|
||||
(check "member found"
|
||||
(ev "(member 3 '(1 2 3 4 5))")
|
||||
(list 3 4 5))
|
||||
|
||||
(check "member not found"
|
||||
(ev "(member 9 '(1 2 3))")
|
||||
nil)
|
||||
|
||||
;; ── subst ────────────────────────────────────────────────────────
|
||||
|
||||
(check "subst flat"
|
||||
(ev "(subst 'b 'a '(a b c a))")
|
||||
(list "B" "B" "C" "B"))
|
||||
|
||||
(check "subst nested"
|
||||
(ev "(subst 99 1 '(1 (2 1) 3))")
|
||||
(list 99 (list 2 99) 3))
|
||||
|
||||
;; ── assoc ────────────────────────────────────────────────────────
|
||||
|
||||
(check "assoc found"
|
||||
(ev "(assoc 'b '((a 1) (b 2) (c 3)))")
|
||||
(list "B" 2))
|
||||
|
||||
(check "assoc not found"
|
||||
(ev "(assoc 'z '((a 1) (b 2)))")
|
||||
nil)
|
||||
|
||||
;; ── list ops ─────────────────────────────────────────────────────
|
||||
|
||||
(check "last"
|
||||
(ev "(last '(1 2 3 4))")
|
||||
(list 4))
|
||||
|
||||
(check "butlast"
|
||||
(ev "(butlast '(1 2 3 4))")
|
||||
(list 1 2 3))
|
||||
|
||||
(check "nthcdr"
|
||||
(ev "(nthcdr 2 '(a b c d))")
|
||||
(list "C" "D"))
|
||||
|
||||
(check "list*"
|
||||
(ev "(list* 1 2 '(3 4))")
|
||||
(list 1 2 3 4))
|
||||
|
||||
(check "cadr"
|
||||
(ev "(cadr '(1 2 3))")
|
||||
2)
|
||||
|
||||
(check "caddr"
|
||||
(ev "(caddr '(1 2 3))")
|
||||
3)
|
||||
|
||||
(check "cadddr"
|
||||
(ev "(cadddr '(1 2 3 4))")
|
||||
4)
|
||||
|
||||
(check "cddr"
|
||||
(ev "(cddr '(1 2 3 4))")
|
||||
(list 3 4))
|
||||
|
||||
;; ── subseq ───────────────────────────────────────────────────────
|
||||
|
||||
(check "subseq string"
|
||||
(ev "(subseq \"hello\" 1 3)")
|
||||
"el")
|
||||
|
||||
(check "subseq list"
|
||||
(ev "(subseq '(a b c d) 1 3)")
|
||||
(list "B" "C"))
|
||||
|
||||
(check "subseq no end"
|
||||
(ev "(subseq \"hello\" 2)")
|
||||
"llo")
|
||||
|
||||
;; ── FORMAT ─────────────────────────────────────────────────────────
|
||||
|
||||
(check "format ~A"
|
||||
(ev "(format nil \"hello ~A\" \"world\")")
|
||||
"hello world")
|
||||
|
||||
(check "format ~D"
|
||||
(ev "(format nil \"~D items\" 42)")
|
||||
"42 items")
|
||||
|
||||
(check "format two args"
|
||||
(ev "(format nil \"~A ~A\" 1 2)")
|
||||
"1 2")
|
||||
|
||||
(check "format ~A+~A=~A"
|
||||
(ev "(format nil \"~A + ~A = ~A\" 1 2 3)")
|
||||
"1 + 2 = 3")
|
||||
|
||||
(check "format iterate"
|
||||
(ev "(format nil \"~{~A~}\" (quote (1 2 3)))")
|
||||
"123")
|
||||
|
||||
(check "format iterate with space"
|
||||
(ev "(format nil \"(~{~A ~})\" (quote (1 2 3)))")
|
||||
"(1 2 3 )")
|
||||
|
||||
;; ── packages ─────────────────────────────────────────────────────
|
||||
|
||||
(check "defpackage returns name"
|
||||
(ev "(defpackage :my-pkg (:use :cl))")
|
||||
"MY-PKG")
|
||||
|
||||
(check "in-package"
|
||||
(ev "(progn (defpackage :test-pkg) (in-package :test-pkg) (package-name))")
|
||||
"TEST-PKG")
|
||||
|
||||
(check "package-qualified function"
|
||||
(ev "(cl:car (quote (1 2 3)))")
|
||||
1)
|
||||
|
||||
(check "package-qualified function 2"
|
||||
(ev "(cl:mapcar (function evenp) (quote (2 3 4)))")
|
||||
(list true nil true))
|
||||
|
||||
;; ── summary ──────────────────────────────────────────────────────
|
||||
|
||||
(define stdlib-passed passed)
|
||||
(define stdlib-failed failed)
|
||||
(define stdlib-failures failures)
|
||||
@@ -1,14 +0,0 @@
|
||||
ANS Forth conformance tests — vendored from
|
||||
https://github.com/gerryjackson/forth2012-test-suite (master, commit-locked
|
||||
on first fetch: 2026-04-24).
|
||||
|
||||
Files in this directory are pristine copies of upstream — do not edit them.
|
||||
They are consumed by the conformance runner in `lib/forth/conformance.sh`.
|
||||
|
||||
- `tester.fr` — John Hayes' test harness (`T{ ... -> ... }T`). (C) 1995
|
||||
Johns Hopkins APL, distributable under its notice.
|
||||
- `core.fr` — Core word set tests (Hayes, ~1000 lines).
|
||||
- `coreexttest.fth` — Core Extension tests (Gerry Jackson).
|
||||
|
||||
Only `core.fr` is expected to run green end-to-end for Phase 3; the others
|
||||
stay parked until later phases.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,775 +0,0 @@
|
||||
\ To test the ANS Forth Core Extension word set
|
||||
|
||||
\ This program was written by Gerry Jackson in 2006, with contributions from
|
||||
\ others where indicated, and is in the public domain - it can be distributed
|
||||
\ and/or modified in any way but please retain this notice.
|
||||
|
||||
\ This program is distributed in the hope that it will be useful,
|
||||
\ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
\ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
\ The tests are not claimed to be comprehensive or correct
|
||||
|
||||
\ ------------------------------------------------------------------------------
|
||||
\ Version 0.15 1 August 2025 Added two tests to VALUE
|
||||
\ 0.14 21 July 2022 Updated first line of BUFFER: test as recommended
|
||||
\ in issue 32
|
||||
\ 0.13 28 October 2015
|
||||
\ Replace <FALSE> and <TRUE> with FALSE and TRUE to avoid
|
||||
\ dependence on Core tests
|
||||
\ Moved SAVE-INPUT and RESTORE-INPUT tests in a file to filetest.fth
|
||||
\ Use of 2VARIABLE (from optional wordset) replaced with CREATE.
|
||||
\ Minor lower to upper case conversions.
|
||||
\ Calls to COMPARE replaced by S= (in utilities.fth) to avoid use
|
||||
\ of a word from an optional word set.
|
||||
\ UNUSED tests revised as UNUSED UNUSED = may return FALSE when an
|
||||
\ implementation has the data stack sharing unused dataspace.
|
||||
\ Double number input dependency removed from the HOLDS tests.
|
||||
\ Minor case sensitivities removed in definition names.
|
||||
\ 0.11 25 April 2015
|
||||
\ Added tests for PARSE-NAME HOLDS BUFFER:
|
||||
\ S\" tests added
|
||||
\ DEFER IS ACTION-OF DEFER! DEFER@ tests added
|
||||
\ Empty CASE statement test added
|
||||
\ [COMPILE] tests removed because it is obsolescent in Forth 2012
|
||||
\ 0.10 1 August 2014
|
||||
\ Added tests contributed by James Bowman for:
|
||||
\ <> U> 0<> 0> NIP TUCK ROLL PICK 2>R 2R@ 2R>
|
||||
\ HEX WITHIN UNUSED AGAIN MARKER
|
||||
\ Added tests for:
|
||||
\ .R U.R ERASE PAD REFILL SOURCE-ID
|
||||
\ Removed ABORT from NeverExecuted to enable Win32
|
||||
\ to continue after failure of RESTORE-INPUT.
|
||||
\ Removed max-intx which is no longer used.
|
||||
\ 0.7 6 June 2012 Extra CASE test added
|
||||
\ 0.6 1 April 2012 Tests placed in the public domain.
|
||||
\ SAVE-INPUT & RESTORE-INPUT tests, position
|
||||
\ of T{ moved so that tests work with ttester.fs
|
||||
\ CONVERT test deleted - obsolete word removed from Forth 200X
|
||||
\ IMMEDIATE VALUEs tested
|
||||
\ RECURSE with :NONAME tested
|
||||
\ PARSE and .( tested
|
||||
\ Parsing behaviour of C" added
|
||||
\ 0.5 14 September 2011 Removed the double [ELSE] from the
|
||||
\ initial SAVE-INPUT & RESTORE-INPUT test
|
||||
\ 0.4 30 November 2009 max-int replaced with max-intx to
|
||||
\ avoid redefinition warnings.
|
||||
\ 0.3 6 March 2009 { and } replaced with T{ and }T
|
||||
\ CONVERT test now independent of cell size
|
||||
\ 0.2 20 April 2007 ANS Forth words changed to upper case
|
||||
\ Tests qd3 to qd6 by Reinhold Straub
|
||||
\ 0.1 Oct 2006 First version released
|
||||
\ -----------------------------------------------------------------------------
|
||||
\ The tests are based on John Hayes test program for the core word set
|
||||
|
||||
\ Words tested in this file are:
|
||||
\ .( .R 0<> 0> 2>R 2R> 2R@ :NONAME <> ?DO AGAIN C" CASE COMPILE, ENDCASE
|
||||
\ ENDOF ERASE FALSE HEX MARKER NIP OF PAD PARSE PICK REFILL
|
||||
\ RESTORE-INPUT ROLL SAVE-INPUT SOURCE-ID TO TRUE TUCK U.R U> UNUSED
|
||||
\ VALUE WITHIN [COMPILE]
|
||||
|
||||
\ Words not tested or partially tested:
|
||||
\ \ because it has been extensively used already and is, hence, unnecessary
|
||||
\ REFILL and SOURCE-ID from the user input device which are not possible
|
||||
\ when testing from a file such as this one
|
||||
\ UNUSED (partially tested) as the value returned is system dependent
|
||||
\ Obsolescent words #TIB CONVERT EXPECT QUERY SPAN TIB as they have been
|
||||
\ removed from the Forth 2012 standard
|
||||
|
||||
\ Results from words that output to the user output device have to visually
|
||||
\ checked for correctness. These are .R U.R .(
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
\ Assumptions & dependencies:
|
||||
\ - tester.fr (or ttester.fs), errorreport.fth and utilities.fth have been
|
||||
\ included prior to this file
|
||||
\ - the Core word set available
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING Core Extension words
|
||||
|
||||
DECIMAL
|
||||
|
||||
TESTING TRUE FALSE
|
||||
|
||||
T{ TRUE -> 0 INVERT }T
|
||||
T{ FALSE -> 0 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING <> U> (contributed by James Bowman)
|
||||
|
||||
T{ 0 0 <> -> FALSE }T
|
||||
T{ 1 1 <> -> FALSE }T
|
||||
T{ -1 -1 <> -> FALSE }T
|
||||
T{ 1 0 <> -> TRUE }T
|
||||
T{ -1 0 <> -> TRUE }T
|
||||
T{ 0 1 <> -> TRUE }T
|
||||
T{ 0 -1 <> -> TRUE }T
|
||||
|
||||
T{ 0 1 U> -> FALSE }T
|
||||
T{ 1 2 U> -> FALSE }T
|
||||
T{ 0 MID-UINT U> -> FALSE }T
|
||||
T{ 0 MAX-UINT U> -> FALSE }T
|
||||
T{ MID-UINT MAX-UINT U> -> FALSE }T
|
||||
T{ 0 0 U> -> FALSE }T
|
||||
T{ 1 1 U> -> FALSE }T
|
||||
T{ 1 0 U> -> TRUE }T
|
||||
T{ 2 1 U> -> TRUE }T
|
||||
T{ MID-UINT 0 U> -> TRUE }T
|
||||
T{ MAX-UINT 0 U> -> TRUE }T
|
||||
T{ MAX-UINT MID-UINT U> -> TRUE }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING 0<> 0> (contributed by James Bowman)
|
||||
|
||||
T{ 0 0<> -> FALSE }T
|
||||
T{ 1 0<> -> TRUE }T
|
||||
T{ 2 0<> -> TRUE }T
|
||||
T{ -1 0<> -> TRUE }T
|
||||
T{ MAX-UINT 0<> -> TRUE }T
|
||||
T{ MIN-INT 0<> -> TRUE }T
|
||||
T{ MAX-INT 0<> -> TRUE }T
|
||||
|
||||
T{ 0 0> -> FALSE }T
|
||||
T{ -1 0> -> FALSE }T
|
||||
T{ MIN-INT 0> -> FALSE }T
|
||||
T{ 1 0> -> TRUE }T
|
||||
T{ MAX-INT 0> -> TRUE }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING NIP TUCK ROLL PICK (contributed by James Bowman)
|
||||
|
||||
T{ 1 2 NIP -> 2 }T
|
||||
T{ 1 2 3 NIP -> 1 3 }T
|
||||
|
||||
T{ 1 2 TUCK -> 2 1 2 }T
|
||||
T{ 1 2 3 TUCK -> 1 3 2 3 }T
|
||||
|
||||
T{ : RO5 100 200 300 400 500 ; -> }T
|
||||
T{ RO5 3 ROLL -> 100 300 400 500 200 }T
|
||||
T{ RO5 2 ROLL -> RO5 ROT }T
|
||||
T{ RO5 1 ROLL -> RO5 SWAP }T
|
||||
T{ RO5 0 ROLL -> RO5 }T
|
||||
|
||||
T{ RO5 2 PICK -> 100 200 300 400 500 300 }T
|
||||
T{ RO5 1 PICK -> RO5 OVER }T
|
||||
T{ RO5 0 PICK -> RO5 DUP }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING 2>R 2R@ 2R> (contributed by James Bowman)
|
||||
|
||||
T{ : RR0 2>R 100 R> R> ; -> }T
|
||||
T{ 300 400 RR0 -> 100 400 300 }T
|
||||
T{ 200 300 400 RR0 -> 200 100 400 300 }T
|
||||
|
||||
T{ : RR1 2>R 100 2R@ R> R> ; -> }T
|
||||
T{ 300 400 RR1 -> 100 300 400 400 300 }T
|
||||
T{ 200 300 400 RR1 -> 200 100 300 400 400 300 }T
|
||||
|
||||
T{ : RR2 2>R 100 2R> ; -> }T
|
||||
T{ 300 400 RR2 -> 100 300 400 }T
|
||||
T{ 200 300 400 RR2 -> 200 100 300 400 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING HEX (contributed by James Bowman)
|
||||
|
||||
T{ BASE @ HEX BASE @ DECIMAL BASE @ - SWAP BASE ! -> 6 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING WITHIN (contributed by James Bowman)
|
||||
|
||||
T{ 0 0 0 WITHIN -> FALSE }T
|
||||
T{ 0 0 MID-UINT WITHIN -> TRUE }T
|
||||
T{ 0 0 MID-UINT+1 WITHIN -> TRUE }T
|
||||
T{ 0 0 MAX-UINT WITHIN -> TRUE }T
|
||||
T{ 0 MID-UINT 0 WITHIN -> FALSE }T
|
||||
T{ 0 MID-UINT MID-UINT WITHIN -> FALSE }T
|
||||
T{ 0 MID-UINT MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ 0 MID-UINT MAX-UINT WITHIN -> FALSE }T
|
||||
T{ 0 MID-UINT+1 0 WITHIN -> FALSE }T
|
||||
T{ 0 MID-UINT+1 MID-UINT WITHIN -> TRUE }T
|
||||
T{ 0 MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ 0 MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
|
||||
T{ 0 MAX-UINT 0 WITHIN -> FALSE }T
|
||||
T{ 0 MAX-UINT MID-UINT WITHIN -> TRUE }T
|
||||
T{ 0 MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
|
||||
T{ 0 MAX-UINT MAX-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT 0 0 WITHIN -> FALSE }T
|
||||
T{ MID-UINT 0 MID-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT 0 MID-UINT+1 WITHIN -> TRUE }T
|
||||
T{ MID-UINT 0 MAX-UINT WITHIN -> TRUE }T
|
||||
T{ MID-UINT MID-UINT 0 WITHIN -> TRUE }T
|
||||
T{ MID-UINT MID-UINT MID-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT MID-UINT MID-UINT+1 WITHIN -> TRUE }T
|
||||
T{ MID-UINT MID-UINT MAX-UINT WITHIN -> TRUE }T
|
||||
T{ MID-UINT MID-UINT+1 0 WITHIN -> FALSE }T
|
||||
T{ MID-UINT MID-UINT+1 MID-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ MID-UINT MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT MAX-UINT 0 WITHIN -> FALSE }T
|
||||
T{ MID-UINT MAX-UINT MID-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
|
||||
T{ MID-UINT MAX-UINT MAX-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 0 0 WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 0 MID-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 0 MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 0 MAX-UINT WITHIN -> TRUE }T
|
||||
T{ MID-UINT+1 MID-UINT 0 WITHIN -> TRUE }T
|
||||
T{ MID-UINT+1 MID-UINT MID-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 MID-UINT MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 MID-UINT MAX-UINT WITHIN -> TRUE }T
|
||||
T{ MID-UINT+1 MID-UINT+1 0 WITHIN -> TRUE }T
|
||||
T{ MID-UINT+1 MID-UINT+1 MID-UINT WITHIN -> TRUE }T
|
||||
T{ MID-UINT+1 MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 MID-UINT+1 MAX-UINT WITHIN -> TRUE }T
|
||||
T{ MID-UINT+1 MAX-UINT 0 WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 MAX-UINT MID-UINT WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 MAX-UINT MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ MID-UINT+1 MAX-UINT MAX-UINT WITHIN -> FALSE }T
|
||||
T{ MAX-UINT 0 0 WITHIN -> FALSE }T
|
||||
T{ MAX-UINT 0 MID-UINT WITHIN -> FALSE }T
|
||||
T{ MAX-UINT 0 MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ MAX-UINT 0 MAX-UINT WITHIN -> FALSE }T
|
||||
T{ MAX-UINT MID-UINT 0 WITHIN -> TRUE }T
|
||||
T{ MAX-UINT MID-UINT MID-UINT WITHIN -> FALSE }T
|
||||
T{ MAX-UINT MID-UINT MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ MAX-UINT MID-UINT MAX-UINT WITHIN -> FALSE }T
|
||||
T{ MAX-UINT MID-UINT+1 0 WITHIN -> TRUE }T
|
||||
T{ MAX-UINT MID-UINT+1 MID-UINT WITHIN -> TRUE }T
|
||||
T{ MAX-UINT MID-UINT+1 MID-UINT+1 WITHIN -> FALSE }T
|
||||
T{ MAX-UINT MID-UINT+1 MAX-UINT WITHIN -> FALSE }T
|
||||
T{ MAX-UINT MAX-UINT 0 WITHIN -> TRUE }T
|
||||
T{ MAX-UINT MAX-UINT MID-UINT WITHIN -> TRUE }T
|
||||
T{ MAX-UINT MAX-UINT MID-UINT+1 WITHIN -> TRUE }T
|
||||
T{ MAX-UINT MAX-UINT MAX-UINT WITHIN -> FALSE }T
|
||||
|
||||
T{ MIN-INT MIN-INT MIN-INT WITHIN -> FALSE }T
|
||||
T{ MIN-INT MIN-INT 0 WITHIN -> TRUE }T
|
||||
T{ MIN-INT MIN-INT 1 WITHIN -> TRUE }T
|
||||
T{ MIN-INT MIN-INT MAX-INT WITHIN -> TRUE }T
|
||||
T{ MIN-INT 0 MIN-INT WITHIN -> FALSE }T
|
||||
T{ MIN-INT 0 0 WITHIN -> FALSE }T
|
||||
T{ MIN-INT 0 1 WITHIN -> FALSE }T
|
||||
T{ MIN-INT 0 MAX-INT WITHIN -> FALSE }T
|
||||
T{ MIN-INT 1 MIN-INT WITHIN -> FALSE }T
|
||||
T{ MIN-INT 1 0 WITHIN -> TRUE }T
|
||||
T{ MIN-INT 1 1 WITHIN -> FALSE }T
|
||||
T{ MIN-INT 1 MAX-INT WITHIN -> FALSE }T
|
||||
T{ MIN-INT MAX-INT MIN-INT WITHIN -> FALSE }T
|
||||
T{ MIN-INT MAX-INT 0 WITHIN -> TRUE }T
|
||||
T{ MIN-INT MAX-INT 1 WITHIN -> TRUE }T
|
||||
T{ MIN-INT MAX-INT MAX-INT WITHIN -> FALSE }T
|
||||
T{ 0 MIN-INT MIN-INT WITHIN -> FALSE }T
|
||||
T{ 0 MIN-INT 0 WITHIN -> FALSE }T
|
||||
T{ 0 MIN-INT 1 WITHIN -> TRUE }T
|
||||
T{ 0 MIN-INT MAX-INT WITHIN -> TRUE }T
|
||||
T{ 0 0 MIN-INT WITHIN -> TRUE }T
|
||||
T{ 0 0 0 WITHIN -> FALSE }T
|
||||
T{ 0 0 1 WITHIN -> TRUE }T
|
||||
T{ 0 0 MAX-INT WITHIN -> TRUE }T
|
||||
T{ 0 1 MIN-INT WITHIN -> FALSE }T
|
||||
T{ 0 1 0 WITHIN -> FALSE }T
|
||||
T{ 0 1 1 WITHIN -> FALSE }T
|
||||
T{ 0 1 MAX-INT WITHIN -> FALSE }T
|
||||
T{ 0 MAX-INT MIN-INT WITHIN -> FALSE }T
|
||||
T{ 0 MAX-INT 0 WITHIN -> FALSE }T
|
||||
T{ 0 MAX-INT 1 WITHIN -> TRUE }T
|
||||
T{ 0 MAX-INT MAX-INT WITHIN -> FALSE }T
|
||||
T{ 1 MIN-INT MIN-INT WITHIN -> FALSE }T
|
||||
T{ 1 MIN-INT 0 WITHIN -> FALSE }T
|
||||
T{ 1 MIN-INT 1 WITHIN -> FALSE }T
|
||||
T{ 1 MIN-INT MAX-INT WITHIN -> TRUE }T
|
||||
T{ 1 0 MIN-INT WITHIN -> TRUE }T
|
||||
T{ 1 0 0 WITHIN -> FALSE }T
|
||||
T{ 1 0 1 WITHIN -> FALSE }T
|
||||
T{ 1 0 MAX-INT WITHIN -> TRUE }T
|
||||
T{ 1 1 MIN-INT WITHIN -> TRUE }T
|
||||
T{ 1 1 0 WITHIN -> TRUE }T
|
||||
T{ 1 1 1 WITHIN -> FALSE }T
|
||||
T{ 1 1 MAX-INT WITHIN -> TRUE }T
|
||||
T{ 1 MAX-INT MIN-INT WITHIN -> FALSE }T
|
||||
T{ 1 MAX-INT 0 WITHIN -> FALSE }T
|
||||
T{ 1 MAX-INT 1 WITHIN -> FALSE }T
|
||||
T{ 1 MAX-INT MAX-INT WITHIN -> FALSE }T
|
||||
T{ MAX-INT MIN-INT MIN-INT WITHIN -> FALSE }T
|
||||
T{ MAX-INT MIN-INT 0 WITHIN -> FALSE }T
|
||||
T{ MAX-INT MIN-INT 1 WITHIN -> FALSE }T
|
||||
T{ MAX-INT MIN-INT MAX-INT WITHIN -> FALSE }T
|
||||
T{ MAX-INT 0 MIN-INT WITHIN -> TRUE }T
|
||||
T{ MAX-INT 0 0 WITHIN -> FALSE }T
|
||||
T{ MAX-INT 0 1 WITHIN -> FALSE }T
|
||||
T{ MAX-INT 0 MAX-INT WITHIN -> FALSE }T
|
||||
T{ MAX-INT 1 MIN-INT WITHIN -> TRUE }T
|
||||
T{ MAX-INT 1 0 WITHIN -> TRUE }T
|
||||
T{ MAX-INT 1 1 WITHIN -> FALSE }T
|
||||
T{ MAX-INT 1 MAX-INT WITHIN -> FALSE }T
|
||||
T{ MAX-INT MAX-INT MIN-INT WITHIN -> TRUE }T
|
||||
T{ MAX-INT MAX-INT 0 WITHIN -> TRUE }T
|
||||
T{ MAX-INT MAX-INT 1 WITHIN -> TRUE }T
|
||||
T{ MAX-INT MAX-INT MAX-INT WITHIN -> FALSE }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING UNUSED (contributed by James Bowman & Peter Knaggs)
|
||||
|
||||
VARIABLE UNUSED0
|
||||
T{ UNUSED DROP -> }T
|
||||
T{ ALIGN UNUSED UNUSED0 ! 0 , UNUSED CELL+ UNUSED0 @ = -> TRUE }T
|
||||
T{ UNUSED UNUSED0 ! 0 C, UNUSED CHAR+ UNUSED0 @ =
|
||||
-> TRUE }T \ aligned -> unaligned
|
||||
T{ UNUSED UNUSED0 ! 0 C, UNUSED CHAR+ UNUSED0 @ = -> TRUE }T \ unaligned -> ?
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING AGAIN (contributed by James Bowman)
|
||||
|
||||
T{ : AG0 701 BEGIN DUP 7 MOD 0= IF EXIT THEN 1+ AGAIN ; -> }T
|
||||
T{ AG0 -> 707 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING MARKER (contributed by James Bowman)
|
||||
|
||||
T{ : MA? BL WORD FIND NIP 0<> ; -> }T
|
||||
T{ MARKER MA0 -> }T
|
||||
T{ : MA1 111 ; -> }T
|
||||
T{ MARKER MA2 -> }T
|
||||
T{ : MA1 222 ; -> }T
|
||||
T{ MA? MA0 MA? MA1 MA? MA2 -> TRUE TRUE TRUE }T
|
||||
T{ MA1 MA2 MA1 -> 222 111 }T
|
||||
T{ MA? MA0 MA? MA1 MA? MA2 -> TRUE TRUE FALSE }T
|
||||
T{ MA0 -> }T
|
||||
T{ MA? MA0 MA? MA1 MA? MA2 -> FALSE FALSE FALSE }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING ?DO
|
||||
|
||||
: QD ?DO I LOOP ;
|
||||
T{ 789 789 QD -> }T
|
||||
T{ -9876 -9876 QD -> }T
|
||||
T{ 5 0 QD -> 0 1 2 3 4 }T
|
||||
|
||||
: QD1 ?DO I 10 +LOOP ;
|
||||
T{ 50 1 QD1 -> 1 11 21 31 41 }T
|
||||
T{ 50 0 QD1 -> 0 10 20 30 40 }T
|
||||
|
||||
: QD2 ?DO I 3 > IF LEAVE ELSE I THEN LOOP ;
|
||||
T{ 5 -1 QD2 -> -1 0 1 2 3 }T
|
||||
|
||||
: QD3 ?DO I 1 +LOOP ;
|
||||
T{ 4 4 QD3 -> }T
|
||||
T{ 4 1 QD3 -> 1 2 3 }T
|
||||
T{ 2 -1 QD3 -> -1 0 1 }T
|
||||
|
||||
: QD4 ?DO I -1 +LOOP ;
|
||||
T{ 4 4 QD4 -> }T
|
||||
T{ 1 4 QD4 -> 4 3 2 1 }T
|
||||
T{ -1 2 QD4 -> 2 1 0 -1 }T
|
||||
|
||||
: QD5 ?DO I -10 +LOOP ;
|
||||
T{ 1 50 QD5 -> 50 40 30 20 10 }T
|
||||
T{ 0 50 QD5 -> 50 40 30 20 10 0 }T
|
||||
T{ -25 10 QD5 -> 10 0 -10 -20 }T
|
||||
|
||||
VARIABLE ITERS
|
||||
VARIABLE INCRMNT
|
||||
|
||||
: QD6 ( limit start increment -- )
|
||||
INCRMNT !
|
||||
0 ITERS !
|
||||
?DO
|
||||
1 ITERS +!
|
||||
I
|
||||
ITERS @ 6 = IF LEAVE THEN
|
||||
INCRMNT @
|
||||
+LOOP ITERS @
|
||||
;
|
||||
|
||||
T{ 4 4 -1 QD6 -> 0 }T
|
||||
T{ 1 4 -1 QD6 -> 4 3 2 1 4 }T
|
||||
T{ 4 1 -1 QD6 -> 1 0 -1 -2 -3 -4 6 }T
|
||||
T{ 4 1 0 QD6 -> 1 1 1 1 1 1 6 }T
|
||||
T{ 0 0 0 QD6 -> 0 }T
|
||||
T{ 1 4 0 QD6 -> 4 4 4 4 4 4 6 }T
|
||||
T{ 1 4 1 QD6 -> 4 5 6 7 8 9 6 }T
|
||||
T{ 4 1 1 QD6 -> 1 2 3 3 }T
|
||||
T{ 4 4 1 QD6 -> 0 }T
|
||||
T{ 2 -1 -1 QD6 -> -1 -2 -3 -4 -5 -6 6 }T
|
||||
T{ -1 2 -1 QD6 -> 2 1 0 -1 4 }T
|
||||
T{ 2 -1 0 QD6 -> -1 -1 -1 -1 -1 -1 6 }T
|
||||
T{ -1 2 0 QD6 -> 2 2 2 2 2 2 6 }T
|
||||
T{ -1 2 1 QD6 -> 2 3 4 5 6 7 6 }T
|
||||
T{ 2 -1 1 QD6 -> -1 0 1 3 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING BUFFER:
|
||||
|
||||
T{ 2 CELLS BUFFER: BUF:TEST -> }T
|
||||
T{ BUF:TEST DUP ALIGNED = -> TRUE }T
|
||||
T{ 111 BUF:TEST ! 222 BUF:TEST CELL+ ! -> }T
|
||||
T{ BUF:TEST @ BUF:TEST CELL+ @ -> 111 222 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING VALUE TO
|
||||
|
||||
T{ 111 VALUE VAL1 -999 VALUE VAL2 -> }T
|
||||
T{ VAL1 -> 111 }T
|
||||
T{ VAL2 -> -999 }T
|
||||
T{ 222 TO VAL1 -> }T
|
||||
T{ VAL1 -> 222 }T
|
||||
T{ : VD1 VAL1 ; -> }T
|
||||
T{ VD1 -> 222 }T
|
||||
T{ : VD2 TO VAL2 ; -> }T
|
||||
T{ VAL2 -> -999 }T
|
||||
T{ -333 VD2 -> }T
|
||||
T{ VAL2 -> -333 }T
|
||||
T{ VAL1 -> 222 }T
|
||||
T{ 444 TO VAL1 -> }T
|
||||
T{ VD1 -> 444 }T
|
||||
T{ 123 VALUE VAL3 IMMEDIATE VAL3 -> 123 }T
|
||||
T{ : VD3 VAL3 LITERAL ; VD3 -> 123 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING CASE OF ENDOF ENDCASE
|
||||
|
||||
: CS1 CASE 1 OF 111 ENDOF
|
||||
2 OF 222 ENDOF
|
||||
3 OF 333 ENDOF
|
||||
>R 999 R>
|
||||
ENDCASE
|
||||
;
|
||||
|
||||
T{ 1 CS1 -> 111 }T
|
||||
T{ 2 CS1 -> 222 }T
|
||||
T{ 3 CS1 -> 333 }T
|
||||
T{ 4 CS1 -> 999 }T
|
||||
|
||||
\ Nested CASE's
|
||||
|
||||
: CS2 >R CASE -1 OF CASE R@ 1 OF 100 ENDOF
|
||||
2 OF 200 ENDOF
|
||||
>R -300 R>
|
||||
ENDCASE
|
||||
ENDOF
|
||||
-2 OF CASE R@ 1 OF -99 ENDOF
|
||||
>R -199 R>
|
||||
ENDCASE
|
||||
ENDOF
|
||||
>R 299 R>
|
||||
ENDCASE R> DROP
|
||||
;
|
||||
|
||||
T{ -1 1 CS2 -> 100 }T
|
||||
T{ -1 2 CS2 -> 200 }T
|
||||
T{ -1 3 CS2 -> -300 }T
|
||||
T{ -2 1 CS2 -> -99 }T
|
||||
T{ -2 2 CS2 -> -199 }T
|
||||
T{ 0 2 CS2 -> 299 }T
|
||||
|
||||
\ Boolean short circuiting using CASE
|
||||
|
||||
: CS3 ( N1 -- N2 )
|
||||
CASE 1- FALSE OF 11 ENDOF
|
||||
1- FALSE OF 22 ENDOF
|
||||
1- FALSE OF 33 ENDOF
|
||||
44 SWAP
|
||||
ENDCASE
|
||||
;
|
||||
|
||||
T{ 1 CS3 -> 11 }T
|
||||
T{ 2 CS3 -> 22 }T
|
||||
T{ 3 CS3 -> 33 }T
|
||||
T{ 9 CS3 -> 44 }T
|
||||
|
||||
\ Empty CASE statements with/without default
|
||||
|
||||
T{ : CS4 CASE ENDCASE ; 1 CS4 -> }T
|
||||
T{ : CS5 CASE 2 SWAP ENDCASE ; 1 CS5 -> 2 }T
|
||||
T{ : CS6 CASE 1 OF ENDOF 2 ENDCASE ; 1 CS6 -> }T
|
||||
T{ : CS7 CASE 3 OF ENDOF 2 ENDCASE ; 1 CS7 -> 1 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING :NONAME RECURSE
|
||||
|
||||
VARIABLE NN1
|
||||
VARIABLE NN2
|
||||
:NONAME 1234 ; NN1 !
|
||||
:NONAME 9876 ; NN2 !
|
||||
T{ NN1 @ EXECUTE -> 1234 }T
|
||||
T{ NN2 @ EXECUTE -> 9876 }T
|
||||
|
||||
T{ :NONAME ( n -- 0,1,..n ) DUP IF DUP >R 1- RECURSE R> THEN ;
|
||||
CONSTANT RN1 -> }T
|
||||
T{ 0 RN1 EXECUTE -> 0 }T
|
||||
T{ 4 RN1 EXECUTE -> 0 1 2 3 4 }T
|
||||
|
||||
:NONAME ( n -- n1 ) \ Multiple RECURSEs in one definition
|
||||
1- DUP
|
||||
CASE 0 OF EXIT ENDOF
|
||||
1 OF 11 SWAP RECURSE ENDOF
|
||||
2 OF 22 SWAP RECURSE ENDOF
|
||||
3 OF 33 SWAP RECURSE ENDOF
|
||||
DROP ABS RECURSE EXIT
|
||||
ENDCASE
|
||||
; CONSTANT RN2
|
||||
|
||||
T{ 1 RN2 EXECUTE -> 0 }T
|
||||
T{ 2 RN2 EXECUTE -> 11 0 }T
|
||||
T{ 4 RN2 EXECUTE -> 33 22 11 0 }T
|
||||
T{ 25 RN2 EXECUTE -> 33 22 11 0 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING C"
|
||||
|
||||
T{ : CQ1 C" 123" ; -> }T
|
||||
T{ CQ1 COUNT EVALUATE -> 123 }T
|
||||
T{ : CQ2 C" " ; -> }T
|
||||
T{ CQ2 COUNT EVALUATE -> }T
|
||||
T{ : CQ3 C" 2345"COUNT EVALUATE ; CQ3 -> 2345 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING COMPILE,
|
||||
|
||||
:NONAME DUP + ; CONSTANT DUP+
|
||||
T{ : Q DUP+ COMPILE, ; -> }T
|
||||
T{ : AS1 [ Q ] ; -> }T
|
||||
T{ 123 AS1 -> 246 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
\ Cannot automatically test SAVE-INPUT and RESTORE-INPUT from a console source
|
||||
|
||||
TESTING SAVE-INPUT and RESTORE-INPUT with a string source
|
||||
|
||||
VARIABLE SI_INC 0 SI_INC !
|
||||
|
||||
: SI1
|
||||
SI_INC @ >IN +!
|
||||
15 SI_INC !
|
||||
;
|
||||
|
||||
: S$ S" SAVE-INPUT SI1 RESTORE-INPUT 12345" ;
|
||||
|
||||
T{ S$ EVALUATE SI_INC @ -> 0 2345 15 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING .(
|
||||
|
||||
CR CR .( Output from .()
|
||||
T{ CR .( You should see -9876: ) -9876 . -> }T
|
||||
T{ CR .( and again: ).( -9876)CR -> }T
|
||||
|
||||
CR CR .( On the next 2 lines you should see First then Second messages:)
|
||||
T{ : DOTP CR ." Second message via ." [CHAR] " EMIT \ Check .( is immediate
|
||||
[ CR ] .( First message via .( ) ; DOTP -> }T
|
||||
CR CR
|
||||
T{ : IMM? BL WORD FIND NIP ; IMM? .( -> 1 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING .R and U.R - has to handle different cell sizes
|
||||
|
||||
\ Create some large integers just below/above MAX and Min INTs
|
||||
MAX-INT 73 79 */ CONSTANT LI1
|
||||
MIN-INT 71 73 */ CONSTANT LI2
|
||||
|
||||
LI1 0 <# #S #> NIP CONSTANT LENLI1
|
||||
|
||||
: (.R&U.R) ( u1 u2 -- ) \ u1 <= string length, u2 is required indentation
|
||||
TUCK + >R
|
||||
LI1 OVER SPACES . CR R@ LI1 SWAP .R CR
|
||||
LI2 OVER SPACES . CR R@ 1+ LI2 SWAP .R CR
|
||||
LI1 OVER SPACES U. CR R@ LI1 SWAP U.R CR
|
||||
LI2 SWAP SPACES U. CR R> LI2 SWAP U.R CR
|
||||
;
|
||||
|
||||
: .R&U.R ( -- )
|
||||
CR ." You should see lines duplicated:" CR
|
||||
." indented by 0 spaces" CR 0 0 (.R&U.R) CR
|
||||
." indented by 0 spaces" CR LENLI1 0 (.R&U.R) CR \ Just fits required width
|
||||
." indented by 5 spaces" CR LENLI1 5 (.R&U.R) CR
|
||||
;
|
||||
|
||||
CR CR .( Output from .R and U.R)
|
||||
T{ .R&U.R -> }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING PAD ERASE
|
||||
\ Must handle different size characters i.e. 1 CHARS >= 1
|
||||
|
||||
84 CONSTANT CHARS/PAD \ Minimum size of PAD in chars
|
||||
CHARS/PAD CHARS CONSTANT AUS/PAD
|
||||
: CHECKPAD ( caddr u ch -- f ) \ f = TRUE if u chars = ch
|
||||
SWAP 0
|
||||
?DO
|
||||
OVER I CHARS + C@ OVER <>
|
||||
IF 2DROP UNLOOP FALSE EXIT THEN
|
||||
LOOP
|
||||
2DROP TRUE
|
||||
;
|
||||
|
||||
T{ PAD DROP -> }T
|
||||
T{ 0 INVERT PAD C! -> }T
|
||||
T{ PAD C@ CONSTANT MAXCHAR -> }T
|
||||
T{ PAD CHARS/PAD 2DUP MAXCHAR FILL MAXCHAR CHECKPAD -> TRUE }T
|
||||
T{ PAD CHARS/PAD 2DUP CHARS ERASE 0 CHECKPAD -> TRUE }T
|
||||
T{ PAD CHARS/PAD 2DUP MAXCHAR FILL PAD 0 ERASE MAXCHAR CHECKPAD -> TRUE }T
|
||||
T{ PAD 43 CHARS + 9 CHARS ERASE -> }T
|
||||
T{ PAD 43 MAXCHAR CHECKPAD -> TRUE }T
|
||||
T{ PAD 43 CHARS + 9 0 CHECKPAD -> TRUE }T
|
||||
T{ PAD 52 CHARS + CHARS/PAD 52 - MAXCHAR CHECKPAD -> TRUE }T
|
||||
|
||||
\ Check that use of WORD and pictured numeric output do not corrupt PAD
|
||||
\ Minimum size of buffers for these are 33 chars and (2*n)+2 chars respectively
|
||||
\ where n is number of bits per cell
|
||||
|
||||
PAD CHARS/PAD ERASE
|
||||
2 BASE !
|
||||
MAX-UINT MAX-UINT <# #S CHAR 1 DUP HOLD HOLD #> 2DROP
|
||||
DECIMAL
|
||||
BL WORD 12345678123456781234567812345678 DROP
|
||||
T{ PAD CHARS/PAD 0 CHECKPAD -> TRUE }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING PARSE
|
||||
|
||||
T{ CHAR | PARSE 1234| DUP ROT ROT EVALUATE -> 4 1234 }T
|
||||
T{ CHAR ^ PARSE 23 45 ^ DUP ROT ROT EVALUATE -> 7 23 45 }T
|
||||
: PA1 [CHAR] $ PARSE DUP >R PAD SWAP CHARS MOVE PAD R> ;
|
||||
T{ PA1 3456
|
||||
DUP ROT ROT EVALUATE -> 4 3456 }T
|
||||
T{ CHAR A PARSE A SWAP DROP -> 0 }T
|
||||
T{ CHAR Z PARSE
|
||||
SWAP DROP -> 0 }T
|
||||
T{ CHAR " PARSE 4567 "DUP ROT ROT EVALUATE -> 5 4567 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING PARSE-NAME (Forth 2012)
|
||||
\ Adapted from the PARSE-NAME RfD tests
|
||||
|
||||
T{ PARSE-NAME abcd STR1 S= -> TRUE }T \ No leading spaces
|
||||
T{ PARSE-NAME abcde STR2 S= -> TRUE }T \ Leading spaces
|
||||
|
||||
\ Test empty parse area, new lines are necessary
|
||||
T{ PARSE-NAME
|
||||
NIP -> 0 }T
|
||||
\ Empty parse area with spaces after PARSE-NAME
|
||||
T{ PARSE-NAME
|
||||
NIP -> 0 }T
|
||||
|
||||
T{ : PARSE-NAME-TEST ( "name1" "name2" -- n )
|
||||
PARSE-NAME PARSE-NAME S= ; -> }T
|
||||
T{ PARSE-NAME-TEST abcd abcd -> TRUE }T
|
||||
T{ PARSE-NAME-TEST abcd abcd -> TRUE }T \ Leading spaces
|
||||
T{ PARSE-NAME-TEST abcde abcdf -> FALSE }T
|
||||
T{ PARSE-NAME-TEST abcdf abcde -> FALSE }T
|
||||
T{ PARSE-NAME-TEST abcde abcde
|
||||
-> TRUE }T \ Parse to end of line
|
||||
T{ PARSE-NAME-TEST abcde abcde
|
||||
-> TRUE }T \ Leading and trailing spaces
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING DEFER DEFER@ DEFER! IS ACTION-OF (Forth 2012)
|
||||
\ Adapted from the Forth 200X RfD tests
|
||||
|
||||
T{ DEFER DEFER1 -> }T
|
||||
T{ : MY-DEFER DEFER ; -> }T
|
||||
T{ : IS-DEFER1 IS DEFER1 ; -> }T
|
||||
T{ : ACTION-DEFER1 ACTION-OF DEFER1 ; -> }T
|
||||
T{ : DEF! DEFER! ; -> }T
|
||||
T{ : DEF@ DEFER@ ; -> }T
|
||||
|
||||
T{ ' * ' DEFER1 DEFER! -> }T
|
||||
T{ 2 3 DEFER1 -> 6 }T
|
||||
T{ ' DEFER1 DEFER@ -> ' * }T
|
||||
T{ ' DEFER1 DEF@ -> ' * }T
|
||||
T{ ACTION-OF DEFER1 -> ' * }T
|
||||
T{ ACTION-DEFER1 -> ' * }T
|
||||
T{ ' + IS DEFER1 -> }T
|
||||
T{ 1 2 DEFER1 -> 3 }T
|
||||
T{ ' DEFER1 DEFER@ -> ' + }T
|
||||
T{ ' DEFER1 DEF@ -> ' + }T
|
||||
T{ ACTION-OF DEFER1 -> ' + }T
|
||||
T{ ACTION-DEFER1 -> ' + }T
|
||||
T{ ' - IS-DEFER1 -> }T
|
||||
T{ 1 2 DEFER1 -> -1 }T
|
||||
T{ ' DEFER1 DEFER@ -> ' - }T
|
||||
T{ ' DEFER1 DEF@ -> ' - }T
|
||||
T{ ACTION-OF DEFER1 -> ' - }T
|
||||
T{ ACTION-DEFER1 -> ' - }T
|
||||
|
||||
T{ MY-DEFER DEFER2 -> }T
|
||||
T{ ' DUP IS DEFER2 -> }T
|
||||
T{ 1 DEFER2 -> 1 1 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING HOLDS (Forth 2012)
|
||||
|
||||
: HTEST S" Testing HOLDS" ;
|
||||
: HTEST2 S" works" ;
|
||||
: HTEST3 S" Testing HOLDS works 123" ;
|
||||
T{ 0 0 <# HTEST HOLDS #> HTEST S= -> TRUE }T
|
||||
T{ 123 0 <# #S BL HOLD HTEST2 HOLDS BL HOLD HTEST HOLDS #>
|
||||
HTEST3 S= -> TRUE }T
|
||||
T{ : HLD HOLDS ; -> }T
|
||||
T{ 0 0 <# HTEST HLD #> HTEST S= -> TRUE }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
TESTING REFILL SOURCE-ID
|
||||
\ REFILL and SOURCE-ID from the user input device can't be tested from a file,
|
||||
\ can only be tested from a string via EVALUATE
|
||||
|
||||
T{ : RF1 S" REFILL" EVALUATE ; RF1 -> FALSE }T
|
||||
T{ : SID1 S" SOURCE-ID" EVALUATE ; SID1 -> -1 }T
|
||||
|
||||
\ ------------------------------------------------------------------------------
|
||||
TESTING S\" (Forth 2012 compilation mode)
|
||||
\ Extended the Forth 200X RfD tests
|
||||
\ Note this tests the Core Ext definition of S\" which has unedfined
|
||||
\ interpretation semantics. S\" in interpretation mode is tested in the tests on
|
||||
\ the File-Access word set
|
||||
|
||||
T{ : SSQ1 S\" abc" S" abc" S= ; -> }T \ No escapes
|
||||
T{ SSQ1 -> TRUE }T
|
||||
T{ : SSQ2 S\" " ; SSQ2 SWAP DROP -> 0 }T \ Empty string
|
||||
|
||||
T{ : SSQ3 S\" \a\b\e\f\l\m\q\r\t\v\x0F0\x1Fa\xaBx\z\"\\" ; -> }T
|
||||
T{ SSQ3 SWAP DROP -> 20 }T \ String length
|
||||
T{ SSQ3 DROP C@ -> 7 }T \ \a BEL Bell
|
||||
T{ SSQ3 DROP 1 CHARS + C@ -> 8 }T \ \b BS Backspace
|
||||
T{ SSQ3 DROP 2 CHARS + C@ -> 27 }T \ \e ESC Escape
|
||||
T{ SSQ3 DROP 3 CHARS + C@ -> 12 }T \ \f FF Form feed
|
||||
T{ SSQ3 DROP 4 CHARS + C@ -> 10 }T \ \l LF Line feed
|
||||
T{ SSQ3 DROP 5 CHARS + C@ -> 13 }T \ \m CR of CR/LF pair
|
||||
T{ SSQ3 DROP 6 CHARS + C@ -> 10 }T \ LF of CR/LF pair
|
||||
T{ SSQ3 DROP 7 CHARS + C@ -> 34 }T \ \q " Double Quote
|
||||
T{ SSQ3 DROP 8 CHARS + C@ -> 13 }T \ \r CR Carriage Return
|
||||
T{ SSQ3 DROP 9 CHARS + C@ -> 9 }T \ \t TAB Horizontal Tab
|
||||
T{ SSQ3 DROP 10 CHARS + C@ -> 11 }T \ \v VT Vertical Tab
|
||||
T{ SSQ3 DROP 11 CHARS + C@ -> 15 }T \ \x0F Given Char
|
||||
T{ SSQ3 DROP 12 CHARS + C@ -> 48 }T \ 0 0 Digit follow on
|
||||
T{ SSQ3 DROP 13 CHARS + C@ -> 31 }T \ \x1F Given Char
|
||||
T{ SSQ3 DROP 14 CHARS + C@ -> 97 }T \ a a Hex follow on
|
||||
T{ SSQ3 DROP 15 CHARS + C@ -> 171 }T \ \xaB Insensitive Given Char
|
||||
T{ SSQ3 DROP 16 CHARS + C@ -> 120 }T \ x x Non hex follow on
|
||||
T{ SSQ3 DROP 17 CHARS + C@ -> 0 }T \ \z NUL No Character
|
||||
T{ SSQ3 DROP 18 CHARS + C@ -> 34 }T \ \" " Double Quote
|
||||
T{ SSQ3 DROP 19 CHARS + C@ -> 92 }T \ \\ \ Back Slash
|
||||
|
||||
\ The above does not test \n as this is a system dependent value.
|
||||
\ Check it displays a new line
|
||||
CR .( The next test should display:)
|
||||
CR .( One line...)
|
||||
CR .( another line)
|
||||
T{ : SSQ4 S\" \nOne line...\nanotherLine\n" TYPE ; SSQ4 -> }T
|
||||
|
||||
\ Test bare escapable characters appear as themselves
|
||||
T{ : SSQ5 S\" abeflmnqrtvxz" S" abeflmnqrtvxz" S= ; SSQ5 -> TRUE }T
|
||||
|
||||
T{ : SSQ6 S\" a\""2DROP 1111 ; SSQ6 -> 1111 }T \ Parsing behaviour
|
||||
|
||||
T{ : SSQ7 S\" 111 : SSQ8 S\\\" 222\" EVALUATE ; SSQ8 333" EVALUATE ; -> }T
|
||||
T{ SSQ7 -> 111 222 333 }T
|
||||
T{ : SSQ9 S\" 11 : SSQ10 S\\\" \\x32\\x32\" EVALUATE ; SSQ10 33" EVALUATE ; -> }T
|
||||
T{ SSQ9 -> 11 22 33 }T
|
||||
|
||||
\ -----------------------------------------------------------------------------
|
||||
CORE-EXT-ERRORS SET-ERROR-COUNT
|
||||
|
||||
CR .( End of Core Extension word tests) CR
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
\ From: John Hayes S1I
|
||||
\ Subject: tester.fr
|
||||
\ Date: Mon, 27 Nov 95 13:10:09 PST
|
||||
|
||||
\ (C) 1995 JOHNS HOPKINS UNIVERSITY / APPLIED PHYSICS LABORATORY
|
||||
\ MAY BE DISTRIBUTED FREELY AS LONG AS THIS COPYRIGHT NOTICE REMAINS.
|
||||
\ VERSION 1.2
|
||||
|
||||
\ 24/11/2015 Replaced Core Ext word <> with = 0=
|
||||
\ 31/3/2015 Variable #ERRORS added and incremented for each error reported.
|
||||
\ 22/1/09 The words { and } have been changed to T{ and }T respectively to
|
||||
\ agree with the Forth 200X file ttester.fs. This avoids clashes with
|
||||
\ locals using { ... } and the FSL use of }
|
||||
|
||||
HEX
|
||||
|
||||
\ SET THE FOLLOWING FLAG TO TRUE FOR MORE VERBOSE OUTPUT; THIS MAY
|
||||
\ ALLOW YOU TO TELL WHICH TEST CAUSED YOUR SYSTEM TO HANG.
|
||||
VARIABLE VERBOSE
|
||||
FALSE VERBOSE !
|
||||
\ TRUE VERBOSE !
|
||||
|
||||
: EMPTY-STACK \ ( ... -- ) EMPTY STACK: HANDLES UNDERFLOWED STACK TOO.
|
||||
DEPTH ?DUP IF DUP 0< IF NEGATE 0 DO 0 LOOP ELSE 0 DO DROP LOOP THEN THEN ;
|
||||
|
||||
VARIABLE #ERRORS 0 #ERRORS !
|
||||
|
||||
: ERROR \ ( C-ADDR U -- ) DISPLAY AN ERROR MESSAGE FOLLOWED BY
|
||||
\ THE LINE THAT HAD THE ERROR.
|
||||
CR TYPE SOURCE TYPE \ DISPLAY LINE CORRESPONDING TO ERROR
|
||||
EMPTY-STACK \ THROW AWAY EVERY THING ELSE
|
||||
#ERRORS @ 1 + #ERRORS !
|
||||
\ QUIT \ *** Uncomment this line to QUIT on an error
|
||||
;
|
||||
|
||||
VARIABLE ACTUAL-DEPTH \ STACK RECORD
|
||||
CREATE ACTUAL-RESULTS 20 CELLS ALLOT
|
||||
|
||||
: T{ \ ( -- ) SYNTACTIC SUGAR.
|
||||
;
|
||||
|
||||
: -> \ ( ... -- ) RECORD DEPTH AND CONTENT OF STACK.
|
||||
DEPTH DUP ACTUAL-DEPTH ! \ RECORD DEPTH
|
||||
?DUP IF \ IF THERE IS SOMETHING ON STACK
|
||||
0 DO ACTUAL-RESULTS I CELLS + ! LOOP \ SAVE THEM
|
||||
THEN ;
|
||||
|
||||
: }T \ ( ... -- ) COMPARE STACK (EXPECTED) CONTENTS WITH SAVED
|
||||
\ (ACTUAL) CONTENTS.
|
||||
DEPTH ACTUAL-DEPTH @ = IF \ IF DEPTHS MATCH
|
||||
DEPTH ?DUP IF \ IF THERE IS SOMETHING ON THE STACK
|
||||
0 DO \ FOR EACH STACK ITEM
|
||||
ACTUAL-RESULTS I CELLS + @ \ COMPARE ACTUAL WITH EXPECTED
|
||||
= 0= IF S" INCORRECT RESULT: " ERROR LEAVE THEN
|
||||
LOOP
|
||||
THEN
|
||||
ELSE \ DEPTH MISMATCH
|
||||
S" WRONG NUMBER OF RESULTS: " ERROR
|
||||
THEN ;
|
||||
|
||||
: TESTING \ ( -- ) TALKING COMMENT.
|
||||
SOURCE VERBOSE @
|
||||
IF DUP >R TYPE CR R> >IN !
|
||||
ELSE >IN ! DROP [CHAR] * EMIT
|
||||
THEN ;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,170 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the Hayes/Gerry-Jackson Core conformance suite against our Forth
|
||||
# interpreter and emit scoreboard.json + scoreboard.md.
|
||||
#
|
||||
# Method:
|
||||
# 1. Preprocess lib/forth/ans-tests/core.fr — strip \ comments, ( ... )
|
||||
# comments, and TESTING … metadata lines.
|
||||
# 2. Split into chunks ending at each `}T` so an error in one test
|
||||
# chunk doesn't abort the run.
|
||||
# 3. Emit an SX file that exposes those chunks as a list.
|
||||
# 4. Run our Forth + hayes-runner under sx_server; record pass/fail/error.
|
||||
|
||||
set -e
|
||||
FORTH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$FORTH_DIR/../.." && pwd)"
|
||||
SX_SERVER="${SX_SERVER:-/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe}"
|
||||
SOURCE="$FORTH_DIR/ans-tests/core.fr"
|
||||
OUT_JSON="$FORTH_DIR/scoreboard.json"
|
||||
OUT_MD="$FORTH_DIR/scoreboard.md"
|
||||
TMP="$(mktemp -d)"
|
||||
PREPROC="$TMP/preproc.forth"
|
||||
CHUNKS_SX="$TMP/chunks.sx"
|
||||
|
||||
cd "$ROOT"
|
||||
|
||||
# 1. preprocess
|
||||
awk '
|
||||
{
|
||||
line = $0
|
||||
# protect POSTPONE \ so the comment-strip below leaves the literal \ alone
|
||||
gsub(/POSTPONE[ \t]+\\/, "POSTPONE @@BS@@", line)
|
||||
# strip leading/embedded \ line comments (must be \ followed by space or EOL)
|
||||
gsub(/(^|[ \t])\\([ \t].*|$)/, " ", line)
|
||||
# strip ( ... ) block comments that sit on one line
|
||||
gsub(/\([^)]*\)/, " ", line)
|
||||
# strip TESTING … metadata lines (rest of line, incl. bare TESTING)
|
||||
sub(/TESTING([ \t].*)?$/, " ", line)
|
||||
# restore the protected backslash
|
||||
gsub(/@@BS@@/, "\\", line)
|
||||
print line
|
||||
}' "$SOURCE" > "$PREPROC"
|
||||
|
||||
# 2 + 3: split into chunks at each `}T` and emit as a SX file
|
||||
#
|
||||
# Cap chunks via MAX_CHUNKS env (default 638 = full Hayes Core). Lower
|
||||
# it temporarily if later tests regress into an infinite loop while you
|
||||
# are iterating on primitives.
|
||||
MAX_CHUNKS="${MAX_CHUNKS:-638}"
|
||||
|
||||
MAX_CHUNKS="$MAX_CHUNKS" python3 - "$PREPROC" "$CHUNKS_SX" <<'PY'
|
||||
import os, re, sys
|
||||
preproc_path, out_path = sys.argv[1], sys.argv[2]
|
||||
max_chunks = int(os.environ.get("MAX_CHUNKS", "590"))
|
||||
text = open(preproc_path).read()
|
||||
# keep the `}T` attached to the preceding chunk
|
||||
parts = re.split(r'(\}T)', text)
|
||||
chunks = []
|
||||
buf = ""
|
||||
for p in parts:
|
||||
buf += p
|
||||
if p == "}T":
|
||||
s = buf.strip()
|
||||
if s:
|
||||
chunks.append(s)
|
||||
buf = ""
|
||||
if buf.strip():
|
||||
chunks.append(buf.strip())
|
||||
chunks = chunks[:max_chunks]
|
||||
|
||||
def esc(s):
|
||||
s = s.replace('\\', '\\\\').replace('"', '\\"')
|
||||
s = s.replace('\r', ' ').replace('\n', ' ')
|
||||
s = re.sub(r'\s+', ' ', s).strip()
|
||||
return s
|
||||
|
||||
with open(out_path, "w") as f:
|
||||
f.write("(define hayes-chunks (list\n")
|
||||
for c in chunks:
|
||||
f.write(' "' + esc(c) + '"\n')
|
||||
f.write("))\n\n")
|
||||
f.write("(define\n")
|
||||
f.write(" hayes-run-all\n")
|
||||
f.write(" (fn\n")
|
||||
f.write(" ()\n")
|
||||
f.write(" (hayes-reset!)\n")
|
||||
f.write(" (let ((s (hayes-boot)))\n")
|
||||
f.write(" (for-each (fn (c) (hayes-run-chunk s c)) hayes-chunks))\n")
|
||||
f.write(" (hayes-summary)))\n")
|
||||
PY
|
||||
|
||||
# 4. run it
|
||||
OUT=$(printf '(epoch 1)\n(load "lib/forth/runtime.sx")\n(epoch 2)\n(load "lib/forth/reader.sx")\n(epoch 3)\n(load "lib/forth/interpreter.sx")\n(epoch 4)\n(load "lib/forth/compiler.sx")\n(epoch 5)\n(load "lib/forth/hayes-runner.sx")\n(epoch 6)\n(load "%s")\n(epoch 7)\n(eval "(hayes-run-all)")\n' "$CHUNKS_SX" \
|
||||
| timeout 180 "$SX_SERVER" 2>&1)
|
||||
STATUS=$?
|
||||
|
||||
SUMMARY=$(printf '%s\n' "$OUT" | awk '/^\{:pass / {print; exit}')
|
||||
PASS=$(printf '%s' "$SUMMARY" | sed -n 's/.*:pass \([0-9-]*\).*/\1/p')
|
||||
FAIL=$(printf '%s' "$SUMMARY" | sed -n 's/.*:fail \([0-9-]*\).*/\1/p')
|
||||
ERR=$(printf '%s' "$SUMMARY" | sed -n 's/.*:error \([0-9-]*\).*/\1/p')
|
||||
TOTAL=$(printf '%s' "$SUMMARY" | sed -n 's/.*:total \([0-9-]*\).*/\1/p')
|
||||
CHUNK_COUNT=$(grep -c '^ "' "$CHUNKS_SX" || echo 0)
|
||||
TOTAL_AVAILABLE=$(grep -c '}T' "$PREPROC" || echo 0)
|
||||
|
||||
NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
|
||||
if [ -z "$PASS" ]; then
|
||||
PASS=0; FAIL=0; ERR=0; TOTAL=0
|
||||
NOTE="runner halted before completing (timeout or SX error)"
|
||||
else
|
||||
NOTE="completed"
|
||||
fi
|
||||
|
||||
PCT=0
|
||||
if [ "$TOTAL" -gt 0 ]; then
|
||||
PCT=$((PASS * 100 / TOTAL))
|
||||
fi
|
||||
|
||||
cat > "$OUT_JSON" <<JSON
|
||||
{
|
||||
"source": "gerryjackson/forth2012-test-suite src/core.fr",
|
||||
"generated_at": "$NOW",
|
||||
"chunks_available": $TOTAL_AVAILABLE,
|
||||
"chunks_fed": $CHUNK_COUNT,
|
||||
"total": $TOTAL,
|
||||
"pass": $PASS,
|
||||
"fail": $FAIL,
|
||||
"error": $ERR,
|
||||
"percent": $PCT,
|
||||
"note": "$NOTE"
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$OUT_MD" <<MD
|
||||
# Forth Hayes Core scoreboard
|
||||
|
||||
| metric | value |
|
||||
| ----------------- | ----: |
|
||||
| chunks available | $TOTAL_AVAILABLE |
|
||||
| chunks fed | $CHUNK_COUNT |
|
||||
| total | $TOTAL |
|
||||
| pass | $PASS |
|
||||
| fail | $FAIL |
|
||||
| error | $ERR |
|
||||
| percent | ${PCT}% |
|
||||
|
||||
- **Source**: \`gerryjackson/forth2012-test-suite\` \`src/core.fr\`
|
||||
- **Generated**: $NOW
|
||||
- **Note**: $NOTE
|
||||
|
||||
A "chunk" is any preprocessed segment ending at a \`}T\` (every Hayes test
|
||||
is one chunk, plus the small declaration blocks between tests).
|
||||
The runner catches raised errors at chunk boundaries so one bad chunk
|
||||
does not abort the rest. \`error\` covers chunks that raised; \`fail\`
|
||||
covers tests whose \`->\` / \`}T\` comparison mismatched.
|
||||
|
||||
### Chunk cap
|
||||
|
||||
\`conformance.sh\` processes the first \`\$MAX_CHUNKS\` chunks (default
|
||||
**638**, i.e. the whole Hayes Core file). Lower the cap temporarily
|
||||
while iterating on primitives if a regression re-opens an infinite
|
||||
loop in later tests.
|
||||
MD
|
||||
|
||||
echo "$SUMMARY"
|
||||
echo "Scoreboard: $OUT_JSON"
|
||||
echo " $OUT_MD"
|
||||
|
||||
if [ "$STATUS" -ne 0 ] && [ "$TOTAL" -eq 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,158 +0,0 @@
|
||||
;; Hayes conformance test runner.
|
||||
;; Installs T{ -> }T as Forth primitives that snapshot and compare dstack,
|
||||
;; plus stub TESTING / HEX / DECIMAL so the Hayes Core file can stream
|
||||
;; through the interpreter without halting on unsupported metadata words.
|
||||
|
||||
(define hayes-pass 0)
|
||||
(define hayes-fail 0)
|
||||
(define hayes-error 0)
|
||||
(define hayes-start-depth 0)
|
||||
(define hayes-actual (list))
|
||||
(define hayes-actual-set false)
|
||||
(define hayes-failures (list))
|
||||
(define hayes-first-error "")
|
||||
(define hayes-error-hist (dict))
|
||||
|
||||
(define
|
||||
hayes-reset!
|
||||
(fn
|
||||
()
|
||||
(set! hayes-pass 0)
|
||||
(set! hayes-fail 0)
|
||||
(set! hayes-error 0)
|
||||
(set! hayes-start-depth 0)
|
||||
(set! hayes-actual (list))
|
||||
(set! hayes-actual-set false)
|
||||
(set! hayes-failures (list))
|
||||
(set! hayes-first-error "")
|
||||
(set! hayes-error-hist (dict))))
|
||||
|
||||
(define
|
||||
hayes-slice
|
||||
(fn
|
||||
(state base)
|
||||
(let
|
||||
((n (- (forth-depth state) base)))
|
||||
(if (<= n 0) (list) (take (get state "dstack") n)))))
|
||||
|
||||
(define
|
||||
hayes-truncate!
|
||||
(fn
|
||||
(state base)
|
||||
(let
|
||||
((n (- (forth-depth state) base)))
|
||||
(when (> n 0) (dict-set! state "dstack" (drop (get state "dstack") n))))))
|
||||
|
||||
(define
|
||||
hayes-install!
|
||||
(fn
|
||||
(state)
|
||||
(forth-def-prim!
|
||||
state
|
||||
"T{"
|
||||
(fn
|
||||
(s)
|
||||
(set! hayes-start-depth (forth-depth s))
|
||||
(set! hayes-actual-set false)
|
||||
(set! hayes-actual (list))))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"->"
|
||||
(fn
|
||||
(s)
|
||||
(set! hayes-actual (hayes-slice s hayes-start-depth))
|
||||
(set! hayes-actual-set true)
|
||||
(hayes-truncate! s hayes-start-depth)))
|
||||
(forth-def-prim!
|
||||
state
|
||||
"}T"
|
||||
(fn
|
||||
(s)
|
||||
(let
|
||||
((expected (hayes-slice s hayes-start-depth)))
|
||||
(hayes-truncate! s hayes-start-depth)
|
||||
(if
|
||||
(and hayes-actual-set (= expected hayes-actual))
|
||||
(set! hayes-pass (+ hayes-pass 1))
|
||||
(begin
|
||||
(set! hayes-fail (+ hayes-fail 1))
|
||||
(set!
|
||||
hayes-failures
|
||||
(concat
|
||||
hayes-failures
|
||||
(list
|
||||
(dict
|
||||
"kind"
|
||||
"fail"
|
||||
"expected"
|
||||
(str expected)
|
||||
"actual"
|
||||
(str hayes-actual))))))))))
|
||||
(forth-def-prim! state "TESTING" (fn (s) nil))
|
||||
;; HEX/DECIMAL are real primitives now (runtime.sx) — no stub needed.
|
||||
state))
|
||||
|
||||
(define
|
||||
hayes-boot
|
||||
(fn () (let ((s (forth-boot))) (hayes-install! s) (hayes-reset!) s)))
|
||||
|
||||
;; Run a single preprocessed chunk (string of Forth source) on the shared
|
||||
;; state. Catch any raised error and move on — the chunk boundary is a
|
||||
;; safe resume point.
|
||||
(define
|
||||
hayes-bump-error-key!
|
||||
(fn
|
||||
(err)
|
||||
(let
|
||||
((msg (str err)))
|
||||
(let
|
||||
((space-idx (index-of msg " ")))
|
||||
(let
|
||||
((key
|
||||
(if
|
||||
(> space-idx 0)
|
||||
(substr msg 0 space-idx)
|
||||
msg)))
|
||||
(dict-set!
|
||||
hayes-error-hist
|
||||
key
|
||||
(+ 1 (or (get hayes-error-hist key) 0))))))))
|
||||
|
||||
(define
|
||||
hayes-run-chunk
|
||||
(fn
|
||||
(state src)
|
||||
(guard
|
||||
(err
|
||||
((= 1 1)
|
||||
(begin
|
||||
(set! hayes-error (+ hayes-error 1))
|
||||
(when
|
||||
(= (len hayes-first-error) 0)
|
||||
(set! hayes-first-error (str err)))
|
||||
(hayes-bump-error-key! err)
|
||||
(dict-set! state "dstack" (list))
|
||||
(dict-set! state "rstack" (list))
|
||||
(dict-set! state "compiling" false)
|
||||
(dict-set! state "current-def" nil)
|
||||
(dict-set! state "cstack" (list))
|
||||
(dict-set! state "input" (list)))))
|
||||
(forth-interpret state src))))
|
||||
|
||||
(define
|
||||
hayes-summary
|
||||
(fn
|
||||
()
|
||||
(dict
|
||||
"pass"
|
||||
hayes-pass
|
||||
"fail"
|
||||
hayes-fail
|
||||
"error"
|
||||
hayes-error
|
||||
"total"
|
||||
(+ (+ hayes-pass hayes-fail) hayes-error)
|
||||
"first-error"
|
||||
hayes-first-error
|
||||
"error-hist"
|
||||
hayes-error-hist)))
|
||||
@@ -5,39 +5,7 @@
|
||||
|
||||
(define
|
||||
forth-execute-word
|
||||
(fn
|
||||
(state word)
|
||||
(dict-set! word "call-count" (+ 1 (or (get word "call-count") 0)))
|
||||
(let ((body (get word "body"))) (body state))))
|
||||
|
||||
(define
|
||||
forth-hot-words
|
||||
(fn
|
||||
(state threshold)
|
||||
(forth-hot-walk
|
||||
(keys (get state "dict"))
|
||||
(get state "dict")
|
||||
threshold
|
||||
(list))))
|
||||
|
||||
(define
|
||||
forth-hot-walk
|
||||
(fn
|
||||
(names dict threshold acc)
|
||||
(if
|
||||
(= (len names) 0)
|
||||
acc
|
||||
(let
|
||||
((n (first names)))
|
||||
(let
|
||||
((w (get dict n)))
|
||||
(let
|
||||
((c (or (get w "call-count") 0)))
|
||||
(forth-hot-walk
|
||||
(rest names)
|
||||
dict
|
||||
threshold
|
||||
(if (>= c threshold) (cons (list n c) acc) acc))))))))
|
||||
(fn (state word) (let ((body (get word "body"))) (body state))))
|
||||
|
||||
(define
|
||||
forth-interpret-token
|
||||
@@ -49,7 +17,7 @@
|
||||
(not (nil? w))
|
||||
(forth-execute-word state w)
|
||||
(let
|
||||
((n (forth-parse-number tok (get (get state "vars") "base"))))
|
||||
((n (forth-parse-number tok (get state "base"))))
|
||||
(if
|
||||
(not (nil? n))
|
||||
(forth-push state n)
|
||||
|
||||
1547
lib/forth/runtime.sx
1547
lib/forth/runtime.sx
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"source": "gerryjackson/forth2012-test-suite src/core.fr",
|
||||
"generated_at": "2026-05-05T21:30:21Z",
|
||||
"chunks_available": 638,
|
||||
"chunks_fed": 638,
|
||||
"total": 638,
|
||||
"pass": 632,
|
||||
"fail": 6,
|
||||
"error": 0,
|
||||
"percent": 99,
|
||||
"note": "completed"
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
# Forth Hayes Core scoreboard
|
||||
|
||||
| metric | value |
|
||||
| ----------------- | ----: |
|
||||
| chunks available | 638 |
|
||||
| chunks fed | 638 |
|
||||
| total | 638 |
|
||||
| pass | 632 |
|
||||
| fail | 6 |
|
||||
| error | 0 |
|
||||
| percent | 99% |
|
||||
|
||||
- **Source**: `gerryjackson/forth2012-test-suite` `src/core.fr`
|
||||
- **Generated**: 2026-05-05T21:30:21Z
|
||||
- **Note**: completed
|
||||
|
||||
A "chunk" is any preprocessed segment ending at a `}T` (every Hayes test
|
||||
is one chunk, plus the small declaration blocks between tests).
|
||||
The runner catches raised errors at chunk boundaries so one bad chunk
|
||||
does not abort the rest. `error` covers chunks that raised; `fail`
|
||||
covers tests whose `->` / `}T` comparison mismatched.
|
||||
|
||||
### Chunk cap
|
||||
|
||||
`conformance.sh` processes the first `$MAX_CHUNKS` chunks (default
|
||||
**638**, i.e. the whole Hayes Core file). Lower the cap temporarily
|
||||
while iterating on primitives if a regression re-opens an infinite
|
||||
loop in later tests.
|
||||
@@ -1,239 +0,0 @@
|
||||
;; Phase 3 — control flow (IF/ELSE/THEN, BEGIN/UNTIL/WHILE/REPEAT/AGAIN,
|
||||
;; DO/LOOP, return stack). Grows as each control construct lands.
|
||||
|
||||
(define forth-p3-passed 0)
|
||||
(define forth-p3-failed 0)
|
||||
(define forth-p3-failures (list))
|
||||
|
||||
(define
|
||||
forth-p3-assert
|
||||
(fn
|
||||
(label expected actual)
|
||||
(if
|
||||
(= expected actual)
|
||||
(set! forth-p3-passed (+ forth-p3-passed 1))
|
||||
(begin
|
||||
(set! forth-p3-failed (+ forth-p3-failed 1))
|
||||
(set!
|
||||
forth-p3-failures
|
||||
(concat
|
||||
forth-p3-failures
|
||||
(list
|
||||
(str label ": expected " (str expected) " got " (str actual)))))))))
|
||||
|
||||
(define
|
||||
forth-p3-check-stack
|
||||
(fn
|
||||
(label src expected)
|
||||
(let ((r (forth-run src))) (forth-p3-assert label expected (nth r 2)))))
|
||||
|
||||
(define
|
||||
forth-p3-if-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p3-check-stack
|
||||
"IF taken (-1)"
|
||||
": Q -1 IF 10 THEN ; Q"
|
||||
(list 10))
|
||||
(forth-p3-check-stack
|
||||
"IF not taken (0)"
|
||||
": Q 0 IF 10 THEN ; Q"
|
||||
(list))
|
||||
(forth-p3-check-stack
|
||||
"IF with non-zero truthy"
|
||||
": Q 42 IF 10 THEN ; Q"
|
||||
(list 10))
|
||||
(forth-p3-check-stack
|
||||
"IF ELSE — true branch"
|
||||
": Q -1 IF 10 ELSE 20 THEN ; Q"
|
||||
(list 10))
|
||||
(forth-p3-check-stack
|
||||
"IF ELSE — false branch"
|
||||
": Q 0 IF 10 ELSE 20 THEN ; Q"
|
||||
(list 20))
|
||||
(forth-p3-check-stack
|
||||
"IF consumes flag"
|
||||
": Q IF 1 ELSE 2 THEN ; 0 Q"
|
||||
(list 2))
|
||||
(forth-p3-check-stack
|
||||
"absolute value via IF"
|
||||
": ABS2 DUP 0 < IF NEGATE THEN ; -7 ABS2"
|
||||
(list 7))
|
||||
(forth-p3-check-stack
|
||||
"abs leaves positive alone"
|
||||
": ABS2 DUP 0 < IF NEGATE THEN ; 7 ABS2"
|
||||
(list 7))
|
||||
(forth-p3-check-stack
|
||||
"sign: negative"
|
||||
": SIGN DUP 0 < IF DROP -1 ELSE DROP 1 THEN ; -3 SIGN"
|
||||
(list -1))
|
||||
(forth-p3-check-stack
|
||||
"sign: positive"
|
||||
": SIGN DUP 0 < IF DROP -1 ELSE DROP 1 THEN ; 3 SIGN"
|
||||
(list 1))
|
||||
(forth-p3-check-stack
|
||||
"nested IF (both true)"
|
||||
": Q 1 IF 1 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
|
||||
(list 10))
|
||||
(forth-p3-check-stack
|
||||
"nested IF (inner false)"
|
||||
": Q 1 IF 0 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
|
||||
(list 20))
|
||||
(forth-p3-check-stack
|
||||
"nested IF (outer false)"
|
||||
": Q 0 IF 0 IF 10 ELSE 20 THEN ELSE 30 THEN ; Q"
|
||||
(list 30))
|
||||
(forth-p3-check-stack
|
||||
"IF before other ops"
|
||||
": Q 1 IF 5 ELSE 6 THEN 2 * ; Q"
|
||||
(list 10))
|
||||
(forth-p3-check-stack
|
||||
"IF in chained def"
|
||||
": POS? 0 > ;
|
||||
: DOUBLE-IF-POS DUP POS? IF 2 * THEN ;
|
||||
3 DOUBLE-IF-POS"
|
||||
(list 6))
|
||||
(forth-p3-check-stack
|
||||
"empty then branch"
|
||||
": Q 1 IF THEN 99 ; Q"
|
||||
(list 99))
|
||||
(forth-p3-check-stack
|
||||
"empty else branch"
|
||||
": Q 0 IF 99 ELSE THEN ; Q"
|
||||
(list))
|
||||
(forth-p3-check-stack
|
||||
"sequential IF blocks"
|
||||
": Q -1 IF 1 THEN -1 IF 2 THEN ; Q"
|
||||
(list 1 2))))
|
||||
|
||||
(define
|
||||
forth-p3-loop-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p3-check-stack
|
||||
"BEGIN UNTIL (countdown to zero)"
|
||||
": CD BEGIN 1- DUP 0 = UNTIL ; 3 CD"
|
||||
(list 0))
|
||||
(forth-p3-check-stack
|
||||
"BEGIN UNTIL — single pass (UNTIL true immediately)"
|
||||
": Q BEGIN -1 UNTIL 42 ; Q"
|
||||
(list 42))
|
||||
(forth-p3-check-stack
|
||||
"BEGIN UNTIL — accumulate sum 1+2+3"
|
||||
": SUM3 0 3 BEGIN TUCK + SWAP 1- DUP 0 = UNTIL DROP ; SUM3"
|
||||
(list 6))
|
||||
(forth-p3-check-stack
|
||||
"BEGIN WHILE REPEAT — triangular sum 5"
|
||||
": TRI 0 5 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
|
||||
(list 15))
|
||||
(forth-p3-check-stack
|
||||
"BEGIN WHILE REPEAT — zero iterations"
|
||||
": TRI 0 0 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
|
||||
(list 0))
|
||||
(forth-p3-check-stack
|
||||
"BEGIN WHILE REPEAT — one iteration"
|
||||
": TRI 0 1 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ; TRI"
|
||||
(list 1))
|
||||
(forth-p3-check-stack
|
||||
"nested BEGIN UNTIL"
|
||||
": INNER BEGIN 1- DUP 0 = UNTIL DROP ;
|
||||
: OUTER BEGIN 3 INNER 1- DUP 0 = UNTIL ;
|
||||
2 OUTER"
|
||||
(list 0))
|
||||
(forth-p3-check-stack
|
||||
"BEGIN UNTIL after colon prefix"
|
||||
": TEN 10 ;
|
||||
: CD TEN BEGIN 1- DUP 0 = UNTIL ;
|
||||
CD"
|
||||
(list 0))
|
||||
(forth-p3-check-stack
|
||||
"WHILE inside IF branch"
|
||||
": Q 1 IF 0 3 BEGIN DUP 0 > WHILE TUCK + SWAP 1- REPEAT DROP ELSE 99 THEN ; Q"
|
||||
(list 6))))
|
||||
|
||||
(define
|
||||
forth-p3-do-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p3-check-stack
|
||||
"DO LOOP — simple sum 0..4"
|
||||
": SUM 0 5 0 DO I + LOOP ; SUM"
|
||||
(list 10))
|
||||
(forth-p3-check-stack
|
||||
"DO LOOP — 10..14 sum using I"
|
||||
": SUM 0 15 10 DO I + LOOP ; SUM"
|
||||
(list 60))
|
||||
(forth-p3-check-stack
|
||||
"DO LOOP — limit = start runs one pass"
|
||||
": SUM 0 5 5 DO I + LOOP ; SUM"
|
||||
(list 5))
|
||||
(forth-p3-check-stack
|
||||
"DO LOOP — count iterations"
|
||||
": COUNT 0 4 0 DO 1+ LOOP ; COUNT"
|
||||
(list 4))
|
||||
(forth-p3-check-stack
|
||||
"DO LOOP — nested, I inner / J outer"
|
||||
": MATRIX 0 3 0 DO 3 0 DO I J + + LOOP LOOP ; MATRIX"
|
||||
(list 18))
|
||||
(forth-p3-check-stack
|
||||
"DO LOOP — I used in arithmetic"
|
||||
": DBL 0 5 1 DO I 2 * + LOOP ; DBL"
|
||||
(list 20))
|
||||
(forth-p3-check-stack
|
||||
"+LOOP — count by 2"
|
||||
": Q 0 10 0 DO I + 2 +LOOP ; Q"
|
||||
(list 20))
|
||||
(forth-p3-check-stack
|
||||
"+LOOP — count by 3"
|
||||
": Q 0 10 0 DO I + 3 +LOOP ; Q"
|
||||
(list 18))
|
||||
(forth-p3-check-stack
|
||||
"+LOOP — negative step"
|
||||
": Q 0 0 10 DO I + -1 +LOOP ; Q"
|
||||
(list 55))
|
||||
(forth-p3-check-stack
|
||||
"LEAVE — early exit at I=3"
|
||||
": Q 0 10 0 DO I 3 = IF LEAVE THEN I + LOOP ; Q"
|
||||
(list 3))
|
||||
(forth-p3-check-stack
|
||||
"LEAVE — in nested loop exits only inner"
|
||||
": Q 0 3 0 DO 5 0 DO I 2 = IF LEAVE THEN I + LOOP LOOP ; Q"
|
||||
(list 3))
|
||||
(forth-p3-check-stack
|
||||
"DO LOOP preserves outer stack"
|
||||
": Q 99 5 0 DO I + LOOP ; Q"
|
||||
(list 109))
|
||||
(forth-p3-check-stack
|
||||
">R R>"
|
||||
": Q 7 >R 11 R> ; Q"
|
||||
(list 11 7))
|
||||
(forth-p3-check-stack
|
||||
">R R@ R>"
|
||||
": Q 7 >R R@ R> ; Q"
|
||||
(list 7 7))
|
||||
(forth-p3-check-stack
|
||||
"2>R 2R>"
|
||||
": Q 1 2 2>R 99 2R> ; Q"
|
||||
(list 99 1 2))
|
||||
(forth-p3-check-stack
|
||||
"2>R 2R@ 2R>"
|
||||
": Q 3 4 2>R 2R@ 2R> ; Q"
|
||||
(list 3 4 3 4))))
|
||||
|
||||
(define
|
||||
forth-p3-run-all
|
||||
(fn
|
||||
()
|
||||
(set! forth-p3-passed 0)
|
||||
(set! forth-p3-failed 0)
|
||||
(set! forth-p3-failures (list))
|
||||
(forth-p3-if-tests)
|
||||
(forth-p3-loop-tests)
|
||||
(forth-p3-do-tests)
|
||||
(dict
|
||||
"passed"
|
||||
forth-p3-passed
|
||||
"failed"
|
||||
forth-p3-failed
|
||||
"failures"
|
||||
forth-p3-failures)))
|
||||
@@ -1,268 +0,0 @@
|
||||
;; Phase 4 — strings + more Core.
|
||||
;; Uses the byte-memory model on state ("mem" dict + "here" cursor).
|
||||
|
||||
(define forth-p4-passed 0)
|
||||
(define forth-p4-failed 0)
|
||||
(define forth-p4-failures (list))
|
||||
|
||||
(define
|
||||
forth-p4-assert
|
||||
(fn
|
||||
(label expected actual)
|
||||
(if
|
||||
(= expected actual)
|
||||
(set! forth-p4-passed (+ forth-p4-passed 1))
|
||||
(begin
|
||||
(set! forth-p4-failed (+ forth-p4-failed 1))
|
||||
(set!
|
||||
forth-p4-failures
|
||||
(concat
|
||||
forth-p4-failures
|
||||
(list
|
||||
(str label ": expected " (str expected) " got " (str actual)))))))))
|
||||
|
||||
(define
|
||||
forth-p4-check-output
|
||||
(fn
|
||||
(label src expected)
|
||||
(let ((r (forth-run src))) (forth-p4-assert label expected (nth r 1)))))
|
||||
|
||||
(define
|
||||
forth-p4-check-stack-size
|
||||
(fn
|
||||
(label src expected-n)
|
||||
(let
|
||||
((r (forth-run src)))
|
||||
(forth-p4-assert label expected-n (len (nth r 2))))))
|
||||
|
||||
(define
|
||||
forth-p4-check-top
|
||||
(fn
|
||||
(label src expected)
|
||||
(let
|
||||
((r (forth-run src)))
|
||||
(let
|
||||
((stk (nth r 2)))
|
||||
(forth-p4-assert label expected (nth stk (- (len stk) 1)))))))
|
||||
|
||||
(define
|
||||
forth-p4-check-typed
|
||||
(fn
|
||||
(label src expected)
|
||||
(forth-p4-check-output label (str src " TYPE") expected)))
|
||||
|
||||
(define
|
||||
forth-p4-string-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-typed
|
||||
"S\" + TYPE — hello"
|
||||
"S\" HELLO\""
|
||||
"HELLO")
|
||||
(forth-p4-check-typed
|
||||
"S\" + TYPE — two words"
|
||||
"S\" HELLO WORLD\""
|
||||
"HELLO WORLD")
|
||||
(forth-p4-check-typed
|
||||
"S\" + TYPE — empty"
|
||||
"S\" \""
|
||||
"")
|
||||
(forth-p4-check-typed
|
||||
"S\" + TYPE — single char"
|
||||
"S\" X\""
|
||||
"X")
|
||||
(forth-p4-check-stack-size
|
||||
"S\" pushes (addr len)"
|
||||
"S\" HI\""
|
||||
2)
|
||||
(forth-p4-check-top
|
||||
"S\" length is correct"
|
||||
"S\" HELLO\""
|
||||
5)
|
||||
(forth-p4-check-output
|
||||
".\" prints at interpret time"
|
||||
".\" HELLO\""
|
||||
"HELLO")
|
||||
(forth-p4-check-output
|
||||
".\" in colon def"
|
||||
": GREET .\" HI \" ; GREET GREET"
|
||||
"HI HI ")))
|
||||
|
||||
(define
|
||||
forth-p4-count-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-typed
|
||||
"C\" + COUNT + TYPE"
|
||||
"C\" ABC\" COUNT"
|
||||
"ABC")
|
||||
(forth-p4-check-typed
|
||||
"C\" then COUNT leaves right len"
|
||||
"C\" HI THERE\" COUNT"
|
||||
"HI THERE")))
|
||||
|
||||
(define
|
||||
forth-p4-fill-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-typed
|
||||
"FILL overwrites prefix bytes"
|
||||
"S\" ABCDE\" 2DUP DROP 3 65 FILL"
|
||||
"AAADE")
|
||||
(forth-p4-check-typed
|
||||
"BLANK sets spaces"
|
||||
"S\" XYZAB\" 2DUP DROP 3 BLANK"
|
||||
" AB")))
|
||||
|
||||
(define
|
||||
forth-p4-cmove-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-output
|
||||
"CMOVE copies HELLO forward"
|
||||
": MKH 72 0 C! 69 1 C! 76 2 C! 76 3 C! 79 4 C! ;
|
||||
: T MKH 0 10 5 CMOVE 10 5 TYPE ; T"
|
||||
"HELLO")
|
||||
(forth-p4-check-output
|
||||
"CMOVE> copies overlapping backward"
|
||||
": MKA 65 0 C! 66 1 C! 67 2 C! ;
|
||||
: T MKA 0 1 2 CMOVE> 0 3 TYPE ; T"
|
||||
"AAB")
|
||||
(forth-p4-check-output
|
||||
"MOVE picks direction for overlap"
|
||||
": MKA 65 0 C! 66 1 C! 67 2 C! ;
|
||||
: T MKA 0 1 2 MOVE 0 3 TYPE ; T"
|
||||
"AAB")))
|
||||
|
||||
(define
|
||||
forth-p4-charplus-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-top
|
||||
"CHAR+ increments"
|
||||
"5 CHAR+"
|
||||
6)))
|
||||
|
||||
(define
|
||||
forth-p4-char-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-top "CHAR A -> 65" "CHAR A" 65)
|
||||
(forth-p4-check-top "CHAR x -> 120" "CHAR x" 120)
|
||||
(forth-p4-check-top "CHAR takes only first char" "CHAR HELLO" 72)
|
||||
(forth-p4-check-top
|
||||
"[CHAR] compiles literal"
|
||||
": AA [CHAR] A ; AA"
|
||||
65)
|
||||
(forth-p4-check-top
|
||||
"[CHAR] reads past IMMEDIATE"
|
||||
": ZZ [CHAR] Z ; ZZ"
|
||||
90)
|
||||
(forth-p4-check-stack-size
|
||||
"[CHAR] doesn't leak at compile time"
|
||||
": FOO [CHAR] A ; "
|
||||
0)))
|
||||
|
||||
(define
|
||||
forth-p4-key-accept-tests
|
||||
(fn
|
||||
()
|
||||
(let
|
||||
((r (forth-run "1000 2 ACCEPT")))
|
||||
(let ((stk (nth r 2))) (forth-p4-assert "ACCEPT empty buf -> 0" (list 0) stk)))))
|
||||
|
||||
(define
|
||||
forth-p4-shift-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-top "1 0 LSHIFT" "1 0 LSHIFT" 1)
|
||||
(forth-p4-check-top "1 1 LSHIFT" "1 1 LSHIFT" 2)
|
||||
(forth-p4-check-top "1 2 LSHIFT" "1 2 LSHIFT" 4)
|
||||
(forth-p4-check-top "1 15 LSHIFT" "1 15 LSHIFT" 32768)
|
||||
(forth-p4-check-top "1 31 LSHIFT" "1 31 LSHIFT" -2147483648)
|
||||
(forth-p4-check-top "1 0 RSHIFT" "1 0 RSHIFT" 1)
|
||||
(forth-p4-check-top "1 1 RSHIFT" "1 1 RSHIFT" 0)
|
||||
(forth-p4-check-top "2 1 RSHIFT" "2 1 RSHIFT" 1)
|
||||
(forth-p4-check-top "4 2 RSHIFT" "4 2 RSHIFT" 1)
|
||||
(forth-p4-check-top "-1 1 RSHIFT (logical, not arithmetic)" "-1 1 RSHIFT" 2147483647)
|
||||
(forth-p4-check-top "MSB via 1S 1 RSHIFT INVERT" "0 INVERT 1 RSHIFT INVERT" -2147483648)))
|
||||
|
||||
(define
|
||||
forth-p4-sp-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-top "SP@ returns depth (0)" "SP@" 0)
|
||||
(forth-p4-check-top
|
||||
"SP@ after pushes"
|
||||
"1 2 3 SP@ SWAP DROP SWAP DROP SWAP DROP"
|
||||
3)
|
||||
(forth-p4-check-stack-size
|
||||
"SP! truncates"
|
||||
"1 2 3 4 5 2 SP!"
|
||||
2)
|
||||
(forth-p4-check-top
|
||||
"SP! leaves base items intact"
|
||||
"1 2 3 4 5 2 SP!"
|
||||
2)))
|
||||
|
||||
(define
|
||||
forth-p4-base-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-top
|
||||
"BASE default is 10"
|
||||
"BASE @"
|
||||
10)
|
||||
(forth-p4-check-top
|
||||
"HEX switches base to 16"
|
||||
"HEX BASE @"
|
||||
16)
|
||||
(forth-p4-check-top
|
||||
"DECIMAL resets to 10"
|
||||
"HEX DECIMAL BASE @"
|
||||
10)
|
||||
(forth-p4-check-top
|
||||
"HEX parses 10 as 16"
|
||||
"HEX 10"
|
||||
16)
|
||||
(forth-p4-check-top
|
||||
"HEX parses FF as 255"
|
||||
"HEX FF"
|
||||
255)
|
||||
(forth-p4-check-top
|
||||
"DECIMAL parses 10 as 10"
|
||||
"HEX DECIMAL 10"
|
||||
10)
|
||||
(forth-p4-check-top
|
||||
"OCTAL parses 17 as 15"
|
||||
"OCTAL 17"
|
||||
15)
|
||||
(forth-p4-check-top
|
||||
"BASE @ ; 16 BASE ! ; BASE @"
|
||||
"BASE @ 16 BASE ! BASE @ SWAP DROP"
|
||||
16)))
|
||||
|
||||
(define
|
||||
forth-p4-run-all
|
||||
(fn
|
||||
()
|
||||
(set! forth-p4-passed 0)
|
||||
(set! forth-p4-failed 0)
|
||||
(set! forth-p4-failures (list))
|
||||
(forth-p4-string-tests)
|
||||
(forth-p4-count-tests)
|
||||
(forth-p4-fill-tests)
|
||||
(forth-p4-cmove-tests)
|
||||
(forth-p4-charplus-tests)
|
||||
(forth-p4-char-tests)
|
||||
(forth-p4-key-accept-tests)
|
||||
(forth-p4-base-tests)
|
||||
(forth-p4-shift-tests)
|
||||
(forth-p4-sp-tests)
|
||||
(dict
|
||||
"passed"
|
||||
forth-p4-passed
|
||||
"failed"
|
||||
forth-p4-failed
|
||||
"failures"
|
||||
forth-p4-failures)))
|
||||
@@ -1,333 +0,0 @@
|
||||
;; Phase 5 — Core Extension + memory primitives.
|
||||
|
||||
(define forth-p5-passed 0)
|
||||
(define forth-p5-failed 0)
|
||||
(define forth-p5-failures (list))
|
||||
|
||||
(define
|
||||
forth-p5-assert
|
||||
(fn
|
||||
(label expected actual)
|
||||
(if
|
||||
(= expected actual)
|
||||
(set! forth-p5-passed (+ forth-p5-passed 1))
|
||||
(begin
|
||||
(set! forth-p5-failed (+ forth-p5-failed 1))
|
||||
(set!
|
||||
forth-p5-failures
|
||||
(concat
|
||||
forth-p5-failures
|
||||
(list
|
||||
(str label ": expected " (str expected) " got " (str actual)))))))))
|
||||
|
||||
(define
|
||||
forth-p5-check-stack
|
||||
(fn
|
||||
(label src expected)
|
||||
(let ((r (forth-run src))) (forth-p5-assert label expected (nth r 2)))))
|
||||
|
||||
(define
|
||||
forth-p5-check-top
|
||||
(fn
|
||||
(label src expected)
|
||||
(let
|
||||
((r (forth-run src)))
|
||||
(let
|
||||
((stk (nth r 2)))
|
||||
(forth-p5-assert label expected (nth stk (- (len stk) 1)))))))
|
||||
|
||||
(define
|
||||
forth-p5-create-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-top
|
||||
"CREATE pushes HERE-at-creation"
|
||||
"HERE CREATE FOO FOO ="
|
||||
-1)
|
||||
(forth-p5-check-top
|
||||
"CREATE + ALLOT advances HERE"
|
||||
"HERE 5 ALLOT HERE SWAP -"
|
||||
5)
|
||||
(forth-p5-check-top
|
||||
"CREATE + , stores cell"
|
||||
"CREATE FOO 42 , FOO @"
|
||||
42)
|
||||
(forth-p5-check-stack
|
||||
"CREATE multiple ,"
|
||||
"CREATE TBL 1 , 2 , 3 , TBL @ TBL CELL+ @ TBL CELL+ CELL+ @"
|
||||
(list 1 2 3))
|
||||
(forth-p5-check-top
|
||||
"C, stores byte"
|
||||
"CREATE B 65 C, 66 C, B C@"
|
||||
65)))
|
||||
|
||||
(define
|
||||
forth-p5-unsigned-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-top "1 2 U<" "1 2 U<" -1)
|
||||
(forth-p5-check-top "2 1 U<" "2 1 U<" 0)
|
||||
(forth-p5-check-top "0 1 U<" "0 1 U<" -1)
|
||||
(forth-p5-check-top "-1 1 U< (since -1 unsigned is huge)" "-1 1 U<" 0)
|
||||
(forth-p5-check-top "1 -1 U<" "1 -1 U<" -1)
|
||||
(forth-p5-check-top "1 2 U>" "1 2 U>" 0)
|
||||
(forth-p5-check-top "-1 1 U>" "-1 1 U>" -1)))
|
||||
|
||||
(define
|
||||
forth-p5-2bang-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-stack
|
||||
"2! / 2@"
|
||||
"CREATE X 0 , 0 , 11 22 X 2! X 2@"
|
||||
(list 11 22))))
|
||||
|
||||
(define
|
||||
forth-p5-mixed-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-stack "S>D positive" "5 S>D" (list 5 0))
|
||||
(forth-p5-check-stack "S>D negative" "-5 S>D" (list -5 -1))
|
||||
(forth-p5-check-stack "S>D zero" "0 S>D" (list 0 0))
|
||||
(forth-p5-check-top "D>S keeps low" "5 0 D>S" 5)
|
||||
(forth-p5-check-stack "M* small positive" "3 4 M*" (list 12 0))
|
||||
(forth-p5-check-stack "M* negative" "-3 4 M*" (list -12 -1))
|
||||
(forth-p5-check-stack
|
||||
"M* negative * negative"
|
||||
"-3 -4 M*"
|
||||
(list 12 0))
|
||||
(forth-p5-check-stack "UM* small" "3 4 UM*" (list 12 0))
|
||||
(forth-p5-check-stack
|
||||
"UM/MOD: 100 0 / 5"
|
||||
"100 0 5 UM/MOD"
|
||||
(list 0 20))
|
||||
(forth-p5-check-stack
|
||||
"FM/MOD: -7 / 2 floored"
|
||||
"-7 -1 2 FM/MOD"
|
||||
(list 1 -4))
|
||||
(forth-p5-check-stack
|
||||
"SM/REM: -7 / 2 truncated"
|
||||
"-7 -1 2 SM/REM"
|
||||
(list -1 -3))
|
||||
(forth-p5-check-top "*/ truncated" "7 11 13 */" 5)
|
||||
(forth-p5-check-stack "*/MOD" "7 11 13 */MOD" (list 12 5))))
|
||||
|
||||
(define
|
||||
forth-p5-double-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-stack "D+ small" "5 0 7 0 D+" (list 12 0))
|
||||
(forth-p5-check-stack "D+ negative" "-5 -1 -3 -1 D+" (list -8 -1))
|
||||
(forth-p5-check-stack "D- small" "10 0 3 0 D-" (list 7 0))
|
||||
(forth-p5-check-stack "DNEGATE positive" "5 0 DNEGATE" (list -5 -1))
|
||||
(forth-p5-check-stack "DNEGATE negative" "-5 -1 DNEGATE" (list 5 0))
|
||||
(forth-p5-check-stack "DABS negative" "-7 -1 DABS" (list 7 0))
|
||||
(forth-p5-check-stack "DABS positive" "7 0 DABS" (list 7 0))
|
||||
(forth-p5-check-top "D= equal" "5 0 5 0 D=" -1)
|
||||
(forth-p5-check-top "D= unequal lo" "5 0 7 0 D=" 0)
|
||||
(forth-p5-check-top "D= unequal hi" "5 0 5 1 D=" 0)
|
||||
(forth-p5-check-top "D< lt" "5 0 7 0 D<" -1)
|
||||
(forth-p5-check-top "D< gt" "7 0 5 0 D<" 0)
|
||||
(forth-p5-check-top "D0= zero" "0 0 D0=" -1)
|
||||
(forth-p5-check-top "D0= nonzero" "5 0 D0=" 0)
|
||||
(forth-p5-check-top "D0< neg" "-5 -1 D0<" -1)
|
||||
(forth-p5-check-top "D0< pos" "5 0 D0<" 0)
|
||||
(forth-p5-check-stack "DMAX" "5 0 7 0 DMAX" (list 7 0))
|
||||
(forth-p5-check-stack "DMIN" "5 0 7 0 DMIN" (list 5 0))))
|
||||
|
||||
(define
|
||||
forth-p5-format-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p4-check-output-passthrough
|
||||
"U. prints with trailing space"
|
||||
"123 U."
|
||||
"123 ")
|
||||
(forth-p4-check-output-passthrough
|
||||
"<# #S #> TYPE — decimal"
|
||||
"123 0 <# #S #> TYPE"
|
||||
"123")
|
||||
(forth-p4-check-output-passthrough
|
||||
"<# #S #> TYPE — hex"
|
||||
"255 HEX 0 <# #S #> TYPE"
|
||||
"FF")
|
||||
(forth-p4-check-output-passthrough
|
||||
"<# # # #> partial"
|
||||
"1234 0 <# # # #> TYPE"
|
||||
"34")
|
||||
(forth-p4-check-output-passthrough
|
||||
"SIGN holds minus"
|
||||
"<# -1 SIGN -1 SIGN 0 0 #> TYPE"
|
||||
"--")
|
||||
(forth-p4-check-output-passthrough
|
||||
".R right-justifies"
|
||||
"42 5 .R"
|
||||
" 42")
|
||||
(forth-p4-check-output-passthrough
|
||||
".R negative"
|
||||
"-42 5 .R"
|
||||
" -42")
|
||||
(forth-p4-check-output-passthrough
|
||||
"U.R"
|
||||
"42 5 U.R"
|
||||
" 42")
|
||||
(forth-p4-check-output-passthrough
|
||||
"HOLD char"
|
||||
"<# 0 0 65 HOLD #> TYPE"
|
||||
"A")))
|
||||
|
||||
(define
|
||||
forth-p5-dict-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-top
|
||||
"EXECUTE via tick"
|
||||
": INC 1+ ; 9 ' INC EXECUTE"
|
||||
10)
|
||||
(forth-p5-check-top
|
||||
"['] inside def"
|
||||
": DUB 2* ; : APPLY ['] DUB EXECUTE ; 5 APPLY"
|
||||
10)
|
||||
(forth-p5-check-top
|
||||
">BODY of CREATE word"
|
||||
"CREATE C 99 , ' C >BODY @"
|
||||
99)
|
||||
(forth-p5-check-stack
|
||||
"WORD parses next token to counted-string"
|
||||
": A 5 ; BL WORD A COUNT TYPE"
|
||||
(list))
|
||||
(forth-p5-check-top
|
||||
"FIND on known word -> non-zero"
|
||||
": A 5 ; BL WORD A FIND SWAP DROP"
|
||||
-1)))
|
||||
|
||||
(define
|
||||
forth-p5-state-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-top
|
||||
"STATE @ in interpret mode"
|
||||
"STATE @"
|
||||
0)
|
||||
(forth-p5-check-top
|
||||
"STATE @ via IMMEDIATE inside compile"
|
||||
": GT8 STATE @ ; IMMEDIATE : T GT8 LITERAL ; T"
|
||||
-1)
|
||||
(forth-p5-check-top
|
||||
"[ ] LITERAL captures"
|
||||
": SEVEN [ 7 ] LITERAL ; SEVEN"
|
||||
7)
|
||||
(forth-p5-check-top
|
||||
"EVALUATE in interpret mode"
|
||||
"S\" 5 7 +\" EVALUATE"
|
||||
12)
|
||||
(forth-p5-check-top
|
||||
"EVALUATE inside def"
|
||||
": A 100 ; : B S\" A\" EVALUATE ; B"
|
||||
100)))
|
||||
|
||||
(define
|
||||
forth-p5-misc-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-top "WITHIN inclusive lower" "3 2 10 WITHIN" -1)
|
||||
(forth-p5-check-top "WITHIN exclusive upper" "10 2 10 WITHIN" 0)
|
||||
(forth-p5-check-top "WITHIN below range" "1 2 10 WITHIN" 0)
|
||||
(forth-p5-check-top "WITHIN at lower" "2 2 10 WITHIN" -1)
|
||||
(forth-p5-check-top
|
||||
"EXIT leaves colon-def early"
|
||||
": F 5 EXIT 99 ; F"
|
||||
5)
|
||||
(forth-p5-check-stack
|
||||
"EXIT in IF branch"
|
||||
": F 5 0 IF DROP 99 EXIT THEN ; F"
|
||||
(list 5))
|
||||
(forth-p5-check-top
|
||||
"UNLOOP + EXIT in DO"
|
||||
": SUM 0 10 0 DO I 5 = IF I UNLOOP EXIT THEN LOOP ; SUM"
|
||||
5)))
|
||||
|
||||
(define
|
||||
forth-p5-fa-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-top
|
||||
"R/O R/W W/O constants"
|
||||
"R/O R/W W/O + +"
|
||||
3)
|
||||
(forth-p5-check-top
|
||||
"CREATE-FILE returns ior=0"
|
||||
"CREATE PAD 50 ALLOT PAD S\" /tmp/test.fxf\" ROT SWAP CMOVE S\" /tmp/test.fxf\" R/W CREATE-FILE SWAP DROP"
|
||||
0)
|
||||
(forth-p5-check-top
|
||||
"WRITE-FILE then CLOSE"
|
||||
"S\" /tmp/t2.fxf\" R/W CREATE-FILE DROP >R S\" HI\" R@ WRITE-FILE R> CLOSE-FILE +"
|
||||
0)
|
||||
(forth-p5-check-top
|
||||
"OPEN-FILE on unknown path returns ior!=0"
|
||||
"S\" /tmp/nope.fxf\" R/O OPEN-FILE SWAP DROP 0 ="
|
||||
0)))
|
||||
|
||||
(define
|
||||
forth-p5-string-tests
|
||||
(fn
|
||||
()
|
||||
(forth-p5-check-top "COMPARE equal" "S\" ABC\" S\" ABC\" COMPARE" 0)
|
||||
(forth-p5-check-top "COMPARE less" "S\" ABC\" S\" ABD\" COMPARE" -1)
|
||||
(forth-p5-check-top "COMPARE greater" "S\" ABD\" S\" ABC\" COMPARE" 1)
|
||||
(forth-p5-check-top
|
||||
"COMPARE prefix less"
|
||||
"S\" AB\" S\" ABC\" COMPARE"
|
||||
-1)
|
||||
(forth-p5-check-top
|
||||
"COMPARE prefix greater"
|
||||
"S\" ABC\" S\" AB\" COMPARE"
|
||||
1)
|
||||
(forth-p5-check-top
|
||||
"SEARCH found flag"
|
||||
"S\" HELLO WORLD\" S\" WORLD\" SEARCH"
|
||||
-1)
|
||||
(forth-p5-check-top
|
||||
"SEARCH not found flag"
|
||||
"S\" HELLO\" S\" XYZ\" SEARCH"
|
||||
0)
|
||||
(forth-p5-check-top
|
||||
"SEARCH empty needle flag"
|
||||
"S\" HELLO\" S\" \" SEARCH"
|
||||
-1)
|
||||
(forth-p5-check-top
|
||||
"SLITERAL via [ S\" ... \" ]"
|
||||
": A [ S\" HI\" ] SLITERAL ; A SWAP DROP"
|
||||
2)))
|
||||
|
||||
(define
|
||||
forth-p4-check-output-passthrough
|
||||
(fn
|
||||
(label src expected)
|
||||
(let ((r (forth-run src))) (forth-p5-assert label expected (nth r 1)))))
|
||||
|
||||
(define
|
||||
forth-p5-run-all
|
||||
(fn
|
||||
()
|
||||
(set! forth-p5-passed 0)
|
||||
(set! forth-p5-failed 0)
|
||||
(set! forth-p5-failures (list))
|
||||
(forth-p5-create-tests)
|
||||
(forth-p5-unsigned-tests)
|
||||
(forth-p5-2bang-tests)
|
||||
(forth-p5-mixed-tests)
|
||||
(forth-p5-double-tests)
|
||||
(forth-p5-format-tests)
|
||||
(forth-p5-dict-tests)
|
||||
(forth-p5-state-tests)
|
||||
(forth-p5-misc-tests)
|
||||
(forth-p5-fa-tests)
|
||||
(forth-p5-string-tests)
|
||||
(dict
|
||||
"passed"
|
||||
forth-p5-passed
|
||||
"failed"
|
||||
forth-p5-failed
|
||||
"failures"
|
||||
forth-p5-failures)))
|
||||
@@ -48,6 +48,15 @@
|
||||
prop
|
||||
value))
|
||||
(list (quote hs-query-all) (nth base-ast 1))))
|
||||
((and (list? base-ast) (= (first base-ast) (quote query)))
|
||||
(list
|
||||
(quote dom-set-prop)
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth base-ast 1)
|
||||
(list (quote hs-query-first) (nth base-ast 1)))
|
||||
prop
|
||||
value))
|
||||
((and (list? base-ast) (= (first base-ast) dot-sym) (let ((inner (nth base-ast 1))) (and (list? inner) (= (first inner) (quote query)) (let ((s (nth inner 1))) (and (string? s) (> (len s) 0) (= (substring s 0 1) "."))))))
|
||||
(let
|
||||
((inner (nth base-ast 1))
|
||||
@@ -146,6 +155,14 @@
|
||||
(nth prop-ast 1)
|
||||
value)
|
||||
(list (quote set!) (hs-to-sx target) value))))))
|
||||
((= th (quote query))
|
||||
(list
|
||||
(quote hs-set-inner-html!)
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth target 1)
|
||||
(list (quote hs-query-first) (nth target 1)))
|
||||
value))
|
||||
(true (list (quote set!) (hs-to-sx target) value)))))))
|
||||
(define
|
||||
emit-on
|
||||
@@ -274,17 +291,33 @@
|
||||
((name (nth ast 1)) (rest-parts (rest (rest ast))))
|
||||
(cond
|
||||
((and (= (len ast) 4) (list? (nth ast 2)) (= (first (nth ast 2)) (quote dict)))
|
||||
(list
|
||||
(quote dom-dispatch)
|
||||
(hs-to-sx (nth ast 3))
|
||||
name
|
||||
(hs-to-sx (nth ast 2))))
|
||||
(let
|
||||
((tgt-ast (nth ast 3)))
|
||||
(list
|
||||
(quote dom-dispatch)
|
||||
(if
|
||||
(and (list? tgt-ast) (= (first tgt-ast) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth tgt-ast 1)
|
||||
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||
(hs-to-sx tgt-ast))
|
||||
name
|
||||
(hs-to-sx (nth ast 2)))))
|
||||
((= (len ast) 3)
|
||||
(list
|
||||
(quote dom-dispatch)
|
||||
(hs-to-sx (nth ast 2))
|
||||
name
|
||||
(list (quote dict) "sender" (quote me))))
|
||||
(let
|
||||
((tgt-ast (nth ast 2)))
|
||||
(list
|
||||
(quote dom-dispatch)
|
||||
(if
|
||||
(and (list? tgt-ast) (= (first tgt-ast) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth tgt-ast 1)
|
||||
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||
(hs-to-sx tgt-ast))
|
||||
name
|
||||
(list (quote dict) "sender" (quote me)))))
|
||||
(true
|
||||
(list
|
||||
(quote dom-dispatch)
|
||||
@@ -706,6 +739,33 @@
|
||||
(quote fn)
|
||||
(cons (quote me) (map make-symbol params))
|
||||
(cons (quote do) (map hs-to-sx body)))))))
|
||||
(define
|
||||
hs-safe-obj
|
||||
(fn
|
||||
(obj-ast)
|
||||
(if
|
||||
(and (list? obj-ast) (= (first obj-ast) (quote ref)))
|
||||
(list (quote host-global) (nth obj-ast 1))
|
||||
(if
|
||||
(and (list? obj-ast) (= (first obj-ast) dot-sym))
|
||||
(let
|
||||
((inner (nth obj-ast 1)) (prop (nth obj-ast 2)))
|
||||
(list (quote host-get) (hs-safe-obj inner) prop))
|
||||
(hs-to-sx obj-ast)))))
|
||||
(define
|
||||
hs-chain-name
|
||||
(fn
|
||||
(obj-ast)
|
||||
(if
|
||||
(and (list? obj-ast) (= (first obj-ast) (quote ref)))
|
||||
(nth obj-ast 1)
|
||||
(if
|
||||
(and (list? obj-ast) (= (first obj-ast) dot-sym))
|
||||
(str (hs-chain-name (nth obj-ast 1)) "." (nth obj-ast 2))
|
||||
(if
|
||||
(and (list? obj-ast) (= (first obj-ast) (quote query)))
|
||||
(nth obj-ast 1)
|
||||
nil)))))
|
||||
(fn
|
||||
(ast)
|
||||
(cond
|
||||
@@ -1226,12 +1286,21 @@
|
||||
(if
|
||||
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
||||
(list
|
||||
(quote for-each)
|
||||
(quote let)
|
||||
(list
|
||||
(quote fn)
|
||||
(list (quote _el))
|
||||
(list (quote dom-add-class) (quote _el) (nth ast 1)))
|
||||
(list (quote hs-query-all) (nth raw-tgt 1)))
|
||||
(list
|
||||
(quote _tgt)
|
||||
(list (quote hs-query-named-all) (nth raw-tgt 1))))
|
||||
(list
|
||||
(quote for-each)
|
||||
(list
|
||||
(quote fn)
|
||||
(list (quote _el))
|
||||
(list
|
||||
(quote dom-add-class)
|
||||
(quote _el)
|
||||
(nth ast 1)))
|
||||
(quote _tgt)))
|
||||
(list
|
||||
(quote dom-add-class)
|
||||
(hs-to-sx raw-tgt)
|
||||
@@ -1244,14 +1313,20 @@
|
||||
(nth ast 2)))
|
||||
((= head (quote set-styles))
|
||||
(let
|
||||
((pairs (nth ast 1)) (tgt (hs-to-sx (nth ast 2))))
|
||||
(cons
|
||||
(quote do)
|
||||
(map
|
||||
(fn
|
||||
(p)
|
||||
(list (quote dom-set-style) tgt (first p) (nth p 1)))
|
||||
pairs))))
|
||||
((pairs (nth ast 1)) (tgt-ast (nth ast 2)))
|
||||
(let
|
||||
((tgt (if (and (list? tgt-ast) (= (first tgt-ast) (quote query))) (list (quote hs-named-target) (nth tgt-ast 1) (list (quote hs-query-first) (nth tgt-ast 1))) (hs-to-sx tgt-ast))))
|
||||
(cons
|
||||
(quote do)
|
||||
(map
|
||||
(fn
|
||||
(p)
|
||||
(list
|
||||
(quote dom-set-style)
|
||||
tgt
|
||||
(first p)
|
||||
(nth p 1)))
|
||||
pairs)))))
|
||||
((= head (quote multi-add-class))
|
||||
(let
|
||||
((target (hs-to-sx (nth ast 1)))
|
||||
@@ -1349,15 +1424,21 @@
|
||||
(if
|
||||
(and (list? raw-tgt) (= (first raw-tgt) (quote query)))
|
||||
(list
|
||||
(quote for-each)
|
||||
(quote let)
|
||||
(list
|
||||
(quote fn)
|
||||
(list (quote _el))
|
||||
(list
|
||||
(quote dom-remove-class)
|
||||
(quote _el)
|
||||
(nth ast 1)))
|
||||
(list (quote hs-query-all) (nth raw-tgt 1)))
|
||||
(quote _tgt)
|
||||
(list (quote hs-query-named-all) (nth raw-tgt 1))))
|
||||
(list
|
||||
(quote for-each)
|
||||
(list
|
||||
(quote fn)
|
||||
(list (quote _el))
|
||||
(list
|
||||
(quote dom-remove-class)
|
||||
(quote _el)
|
||||
(nth ast 1)))
|
||||
(quote _tgt)))
|
||||
(list
|
||||
(quote dom-remove-class)
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt))
|
||||
@@ -1401,15 +1482,32 @@
|
||||
((tgt (nth ast 3)))
|
||||
(list
|
||||
(quote hs-set-attr!)
|
||||
(hs-to-sx tgt)
|
||||
(if
|
||||
(and (list? tgt) (= (first tgt) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth tgt 1)
|
||||
(list (quote hs-query-first) (nth tgt 1)))
|
||||
(hs-to-sx tgt))
|
||||
(nth ast 1)
|
||||
(hs-to-sx (nth ast 2)))))
|
||||
((= head (quote remove-value))
|
||||
(let
|
||||
((val (hs-to-sx (nth ast 1))) (tgt (nth ast 2)))
|
||||
((val (hs-to-sx (nth ast 1))) (raw-tgt (nth ast 2)))
|
||||
(emit-set
|
||||
tgt
|
||||
(list (quote hs-remove-from!) val (hs-to-sx tgt)))))
|
||||
raw-tgt
|
||||
(list
|
||||
(quote hs-remove-from!)
|
||||
val
|
||||
(if
|
||||
(and
|
||||
(list? raw-tgt)
|
||||
(= (first raw-tgt) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth raw-tgt 1)
|
||||
(list (quote hs-query-first) (nth raw-tgt 1)))
|
||||
(hs-to-sx raw-tgt))))))
|
||||
((= head (quote empty-target))
|
||||
(let
|
||||
((tgt (nth ast 1)))
|
||||
@@ -1440,8 +1538,19 @@
|
||||
(hs-to-sx (nth ast 2))))
|
||||
((= head (quote remove-attr))
|
||||
(let
|
||||
((tgt (if (nil? (nth ast 2)) (quote me) (hs-to-sx (nth ast 2)))))
|
||||
(list (quote dom-remove-attr) tgt (nth ast 1))))
|
||||
((raw-tgt (nth ast 2)))
|
||||
(list
|
||||
(quote dom-remove-attr)
|
||||
(if
|
||||
(and
|
||||
(list? raw-tgt)
|
||||
(= (first raw-tgt) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth raw-tgt 1)
|
||||
(list (quote hs-query-first) (nth raw-tgt 1)))
|
||||
(if (nil? raw-tgt) (quote me) (hs-to-sx raw-tgt)))
|
||||
(nth ast 1))))
|
||||
((= head (quote remove-css))
|
||||
(let
|
||||
((tgt (if (nil? (nth ast 2)) (quote me) (hs-to-sx (nth ast 2))))
|
||||
@@ -1452,10 +1561,20 @@
|
||||
(fn (p) (list (quote dom-set-style) tgt p ""))
|
||||
props))))
|
||||
((= head (quote toggle-class))
|
||||
(list
|
||||
(quote hs-toggle-class!)
|
||||
(hs-to-sx (nth ast 2))
|
||||
(nth ast 1)))
|
||||
(let
|
||||
((tgt-ast (nth ast 2)))
|
||||
(list
|
||||
(quote hs-toggle-class!)
|
||||
(if
|
||||
(and
|
||||
(list? tgt-ast)
|
||||
(= (first tgt-ast) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth tgt-ast 1)
|
||||
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||
(hs-to-sx tgt-ast))
|
||||
(nth ast 1))))
|
||||
((= head (quote toggle-class-for))
|
||||
(list
|
||||
(quote do)
|
||||
@@ -1510,11 +1629,21 @@
|
||||
(hs-to-sx tgt-ast)
|
||||
(hs-to-sx val-ast)))))
|
||||
((= head (quote toggle-between))
|
||||
(list
|
||||
(quote hs-toggle-between!)
|
||||
(hs-to-sx (nth ast 3))
|
||||
(nth ast 1)
|
||||
(nth ast 2)))
|
||||
(let
|
||||
((tgt-ast (nth ast 3)))
|
||||
(list
|
||||
(quote hs-toggle-between!)
|
||||
(if
|
||||
(and
|
||||
(list? tgt-ast)
|
||||
(= (first tgt-ast) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth tgt-ast 1)
|
||||
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||
(hs-to-sx tgt-ast))
|
||||
(nth ast 1)
|
||||
(nth ast 2))))
|
||||
((= head (quote toggle-style))
|
||||
(let
|
||||
((raw-tgt (nth ast 2)))
|
||||
@@ -1538,10 +1667,20 @@
|
||||
(quote list)
|
||||
(map hs-to-sx (slice ast 3 (len ast))))))
|
||||
((= head (quote toggle-attr))
|
||||
(list
|
||||
(quote hs-toggle-attr!)
|
||||
(hs-to-sx (nth ast 2))
|
||||
(nth ast 1)))
|
||||
(let
|
||||
((tgt-ast (nth ast 2)))
|
||||
(list
|
||||
(quote hs-toggle-attr!)
|
||||
(if
|
||||
(and
|
||||
(list? tgt-ast)
|
||||
(= (first tgt-ast) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth tgt-ast 1)
|
||||
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||
(hs-to-sx tgt-ast))
|
||||
(nth ast 1))))
|
||||
((= head (quote toggle-attr-between))
|
||||
(list
|
||||
(quote hs-toggle-attr-between!)
|
||||
@@ -1575,7 +1714,22 @@
|
||||
(emit-set
|
||||
raw-tgt
|
||||
(list (quote hs-put-at!) val pos (hs-to-sx raw-tgt))))
|
||||
(true (list (quote hs-put!) val pos (hs-to-sx raw-tgt))))))
|
||||
(true
|
||||
(let
|
||||
((tgt-ast raw-tgt))
|
||||
(list
|
||||
(quote hs-put!)
|
||||
val
|
||||
pos
|
||||
(if
|
||||
(and
|
||||
(list? tgt-ast)
|
||||
(= (first tgt-ast) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth tgt-ast 1)
|
||||
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||
(hs-to-sx tgt-ast))))))))
|
||||
((= head (quote if))
|
||||
(if
|
||||
(> (len ast) 3)
|
||||
@@ -1651,12 +1805,22 @@
|
||||
(detail (if (= (len ast) 4) (nth ast 2) nil)))
|
||||
(list
|
||||
(quote dom-dispatch)
|
||||
(hs-to-sx tgt)
|
||||
(let
|
||||
((tgt-ast tgt))
|
||||
(if
|
||||
(and
|
||||
(list? tgt-ast)
|
||||
(= (first tgt-ast) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth tgt-ast 1)
|
||||
(list (quote hs-query-first) (nth tgt-ast 1)))
|
||||
(hs-to-sx tgt-ast)))
|
||||
name
|
||||
(if has-detail (hs-to-sx detail) nil))))
|
||||
((= head (quote hide))
|
||||
(let
|
||||
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
||||
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-named-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
||||
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
||||
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
||||
(if
|
||||
@@ -1672,7 +1836,7 @@
|
||||
(hs-to-sx when-cond))))))
|
||||
((= head (quote show))
|
||||
(let
|
||||
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
||||
((tgt (let ((raw-tgt (nth ast 1))) (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-query-named-all) (nth raw-tgt 1)) (hs-to-sx raw-tgt))))
|
||||
(strategy (if (> (len ast) 2) (nth ast 2) "display"))
|
||||
(when-cond (if (> (len ast) 3) (nth ast 3) nil)))
|
||||
(if
|
||||
@@ -1735,13 +1899,28 @@
|
||||
((= head (quote call))
|
||||
(let
|
||||
((raw-fn (nth ast 1))
|
||||
(fn-expr
|
||||
(if
|
||||
(string? raw-fn)
|
||||
(make-symbol raw-fn)
|
||||
(hs-to-sx raw-fn)))
|
||||
(args (map hs-to-sx (rest (rest ast)))))
|
||||
(cons fn-expr args)))
|
||||
(if
|
||||
(and (list? raw-fn) (= (first raw-fn) (quote ref)))
|
||||
(let
|
||||
((name (nth raw-fn 1)))
|
||||
(list
|
||||
(quote let)
|
||||
(list
|
||||
(list
|
||||
(quote __hs-fn)
|
||||
(list (quote host-global) name)))
|
||||
(cons
|
||||
(quote do)
|
||||
(list
|
||||
(list
|
||||
(quote if)
|
||||
(list (quote nil?) (quote __hs-fn))
|
||||
(list (quote raise) (str "'" name "' is null"))
|
||||
(cons (quote __hs-fn) args))))))
|
||||
(let
|
||||
((fn-expr (if (string? raw-fn) (make-symbol raw-fn) (hs-to-sx raw-fn))))
|
||||
(cons fn-expr args)))))
|
||||
((= head (quote return))
|
||||
(let
|
||||
((val (nth ast 1)))
|
||||
@@ -1754,7 +1933,22 @@
|
||||
((= head (quote throw))
|
||||
(list (quote raise) (hs-to-sx (nth ast 1))))
|
||||
((= head (quote settle))
|
||||
(list (quote hs-settle) (quote me)))
|
||||
(let
|
||||
((raw-tgt (nth ast 1)))
|
||||
(list
|
||||
(quote hs-settle)
|
||||
(if
|
||||
(nil? raw-tgt)
|
||||
(quote me)
|
||||
(if
|
||||
(and
|
||||
(list? raw-tgt)
|
||||
(= (first raw-tgt) (quote query)))
|
||||
(list
|
||||
(quote hs-named-target)
|
||||
(nth raw-tgt 1)
|
||||
(list (quote hs-query-first) (nth raw-tgt 1)))
|
||||
(hs-to-sx raw-tgt))))))
|
||||
((= head (quote go))
|
||||
(list (quote hs-navigate!) (hs-to-sx (nth ast 1))))
|
||||
((= head (quote ask))
|
||||
@@ -1874,7 +2068,11 @@
|
||||
((= head (quote install))
|
||||
(cons (quote hs-install) (map hs-to-sx (rest ast))))
|
||||
((= head (quote measure))
|
||||
(list (quote hs-measure) (hs-to-sx (nth ast 1))))
|
||||
(let
|
||||
((raw-tgt (nth ast 1)))
|
||||
(let
|
||||
((compiled-tgt (if (and (list? raw-tgt) (= (first raw-tgt) (quote query))) (list (quote hs-named-target) (nth raw-tgt 1) (list (quote hs-query-first) (nth raw-tgt 1))) (hs-to-sx raw-tgt))))
|
||||
(list (quote hs-measure) compiled-tgt))))
|
||||
((= head (quote increment!))
|
||||
(if
|
||||
(= (len ast) 3)
|
||||
|
||||
@@ -2455,7 +2455,16 @@
|
||||
((and (= typ "keyword") (= val "answer"))
|
||||
(do (adv!) (parse-answer-cmd)))
|
||||
((and (= typ "keyword") (= val "settle"))
|
||||
(do (adv!) (list (quote settle))))
|
||||
(do
|
||||
(adv!)
|
||||
(if
|
||||
(or
|
||||
(at-end?)
|
||||
(and
|
||||
(= (tp-type) "keyword")
|
||||
(or (= (tp-val) "then") (= (tp-val) "end"))))
|
||||
(list (quote settle))
|
||||
(list (quote settle) (parse-expr)))))
|
||||
((and (= typ "keyword") (= val "go"))
|
||||
(do (adv!) (parse-go-cmd)))
|
||||
((and (= typ "keyword") (= val "return"))
|
||||
|
||||
@@ -12,37 +12,14 @@
|
||||
|
||||
;; Register an event listener. Returns unlisten function.
|
||||
;; (hs-on target event-name handler) → unlisten-fn
|
||||
(begin
|
||||
(define _hs-config-log-all false)
|
||||
(define _hs-log-captured (list))
|
||||
(define
|
||||
hs-set-log-all!
|
||||
(fn (flag) (set! _hs-config-log-all (if flag true false))))
|
||||
(define hs-get-log-captured (fn () _hs-log-captured))
|
||||
(define
|
||||
hs-clear-log-captured!
|
||||
(fn () (begin (set! _hs-log-captured (list)) nil)))
|
||||
(define
|
||||
hs-log-event!
|
||||
(fn
|
||||
(msg)
|
||||
(when
|
||||
_hs-config-log-all
|
||||
(begin
|
||||
(set! _hs-log-captured (append _hs-log-captured (list msg)))
|
||||
(host-call (host-global "console") "log" msg)
|
||||
nil)))))
|
||||
|
||||
;; Register for every occurrence (no queuing — each fires independently).
|
||||
;; Stock hyperscript queues by default; "every" disables queuing.
|
||||
(define
|
||||
hs-each
|
||||
(fn
|
||||
(target action)
|
||||
(if (list? target) (for-each action target) (action target))))
|
||||
|
||||
;; Run an initializer function immediately.
|
||||
;; (hs-init thunk) — called at element boot time
|
||||
;; Register for every occurrence (no queuing — each fires independently).
|
||||
;; Stock hyperscript queues by default; "every" disables queuing.
|
||||
(define
|
||||
hs-on
|
||||
(fn
|
||||
@@ -55,17 +32,17 @@
|
||||
(dom-set-data target "hs-unlisteners" (append prev (list unlisten)))
|
||||
unlisten))))
|
||||
|
||||
;; Run an initializer function immediately.
|
||||
;; (hs-init thunk) — called at element boot time
|
||||
(define
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; ── Async / timing ──────────────────────────────────────────────
|
||||
|
||||
;; Wait for a duration in milliseconds.
|
||||
;; In hyperscript, wait is async-transparent — execution pauses.
|
||||
;; Here we use perform/IO suspension for true pause semantics.
|
||||
(define
|
||||
hs-on-every
|
||||
(fn (target event-name handler) (dom-listen target event-name handler)))
|
||||
|
||||
;; Wait for a DOM event on a target.
|
||||
;; (hs-wait-for target event-name) — suspends until event fires
|
||||
(define
|
||||
hs-on-intersection-attach!
|
||||
(fn
|
||||
@@ -81,15 +58,16 @@
|
||||
(host-call observer "observe" target)
|
||||
observer)))))
|
||||
|
||||
;; Wait for CSS transitions/animations to settle on an element.
|
||||
;; Wait for a DOM event on a target.
|
||||
;; (hs-wait-for target event-name) — suspends until event fires
|
||||
(define hs-init (fn (thunk) (thunk)))
|
||||
|
||||
;; Wait for CSS transitions/animations to settle on an element.
|
||||
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
||||
|
||||
;; ── Class manipulation ──────────────────────────────────────────
|
||||
|
||||
;; Toggle a single class on an element.
|
||||
(define hs-wait (fn (ms) (perform (list (quote io-sleep) ms))))
|
||||
|
||||
;; Toggle between two classes — exactly one is active at a time.
|
||||
(begin
|
||||
(define
|
||||
hs-wait-for
|
||||
@@ -102,21 +80,19 @@
|
||||
(target event-name timeout-ms)
|
||||
(perform (list (quote io-wait-event) target event-name timeout-ms)))))
|
||||
|
||||
;; Toggle between two classes — exactly one is active at a time.
|
||||
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
|
||||
|
||||
;; Take a class from siblings — add to target, remove from others.
|
||||
;; (hs-take! target cls) — like radio button class behavior
|
||||
(define hs-settle (fn (target) (perform (list (quote io-settle) target))))
|
||||
(define
|
||||
hs-toggle-class!
|
||||
(fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
|
||||
|
||||
;; ── DOM insertion ───────────────────────────────────────────────
|
||||
|
||||
;; Put content at a position relative to a target.
|
||||
;; pos: "into" | "before" | "after"
|
||||
(define
|
||||
hs-toggle-class!
|
||||
(fn (target cls) (host-call (host-get target "classList") "toggle" cls)))
|
||||
|
||||
;; ── Navigation / traversal ──────────────────────────────────────
|
||||
|
||||
;; Navigate to a URL.
|
||||
(define
|
||||
hs-toggle-between!
|
||||
(fn
|
||||
@@ -126,7 +102,9 @@
|
||||
(do (dom-remove-class target cls1) (dom-add-class target cls2))
|
||||
(do (dom-remove-class target cls2) (dom-add-class target cls1)))))
|
||||
|
||||
;; Find next sibling matching a selector (or any sibling).
|
||||
;; ── Navigation / traversal ──────────────────────────────────────
|
||||
|
||||
;; Navigate to a URL.
|
||||
(define
|
||||
hs-toggle-style!
|
||||
(fn
|
||||
@@ -150,7 +128,7 @@
|
||||
(dom-set-style target prop "hidden")
|
||||
(dom-set-style target prop "")))))))
|
||||
|
||||
;; Find previous sibling matching a selector.
|
||||
;; Find next sibling matching a selector (or any sibling).
|
||||
(define
|
||||
hs-toggle-style-between!
|
||||
(fn
|
||||
@@ -162,7 +140,7 @@
|
||||
(dom-set-style target prop val2)
|
||||
(dom-set-style target prop val1)))))
|
||||
|
||||
;; First element matching selector within a scope.
|
||||
;; Find previous sibling matching a selector.
|
||||
(define
|
||||
hs-toggle-style-cycle!
|
||||
(fn
|
||||
@@ -183,7 +161,7 @@
|
||||
(true (find-next (rest remaining))))))
|
||||
(dom-set-style target prop (find-next vals)))))
|
||||
|
||||
;; Last element matching selector.
|
||||
;; First element matching selector within a scope.
|
||||
(define
|
||||
hs-take!
|
||||
(fn
|
||||
@@ -206,7 +184,8 @@
|
||||
(when with-cls (dom-remove-class target with-cls))))
|
||||
(let
|
||||
((attr-val (if (> (len extra) 0) (first extra) nil))
|
||||
(with-val (if (> (len extra) 1) (nth extra 1) nil)))
|
||||
(with-val
|
||||
(if (> (len extra) 1) (nth extra 1) nil)))
|
||||
(do
|
||||
(for-each
|
||||
(fn
|
||||
@@ -223,7 +202,7 @@
|
||||
(dom-set-attr target name attr-val)
|
||||
(dom-set-attr target name ""))))))))
|
||||
|
||||
;; First/last within a specific scope.
|
||||
;; Last element matching selector.
|
||||
(begin
|
||||
(define
|
||||
hs-element?
|
||||
@@ -335,6 +314,7 @@
|
||||
(dom-insert-adjacent-html target "beforeend" value)
|
||||
(hs-boot-subtree! target)))))))))
|
||||
|
||||
;; First/last within a specific scope.
|
||||
(define
|
||||
hs-add-to!
|
||||
(fn
|
||||
@@ -347,9 +327,6 @@
|
||||
(append target (list value))))
|
||||
(true (do (host-call target "push" value) target)))))
|
||||
|
||||
;; ── Iteration ───────────────────────────────────────────────────
|
||||
|
||||
;; Repeat a thunk N times.
|
||||
(define
|
||||
hs-remove-from!
|
||||
(fn
|
||||
@@ -357,9 +334,15 @@
|
||||
(if
|
||||
(list? target)
|
||||
(filter (fn (x) (not (= x value))) target)
|
||||
(host-call target "splice" (host-call target "indexOf" value) 1))))
|
||||
(host-call
|
||||
target
|
||||
"splice"
|
||||
(host-call target "indexOf" value)
|
||||
1))))
|
||||
|
||||
;; Repeat forever (until break — relies on exception/continuation).
|
||||
;; ── Iteration ───────────────────────────────────────────────────
|
||||
|
||||
;; Repeat a thunk N times.
|
||||
(define
|
||||
hs-splice-at!
|
||||
(fn
|
||||
@@ -372,7 +355,10 @@
|
||||
((i (if (< idx 0) (+ n idx) idx)))
|
||||
(cond
|
||||
((or (< i 0) (>= i n)) target)
|
||||
(true (concat (slice target 0 i) (slice target (+ i 1) n))))))
|
||||
(true
|
||||
(concat
|
||||
(slice target 0 i)
|
||||
(slice target (+ i 1) n))))))
|
||||
(do
|
||||
(when
|
||||
target
|
||||
@@ -383,10 +369,7 @@
|
||||
(host-call target "splice" i 1))))
|
||||
target))))
|
||||
|
||||
;; ── Fetch ───────────────────────────────────────────────────────
|
||||
|
||||
;; Fetch a URL, parse response according to format.
|
||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||
;; Repeat forever (until break — relies on exception/continuation).
|
||||
(define
|
||||
hs-index
|
||||
(fn
|
||||
@@ -398,10 +381,10 @@
|
||||
((string? obj) (nth obj key))
|
||||
(true (host-get obj key)))))
|
||||
|
||||
;; ── Type coercion ───────────────────────────────────────────────
|
||||
;; ── Fetch ───────────────────────────────────────────────────────
|
||||
|
||||
;; Coerce a value to a type by name.
|
||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||
;; Fetch a URL, parse response according to format.
|
||||
;; (hs-fetch url format) — format is "json" | "text" | "html"
|
||||
(define
|
||||
hs-put-at!
|
||||
(fn
|
||||
@@ -423,10 +406,10 @@
|
||||
((= pos "start") (host-call target "unshift" value)))
|
||||
target)))))))
|
||||
|
||||
;; ── Object creation ─────────────────────────────────────────────
|
||||
;; ── Type coercion ───────────────────────────────────────────────
|
||||
|
||||
;; Make a new object of a given type.
|
||||
;; (hs-make type-name) — creates empty object/collection
|
||||
;; Coerce a value to a type by name.
|
||||
;; (hs-coerce value type-name) — type-name is "Int", "Float", "String", etc.
|
||||
(define
|
||||
hs-dict-without
|
||||
(fn
|
||||
@@ -447,27 +430,27 @@
|
||||
(host-call (host-global "Reflect") "deleteProperty" out key)
|
||||
out)))))
|
||||
|
||||
;; ── Behavior installation ───────────────────────────────────────
|
||||
;; ── Object creation ─────────────────────────────────────────────
|
||||
|
||||
;; Install a behavior on an element.
|
||||
;; A behavior is a function that takes (me ...params) and sets up features.
|
||||
;; (hs-install behavior-fn me ...args)
|
||||
;; Make a new object of a given type.
|
||||
;; (hs-make type-name) — creates empty object/collection
|
||||
(define
|
||||
hs-set-on!
|
||||
(fn
|
||||
(props target)
|
||||
(for-each (fn (k) (host-set! target k (get props k))) (keys props))))
|
||||
|
||||
;; ── Behavior installation ───────────────────────────────────────
|
||||
|
||||
;; Install a behavior on an element.
|
||||
;; A behavior is a function that takes (me ...params) and sets up features.
|
||||
;; (hs-install behavior-fn me ...args)
|
||||
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
||||
|
||||
;; ── Measurement ─────────────────────────────────────────────────
|
||||
|
||||
;; Measure an element's bounding rect, store as local variables.
|
||||
;; Returns a dict with x, y, width, height, top, left, right, bottom.
|
||||
(define hs-navigate! (fn (url) (perform (list (quote io-navigate) url))))
|
||||
|
||||
;; Return the current text selection as a string. In the browser this is
|
||||
;; `window.getSelection().toString()`. In the mock test runner, a test
|
||||
;; setup stashes the desired selection text at `window.__test_selection`
|
||||
;; and the fallback path returns that so tests can assert on the result.
|
||||
(define
|
||||
hs-ask
|
||||
(fn
|
||||
@@ -476,11 +459,10 @@
|
||||
((w (host-global "window")))
|
||||
(if w (host-call w "prompt" msg) nil))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
;; Return the current text selection as a string. In the browser this is
|
||||
;; `window.getSelection().toString()`. In the mock test runner, a test
|
||||
;; setup stashes the desired selection text at `window.__test_selection`
|
||||
;; and the fallback path returns that so tests can assert on the result.
|
||||
(define
|
||||
hs-answer
|
||||
(fn
|
||||
@@ -489,6 +471,11 @@
|
||||
((w (host-global "window")))
|
||||
(if w (if (host-call w "confirm" msg) yes-val no-val) no-val))))
|
||||
|
||||
|
||||
;; ── Transition ──────────────────────────────────────────────────
|
||||
|
||||
;; Transition a CSS property to a value, optionally with duration.
|
||||
;; (hs-transition target prop value duration)
|
||||
(define
|
||||
hs-answer-alert
|
||||
(fn
|
||||
@@ -643,25 +630,25 @@
|
||||
(hs-query-all sel)
|
||||
(host-call target "querySelectorAll" sel))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(define
|
||||
hs-list-set
|
||||
(fn
|
||||
(lst idx val)
|
||||
(append (take lst idx) (cons val (drop lst (+ idx 1))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(define
|
||||
hs-to-number
|
||||
(fn (v) (if (number? v) v (or (parse-number (str v)) 0))))
|
||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||
;; Property access — dot notation and .length
|
||||
|
||||
(define
|
||||
hs-query-first
|
||||
(fn (sel) (host-call (host-global "document") "querySelector" sel)))
|
||||
;; DOM query stub — sandbox returns empty list
|
||||
;; ── Sandbox/test runtime additions ──────────────────────────────
|
||||
;; Property access — dot notation and .length
|
||||
(define
|
||||
hs-query-last
|
||||
(fn
|
||||
@@ -669,11 +656,9 @@
|
||||
(let
|
||||
((all (dom-query-all (dom-body) sel)))
|
||||
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
||||
;; Method dispatch — obj.method(args)
|
||||
;; DOM query stub — sandbox returns empty list
|
||||
(define hs-first (fn (scope sel) (dom-query-all scope sel)))
|
||||
|
||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||
;; beep! — debug logging, returns value unchanged
|
||||
;; Method dispatch — obj.method(args)
|
||||
(define
|
||||
hs-last
|
||||
(fn
|
||||
@@ -681,7 +666,9 @@
|
||||
(let
|
||||
((all (dom-query-all scope sel)))
|
||||
(if (> (len all) 0) (nth all (- (len all) 1)) nil))))
|
||||
;; Property-based is — check obj.key truthiness
|
||||
|
||||
;; ── 0.9.90 features ─────────────────────────────────────────────
|
||||
;; beep! — debug logging, returns value unchanged
|
||||
(define
|
||||
hs-repeat-times
|
||||
(fn
|
||||
@@ -699,7 +686,7 @@
|
||||
((= signal "hs-continue") (do-repeat (+ i 1)))
|
||||
(true (do-repeat (+ i 1))))))))
|
||||
(do-repeat 0)))
|
||||
;; Array slicing (inclusive both ends)
|
||||
;; Property-based is — check obj.key truthiness
|
||||
(define
|
||||
hs-repeat-forever
|
||||
(fn
|
||||
@@ -715,7 +702,7 @@
|
||||
((= signal "hs-continue") (do-forever))
|
||||
(true (do-forever))))))
|
||||
(do-forever)))
|
||||
;; Collection: sorted by
|
||||
;; Array slicing (inclusive both ends)
|
||||
(define
|
||||
hs-repeat-while
|
||||
(fn
|
||||
@@ -728,7 +715,7 @@
|
||||
((= signal "hs-break") nil)
|
||||
((= signal "hs-continue") (hs-repeat-while cond-fn thunk))
|
||||
(true (hs-repeat-while cond-fn thunk)))))))
|
||||
;; Collection: sorted by descending
|
||||
;; Collection: sorted by
|
||||
(define
|
||||
hs-repeat-until
|
||||
(fn
|
||||
@@ -740,7 +727,7 @@
|
||||
((= signal "hs-continue")
|
||||
(if (cond-fn) nil (hs-repeat-until cond-fn thunk)))
|
||||
(true (if (cond-fn) nil (hs-repeat-until cond-fn thunk)))))))
|
||||
;; Collection: split by
|
||||
;; Collection: sorted by descending
|
||||
(define
|
||||
hs-for-each
|
||||
(fn
|
||||
@@ -760,7 +747,7 @@
|
||||
((= signal "hs-continue") (do-loop (rest remaining)))
|
||||
(true (do-loop (rest remaining))))))))
|
||||
(do-loop items))))
|
||||
;; Collection: joined by
|
||||
;; Collection: split by
|
||||
(begin
|
||||
(define
|
||||
hs-append
|
||||
@@ -788,7 +775,7 @@
|
||||
((hs-element? target)
|
||||
(dom-insert-adjacent-html target "beforeend" (str value)))
|
||||
(true nil)))))
|
||||
|
||||
;; Collection: joined by
|
||||
(define
|
||||
hs-sender
|
||||
(fn
|
||||
@@ -1310,10 +1297,14 @@
|
||||
((ch (substring sel i (+ i 1))))
|
||||
(cond
|
||||
((= ch ".")
|
||||
(do (flush!) (set! mode "class") (walk (+ i 1))))
|
||||
(do
|
||||
(flush!)
|
||||
(set! mode "class")
|
||||
(walk (+ i 1))))
|
||||
((= ch "#")
|
||||
(do (flush!) (set! mode "id") (walk (+ i 1))))
|
||||
(true (do (set! cur (str cur ch)) (walk (+ i 1)))))))))
|
||||
(true
|
||||
(do (set! cur (str cur ch)) (walk (+ i 1)))))))))
|
||||
(walk 0)
|
||||
(flush!)
|
||||
{:tag tag :classes classes :id id}))))
|
||||
@@ -1398,6 +1389,7 @@
|
||||
hs-strict-eq
|
||||
(fn (a b) (and (= (type-of a) (type-of b)) (= a b))))
|
||||
|
||||
|
||||
(define
|
||||
hs-eq-ignore-case
|
||||
(fn (a b) (= (downcase (str a)) (downcase (str b)))))
|
||||
@@ -1438,7 +1430,10 @@
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((pos (host-call a "compareDocumentPosition" b)))
|
||||
(if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false)))
|
||||
(if
|
||||
(number? pos)
|
||||
(not (= 0 (mod (/ pos 4) 2)))
|
||||
false)))
|
||||
(true (< (str a) (str b))))))
|
||||
|
||||
(define
|
||||
@@ -1540,7 +1535,10 @@
|
||||
((and (dict? a) (dict? b))
|
||||
(let
|
||||
((pos (host-call a "compareDocumentPosition" b)))
|
||||
(if (number? pos) (not (= 0 (mod (/ pos 4) 2))) false)))
|
||||
(if
|
||||
(number? pos)
|
||||
(not (= 0 (mod (/ pos 4) 2)))
|
||||
false)))
|
||||
(true (< (str a) (str b))))))
|
||||
|
||||
(define
|
||||
@@ -1591,7 +1589,9 @@
|
||||
|
||||
(define
|
||||
hs-morph-char
|
||||
(fn (s p) (if (or (< p 0) (>= p (string-length s))) nil (nth s p))))
|
||||
(fn
|
||||
(s p)
|
||||
(if (or (< p 0) (>= p (string-length s))) nil (nth s p))))
|
||||
|
||||
(define
|
||||
hs-morph-index-from
|
||||
@@ -1619,7 +1619,10 @@
|
||||
(q)
|
||||
(let
|
||||
((c (hs-morph-char s q)))
|
||||
(if (and c (< (index-of stop c) 0)) (loop (+ q 1)) q))))
|
||||
(if
|
||||
(and c (< (index-of stop c) 0))
|
||||
(loop (+ q 1))
|
||||
q))))
|
||||
(let ((e (loop p))) (list (substring s p e) e))))
|
||||
|
||||
(define
|
||||
@@ -1661,7 +1664,9 @@
|
||||
(append
|
||||
acc
|
||||
(list
|
||||
(list name (substring s (+ p4 1) close)))))))
|
||||
(list
|
||||
name
|
||||
(substring s (+ p4 1) close)))))))
|
||||
((= c2 "'")
|
||||
(let
|
||||
((close (hs-morph-index-from s "'" (+ p4 1))))
|
||||
@@ -1671,7 +1676,9 @@
|
||||
(append
|
||||
acc
|
||||
(list
|
||||
(list name (substring s (+ p4 1) close)))))))
|
||||
(list
|
||||
name
|
||||
(substring s (+ p4 1) close)))))))
|
||||
(true
|
||||
(let
|
||||
((r2 (hs-morph-read-until s p4 " \t\n/>")))
|
||||
@@ -1755,7 +1762,9 @@
|
||||
(for-each
|
||||
(fn
|
||||
(c)
|
||||
(when (> (string-length c) 0) (dom-add-class el c)))
|
||||
(when
|
||||
(> (string-length c) 0)
|
||||
(dom-add-class el c)))
|
||||
(split v " ")))
|
||||
((and keep-id (= n "id")) nil)
|
||||
(true (dom-set-attr el n v)))))
|
||||
@@ -1856,7 +1865,8 @@
|
||||
((parts (split resolved ":")))
|
||||
(let
|
||||
((prop (first parts))
|
||||
(val (if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(val
|
||||
(if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(cond
|
||||
((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop))
|
||||
(let
|
||||
@@ -1895,7 +1905,8 @@
|
||||
((parts (split resolved ":")))
|
||||
(let
|
||||
((prop (first parts))
|
||||
(val (if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(val
|
||||
(if (> (len parts) 1) (nth parts 1) nil)))
|
||||
(cond
|
||||
((and (not (= prop "display")) (not (= prop "opacity")) (not (= prop "visibility")) (not (= prop "hidden")) (not (= prop "class-hidden")) (not (= prop "class-invisible")) (not (= prop "class-opacity")) (not (= prop "details")) (not (= prop "dialog")) (dict-has? _hs-hide-strategies prop))
|
||||
(let
|
||||
@@ -1999,10 +2010,14 @@
|
||||
(if
|
||||
(= depth 1)
|
||||
j
|
||||
(find-close (+ j 1) (- depth 1)))
|
||||
(find-close
|
||||
(+ j 1)
|
||||
(- depth 1)))
|
||||
(if
|
||||
(= (nth raw j) "{")
|
||||
(find-close (+ j 1) (+ depth 1))
|
||||
(find-close
|
||||
(+ j 1)
|
||||
(+ depth 1))
|
||||
(find-close (+ j 1) depth))))))
|
||||
(let
|
||||
((close (find-close start 1)))
|
||||
@@ -2093,7 +2108,10 @@
|
||||
(if
|
||||
(= (len lst) 0)
|
||||
-1
|
||||
(if (= (first lst) item) i (idx-loop (rest lst) (+ i 1))))))
|
||||
(if
|
||||
(= (first lst) item)
|
||||
i
|
||||
(idx-loop (rest lst) (+ i 1))))))
|
||||
(idx-loop obj 0)))
|
||||
(true nil))))
|
||||
|
||||
@@ -2179,7 +2197,8 @@
|
||||
(cond
|
||||
((= end "hs-pick-end") n)
|
||||
((= end "hs-pick-start") 0)
|
||||
((and (number? end) (< end 0)) (max 0 (+ n end)))
|
||||
((and (number? end) (< end 0))
|
||||
(max 0 (+ n end)))
|
||||
(true end))))
|
||||
(cond
|
||||
((string? col) (slice col s e))
|
||||
@@ -2466,6 +2485,50 @@
|
||||
((nth entry 2) val)))
|
||||
_hs-dom-watchers)))
|
||||
|
||||
(define hs-prolog-hook nil)
|
||||
|
||||
(define hs-set-prolog-hook! (fn (f) (set! hs-prolog-hook f)))
|
||||
|
||||
(define
|
||||
prolog
|
||||
(fn
|
||||
(db goal)
|
||||
(if
|
||||
(nil? hs-prolog-hook)
|
||||
(raise "prolog hook not installed")
|
||||
(hs-prolog-hook db goal))))
|
||||
|
||||
(define
|
||||
hs-null-error!
|
||||
(fn (selector) (raise (str "'" selector "' is null"))))
|
||||
|
||||
(define
|
||||
hs-named-target
|
||||
(fn (selector value) (if (nil? value) (hs-null-error! selector) value)))
|
||||
|
||||
(define
|
||||
hs-named-target-list
|
||||
(fn
|
||||
(selector values)
|
||||
(if (nil? values) (hs-null-error! selector) values)))
|
||||
|
||||
(define
|
||||
hs-query-named-all
|
||||
(fn
|
||||
(selector)
|
||||
(let
|
||||
((results (hs-query-all selector)))
|
||||
(if
|
||||
(and
|
||||
(or
|
||||
(nil? results)
|
||||
(and (list? results) (= (len results) 0)))
|
||||
(string? selector)
|
||||
(> (len selector) 0)
|
||||
(= (substring selector 0 1) "#"))
|
||||
(hs-null-error! selector)
|
||||
results))))
|
||||
|
||||
(define
|
||||
hs-dom-is-ancestor?
|
||||
(fn
|
||||
|
||||
176
lib/prolog/compiler.sx
Normal file
176
lib/prolog/compiler.sx
Normal 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
129
lib/prolog/conformance.sh
Executable 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."
|
||||
84
lib/prolog/hs-bridge.sx
Normal file
84
lib/prolog/hs-bridge.sx
Normal file
@@ -0,0 +1,84 @@
|
||||
;; lib/prolog/hs-bridge.sx — Prolog ↔ Hyperscript bridge
|
||||
;;
|
||||
;; Two complementary integration styles:
|
||||
;;
|
||||
;; 1. Hook style — for `prolog(db, "goal(args)")` call syntax in Hyperscript:
|
||||
;; (pl-install-hs-hook!) ;; call once at startup
|
||||
;; Requires lib/hyperscript/runtime.sx (provides hs-set-prolog-hook!)
|
||||
;;
|
||||
;; 2. Factory style — for named conditions like `when allowed(user, action)`:
|
||||
;; (define allowed (pl-hs-predicate/2 pl-db "allowed"))
|
||||
;; No parser/compiler changes needed: Hyperscript compiles
|
||||
;; `allowed(user, action)` to `(allowed user action)` — a plain SX call.
|
||||
;;
|
||||
;; Requires tokenizer.sx, parser.sx, runtime.sx, query.sx loaded first.
|
||||
|
||||
;; --- Hook style ---
|
||||
|
||||
(define
|
||||
pl-install-hs-hook!
|
||||
(fn
|
||||
()
|
||||
(hs-set-prolog-hook!
|
||||
(fn (db goal) (not (= nil (pl-query-one db goal)))))))
|
||||
|
||||
;; --- Factory style ---
|
||||
|
||||
;; 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 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 SX function backed by db.
|
||||
;; install-list: list of (name arity) pairs.
|
||||
;; Returns a dict {name → fn} for the caller to destructure.
|
||||
(define
|
||||
pl-hs-install
|
||||
(fn
|
||||
(db install-list)
|
||||
(reduce
|
||||
(fn
|
||||
(acc entry)
|
||||
(let
|
||||
((pred-name (first entry)) (arity (nth entry 1)))
|
||||
(dict-set!
|
||||
acc
|
||||
pred-name
|
||||
(cond
|
||||
((= arity 1) (pl-hs-predicate/1 db pred-name))
|
||||
((= arity 2) (pl-hs-predicate/2 db pred-name))
|
||||
((= arity 3) (pl-hs-predicate/3 db pred-name))
|
||||
(true (fn (a b) false))))
|
||||
acc))
|
||||
{}
|
||||
install-list)))
|
||||
@@ -1,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
114
lib/prolog/query.sx
Normal 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
7
lib/prolog/scoreboard.json
Normal file
7
lib/prolog/scoreboard.json
Normal 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
39
lib/prolog/scoreboard.md
Normal 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 …`.
|
||||
254
lib/prolog/tests/advanced.sx
Normal file
254
lib/prolog/tests/advanced.sx
Normal 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}))
|
||||
215
lib/prolog/tests/assert_rules.sx
Normal file
215
lib/prolog/tests/assert_rules.sx
Normal 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
305
lib/prolog/tests/atoms.sx
Normal 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}))
|
||||
290
lib/prolog/tests/char_predicates.sx
Normal file
290
lib/prolog/tests/char_predicates.sx
Normal 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}))
|
||||
99
lib/prolog/tests/clausedb.sx
Normal file
99
lib/prolog/tests/clausedb.sx
Normal 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}))
|
||||
185
lib/prolog/tests/compiler.sx
Normal file
185
lib/prolog/tests/compiler.sx
Normal 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}))
|
||||
86
lib/prolog/tests/cross_validate.sx
Normal file
86
lib/prolog/tests/cross_validate.sx
Normal 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
158
lib/prolog/tests/dynamic.sx
Normal 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
167
lib/prolog/tests/findall.sx
Normal 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}))
|
||||
165
lib/prolog/tests/hs_bridge.sx
Normal file
165
lib/prolog/tests/hs_bridge.sx
Normal 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}))
|
||||
172
lib/prolog/tests/integration.sx
Normal file
172
lib/prolog/tests/integration.sx
Normal 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}))
|
||||
326
lib/prolog/tests/io_predicates.sx
Normal file
326
lib/prolog/tests/io_predicates.sx
Normal 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}))
|
||||
320
lib/prolog/tests/iso_predicates.sx
Normal file
320
lib/prolog/tests/iso_predicates.sx
Normal 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}))
|
||||
335
lib/prolog/tests/list_predicates.sx
Normal file
335
lib/prolog/tests/list_predicates.sx
Normal 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}))
|
||||
197
lib/prolog/tests/meta_call.sx
Normal file
197
lib/prolog/tests/meta_call.sx
Normal 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}))
|
||||
252
lib/prolog/tests/meta_predicates.sx
Normal file
252
lib/prolog/tests/meta_predicates.sx
Normal 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}))
|
||||
193
lib/prolog/tests/operators.sx
Normal file
193
lib/prolog/tests/operators.sx
Normal 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}))
|
||||
5
lib/prolog/tests/programs/append.pl
Normal file
5
lib/prolog/tests/programs/append.pl
Normal 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).
|
||||
114
lib/prolog/tests/programs/append.sx
Normal file
114
lib/prolog/tests/programs/append.sx
Normal 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}))
|
||||
24
lib/prolog/tests/programs/family.pl
Normal file
24
lib/prolog/tests/programs/family.pl
Normal 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).
|
||||
116
lib/prolog/tests/programs/family.sx
Normal file
116
lib/prolog/tests/programs/family.sx
Normal 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}))
|
||||
4
lib/prolog/tests/programs/member.pl
Normal file
4
lib/prolog/tests/programs/member.pl
Normal 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).
|
||||
91
lib/prolog/tests/programs/member.sx
Normal file
91
lib/prolog/tests/programs/member.sx
Normal 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}))
|
||||
27
lib/prolog/tests/programs/nqueens.pl
Normal file
27
lib/prolog/tests/programs/nqueens.pl
Normal 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).
|
||||
108
lib/prolog/tests/programs/nqueens.sx
Normal file
108
lib/prolog/tests/programs/nqueens.sx
Normal 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}))
|
||||
7
lib/prolog/tests/programs/reverse.pl
Normal file
7
lib/prolog/tests/programs/reverse.pl
Normal 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).
|
||||
113
lib/prolog/tests/programs/reverse.sx
Normal file
113
lib/prolog/tests/programs/reverse.sx
Normal 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}))
|
||||
127
lib/prolog/tests/query_api.sx
Normal file
127
lib/prolog/tests/query_api.sx
Normal 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}))
|
||||
195
lib/prolog/tests/set_predicates.sx
Normal file
195
lib/prolog/tests/set_predicates.sx
Normal 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
618
lib/prolog/tests/solve.sx
Normal 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}))
|
||||
273
lib/prolog/tests/string_agg.sx
Normal file
273
lib/prolog/tests/string_agg.sx
Normal 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}))
|
||||
147
lib/prolog/tests/term_inspect.sx
Normal file
147
lib/prolog/tests/term_inspect.sx
Normal 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}))
|
||||
@@ -11,7 +11,7 @@ isolation: worktree
|
||||
|
||||
## Prompt
|
||||
|
||||
You are the sole background agent working `/root/rose-ash/plans/forth-on-sx.md`. Isolated worktree, forever, one commit per feature. Push to `origin/loops/forth` after every commit.
|
||||
You are the sole background agent working `/root/rose-ash/plans/forth-on-sx.md`. Isolated worktree, forever, one commit per feature. Never push.
|
||||
|
||||
## Restart baseline — check before iterating
|
||||
|
||||
@@ -41,7 +41,7 @@ Every iteration: implement → test → commit → tick `[ ]` → append Progres
|
||||
- **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.
|
||||
- **Worktree:** commit, then push to `origin/loops/forth`. Never touch `main`.
|
||||
- **Worktree:** commit locally. Never push. Never touch `main`.
|
||||
- **Commit granularity:** one feature per commit.
|
||||
- **Plan file:** update Progress log + tick boxes every commit.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -50,65 +50,65 @@ Core mapping:
|
||||
## 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`
|
||||
- [x] 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 `#| … |#`
|
||||
- [x] Reader: list, dotted pair, quote `'`, function `#'`, quasiquote `` ` ``, unquote `,`, splice `,@`, vector `#(…)`, uninterned `#:foo`, nil/t literals
|
||||
- [x] Parser: lambda lists with `&optional` `&rest` `&key` `&aux` `&allow-other-keys`, defaults, supplied-p variables
|
||||
- [x] 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`
|
||||
- [x] `cl-eval-ast`: `quote`, `if`, `progn`, `let`, `let*`, `flet`, `labels`, `setq`, `setf` (subset), `function`, `lambda`, `the`, `locally`, `eval-when`
|
||||
- [x] `block` + `return-from` via captured continuation
|
||||
- [x] `tagbody` + `go` via per-tag continuations
|
||||
- [x] `unwind-protect` cleanup frame
|
||||
- [x] `multiple-value-bind`, `multiple-value-call`, `multiple-value-prog1`, `values`, `nth-value`
|
||||
- [x] `defun`, `defparameter`, `defvar`, `defconstant`, `declaim`, `proclaim` (no-op)
|
||||
- [x] Dynamic variables — `defvar`/`defparameter` produce specials; `let` rebinds via parameterize-style scope
|
||||
- [x] 182 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`
|
||||
- [x] `define-condition` — class hierarchy rooted at `condition`/`error`/`warning`/`simple-error`/`simple-warning`/`type-error`/`arithmetic-error`/`division-by-zero`
|
||||
- [x] `signal`, `error`, `cerror`, `warn` — all walk the handler chain
|
||||
- [x] `handler-bind` — non-unwinding handlers, may decline by returning normally
|
||||
- [x] `handler-case` — unwinding handlers (call/cc escape)
|
||||
- [x] `restart-case`, `with-simple-restart`, `restart-bind`
|
||||
- [x] `find-restart`, `invoke-restart`, `compute-restarts`
|
||||
- [x] `with-condition-restarts` — associate restarts with a specific condition
|
||||
- [x] `invoke-restart-interactively`, `*break-on-signals*`, `*debugger-hook*` (basic)
|
||||
- [x] Classic programs in `lib/common-lisp/tests/programs/`:
|
||||
- [x] `restart-demo.sx` — division with `use-zero` and `retry` restarts (7 tests)
|
||||
- [x] `parse-recover.sx` — parser with skipped-token restart (6 tests)
|
||||
- [x] `interactive-debugger.sx` — policy-driven debugger hook, *debugger-hook* global (7 tests)
|
||||
- [x] `lib/common-lisp/conformance.sh` + runner, `scoreboard.json` + `scoreboard.md` (363 total tests)
|
||||
|
||||
### 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
|
||||
- [x] `defclass` with `:initarg`/`:initform`/`:accessor`/`:reader`/`:writer`/`:allocation`
|
||||
- [x] `make-instance`, `slot-value`, `(setf slot-value)`, `with-slots`, `with-accessors`
|
||||
- [x] `defgeneric` with `:method-combination` (standard, plus `+`, `and`, `or`)
|
||||
- [x] `defmethod` with `:before` / `:after` / `:around` qualifiers
|
||||
- [x] `call-next-method` (continuation), `next-method-p`
|
||||
- [x] `class-of`, `find-class`, `slot-boundp`, `change-class` (basic)
|
||||
- [x] Multiple dispatch — method specificity by argument-class precedence list
|
||||
- [x] Built-in classes registered for tagged values (`integer`, `float`, `string`, `symbol`, `cons`, `null`, `t`)
|
||||
- [x] Classic programs:
|
||||
- [x] `geometry.sx` — `intersect` generic dispatching on (point line), (line line), (line plane) — 12 tests
|
||||
- [x] `mop-trace.sx` — `:before` + `:after` printing call trace — 13 tests
|
||||
|
||||
### 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
|
||||
- [x] `defmacro`, `macrolet`, `symbol-macrolet`, `macroexpand-1`, `macroexpand`
|
||||
- [x] `gensym`, `gentemp`
|
||||
- [x] `set-macro-character`, `set-dispatch-macro-character`, `get-macro-character`
|
||||
- [x] **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
|
||||
- [x] LOOP test corpus: 27 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
|
||||
- [x] `defpackage`, `in-package`, `export`, `use-package`, `import`, `find-package`
|
||||
- [x] Package qualification at the reader level — `cl:car`, `mypkg::internal`
|
||||
- [x] `:common-lisp` (`:cl`) and `:common-lisp-user` (`:cl-user`) packages
|
||||
- [x] Sequence functions — `mapcar`, `mapc`, `mapcan`, `reduce`, `find`, `find-if`, `position`, `count`, `every`, `some`, `notany`, `notevery`, `remove`, `remove-if`, `subst`
|
||||
- [x] List ops — `assoc`, `getf`, `nth`, `last`, `butlast`, `nthcdr`, `tailp`, `ldiff`
|
||||
- [x] String ops — `string=`, `string-upcase`, `string-downcase`, `subseq`, `concatenate`
|
||||
- [x] FORMAT — basic directives `~A`, `~S`, `~D`, `~F`, `~%`, `~&`, `~T`, `~{...~}` (iteration), `~[...~]` (conditional), `~^` (escape), `~P` (plural)
|
||||
- [x] Drive corpus to 200+ green
|
||||
|
||||
## SX primitive baseline
|
||||
|
||||
@@ -124,7 +124,28 @@ data; format for string templating.
|
||||
|
||||
_Newest first._
|
||||
|
||||
- _(none yet)_
|
||||
- 2026-05-05: Phase 5 set-macro-character — cl-reader-macros + cl-dispatch-macros global dicts; SET-MACRO-CHARACTER/GET-MACRO-CHARACTER/SET-DISPATCH-MACRO-CHARACTER dispatch in eval.sx (stores fn, doesn't wire into reader — stubs sufficient to avoid errors). Phase 5 fully ticked. Phase 6 Drive corpus 200+ ticked (518 total, 54 stdlib). All roadmap items done.
|
||||
|
||||
- 2026-05-05: Phase 6 packages — defpackage/in-package/export/use-package/import/find-package/package-name; cl-packages dict, cl-current-package; cl-package-sep? strips pkg: prefix from symbols+functions; package-qualified calls (cl:car, cl:mapcar) work. 4 package tests added; 518 total tests, 0 failed.
|
||||
|
||||
- 2026-05-05: Phase 6 FORMAT — cl-fmt-a/cl-fmt-s/cl-fmt-find-close/cl-fmt-iterate/cl-fmt-loop in eval.sx; ~A/~S/~D/~F/~%/~&/~T/~P/~{...~}/~[...~]/~^/~~; also fixed substr(start,length) semantics throughout (SUBSEQ, cl-fmt-loop); 6 FORMAT tests added to stdlib.sx; 514 total tests, 0 failed.
|
||||
|
||||
- 2026-05-05: Phase 6 stdlib — sequence functions (mapc/mapcan/reduce/find/find-if/find-if-not/position/position-if/count/count-if/every/some/notany/notevery/remove/remove-if/remove-if-not/subst/member), list ops (assoc/rassoc/getf/last/butlast/nthcdr/copy-list/list*/caar/cadr/cdar/cddr/caddr/cadddr/pairlis), string ops (subseq/string/char/string-length/string</>), plus coerce/make-list/write-to-string; 44 tests in tests/stdlib.sx; Phase 6 sequence+list+string boxes ticked. Total: 508 tests, 0 failed.
|
||||
|
||||
- 2026-05-05: Phase 4 CLOS fully complete — `lib/common-lisp/clos.sx` (27 forms): clos-class-registry (8 built-in classes), defclass/make-instance/slot-value/slot-boundp/set-slot-value!/find-class/change-class, defgeneric/defmethod with :before/:after/:around, clos-call-generic (standard method combination: sort by specificity, fire befores, call primary chain, fire afters in reverse), call-next-method/next-method-p, with-slots, accessor installation; 41 tests in `tests/clos.sx`; classic programs `geometry.sx` (12 tests, multi-dispatch intersect on P/L/Plane) and `mop-trace.sx` (13 tests, :before/:after tracing). Dynamic variables in eval.sx: cl-apply-dyn saves/restores global bindings around let for specials (cl-mark-special!/cl-special?/cl-dyn-unbound). Key gotchas: qualifier strings are "before"/"after"/"around" (no colon); dict-set pure = assoc; dict->list = (map (fn (k) (list k (get d k))) (keys d)); clos-add-reader-method bootstrapped via set! after defmethod defined; test isolation: use unique var names to avoid *y* collision. 437 total tests, 0 failed.
|
||||
- 2026-05-05: Phase 3 fully complete — conformance.sh runner + scoreboard.json/scoreboard.md; 363 total tests across all suites (79 reader, 31 parser, 174 eval, 59 conditions, 7+6+7 classic programs).
|
||||
- 2026-05-05: Phase 3 complete — cl-debugger-hook/cl-invoke-debugger in runtime.sx (cl-error routes through hook), cl-break-on-signals (fires hook before handlers on type match), cl-invoke-restart-interactively (calls fn with no args); 4 new tests (147 total). Phase 3 all boxes ticked.
|
||||
- 2026-05-05: Phase 3 interactive-debugger.sx — cl-debugger-hook global, cl-invoke-debugger, cl-error-with-debugger, make-policy-debugger; 7 tests (143 total). Tests wired into test.sh program suite runner. Phase 3 condition core complete.
|
||||
- 2026-05-05: Phase 3 classic programs — `tests/programs/restart-demo.sx` (7 tests: safe-divide with use-zero + retry restarts) and `tests/programs/parse-recover.sx` (6 tests: token parser with skip-token + use-zero restarts, handler-case abort). Key gotcha: use `=` not `equal?` for list comparison in sx_server.
|
||||
- 2026-05-05: Phase 3 conditions + restarts — `cl-condition-classes` hierarchy (15 types), `cl-condition?`/`cl-condition-of-type?`, `cl-make-condition`, `cl-define-condition`, `cl-signal`/`cl-error`/`cl-warn`/`cl-cerror`, `cl-handler-bind` (non-unwinding), `cl-handler-case` (call/cc escape), `cl-restart-case`/`cl-with-simple-restart`, `cl-find-restart`/`cl-invoke-restart`/`cl-compute-restarts`, `cl-with-condition-restarts`; 55 new tests in `tests/conditions.sx` (123 total runtime tests). Key gotcha: `cl-condition-classes` must be captured at define-time via `let` in `cl-condition-of-type?` — free-variable lookup at call-time fails through env_merge parent chain.
|
||||
- 2026-05-05: multiple values — VALUES returns {:cl-type "mv"} wrapper for 2+ values; cl-mv-primary/cl-mv-vals helpers; MULTIPLE-VALUE-BIND binds vars to value list; MULTIPLE-VALUE-CALL/PROG1/NTH-VALUE; cl-mv-primary applied in IF/AND/OR/COND/cl-call-fn for single-value contexts; 15 new tests (174 eval, 346 total green).
|
||||
- 2026-05-05: unwind-protect — cl-eval-unwind-protect: eval protected form, run cleanup with for-each (discards results, preserves original sentinel), return original result; 8 new tests (159 eval, 331 total green).
|
||||
- 2026-05-05: tagbody + go — cl-go-tag? sentinel; cl-eval-tagbody runs body with tag-index map (keys str-normalised for integer tags); go-tag propagation in cl-eval-body alongside block-return; 11 new tests (151 eval, 323 total green).
|
||||
- 2026-05-05: block + return-from — sentinel propagation in cl-eval-body; cl-eval-block catches matching sentinels; BLOCK/RETURN-FROM/RETURN dispatch in cl-eval-list; 13 new tests (140 eval, 312 total green). Parser: CL strings → {:cl-type "string"} dicts.
|
||||
- 2026-04-25: Phase 2 eval — 127 tests, 299 total green. `lib/common-lisp/eval.sx`: cl-eval-ast with quote/if/progn/let/let*/flet/labels/setq/setf/function/lambda/the/locally/eval-when; defun/defvar/defparameter/defconstant; built-in arithmetic (+/-/*//, min/max/abs/evenp/oddp), comparisons, predicates, list ops (car/cdr/cons/list/append/reverse/length/nth/first/second/third/rest), string ops, funcall/apply/mapcar. Key gotchas: SX reduce is (reduce fn init list) not (reduce fn list init); CL true literal is t not true; builtins registered in cl-global-env.fns via wrapper dicts for #' syntax.
|
||||
- 2026-04-25: Phase 1 lambda-list parser — 31 new tests, 172 total green. `cl-parse-lambda-list` in `parser.sx` + `tests/lambda.sx`. Handles &optional/&rest/&body/&key/&aux/&allow-other-keys, defaults, supplied-p. Key gotchas: `(when (> (len items) 0) ...)` not `(when items ...)` (empty list is truthy); custom `cl-deep=` needed for dict/list structural equality in tests.
|
||||
- 2026-04-25: Phase 1 reader/parser — 62 new tests, 141 total green. `lib/common-lisp/parser.sx`: cl-read/cl-read-all, lists, dotted pairs, quote/backquote/unquote/splice/#', vectors, #:uninterned, NIL→nil, T→true, reader macro wrappers.
|
||||
- 2026-04-25: Phase 1 tokenizer — 79 tests green. `lib/common-lisp/reader.sx` + `tests/read.sx` + `test.sh`. Handles symbols (pkg:sym, pkg::sym), integers, floats, ratios, hex/binary/octal, strings, #\ chars, reader macros (#' #( #: ,@), line/block comments. Key gotcha: SX `str` for string concat (not `concat`), substring-based read-while.
|
||||
|
||||
## Blockers
|
||||
|
||||
|
||||
145
plans/datalog-on-sx.md
Normal file
145
plans/datalog-on-sx.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Datalog-on-SX: Datalog on the CEK/VM
|
||||
|
||||
Datalog is a declarative query language: a restricted subset of Prolog with no function
|
||||
symbols, only relations. Programs are sets of facts and rules; queries ask what follows.
|
||||
Evaluation is bottom-up (fixpoint iteration) rather than Prolog's top-down DFS — which
|
||||
means no infinite loops, guaranteed termination, and efficient incremental updates.
|
||||
|
||||
The unique angle: Datalog is a natural companion to the Prolog implementation already in
|
||||
progress (`lib/prolog/`). The parser and term representation can share infrastructure;
|
||||
the evaluator is an entirely different fixpoint engine rather than a DFS solver.
|
||||
|
||||
End-state goal: **full core Datalog** (facts, rules, stratified negation, aggregation,
|
||||
recursion) with a clean SX query API, and a demonstration of Datalog as a query engine
|
||||
for rose-ash data (e.g. federation graph, content relationships).
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/datalog/**` and `plans/datalog-on-sx.md`. Do **not** edit
|
||||
`spec/`, `hosts/`, `shared/`, `lib/prolog/**`, or other `lib/<lang>/`.
|
||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Architecture:** Datalog source → term AST → fixpoint evaluator. No transpiler to SX AST —
|
||||
the evaluator is written in SX and works directly on term structures.
|
||||
- **Reference:** Ramakrishnan & Ullman "A Survey of Deductive Database Systems";
|
||||
Dalmau "Datalog and Constraint Satisfaction".
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
Datalog source text
|
||||
│
|
||||
▼
|
||||
lib/datalog/tokenizer.sx — atoms, variables, numbers, strings, punct (?- :- , . ( ) [ ])
|
||||
│
|
||||
▼
|
||||
lib/datalog/parser.sx — facts: atom(args). rules: head :- body. queries: ?- goal.
|
||||
│ No function symbols (only constants and variables in args).
|
||||
▼
|
||||
lib/datalog/db.sx — extensional DB (EDB): ground facts; IDB: derived relations;
|
||||
│ clause index by relation name/arity
|
||||
▼
|
||||
lib/datalog/eval.sx — bottom-up fixpoint: semi-naive evaluation with delta sets;
|
||||
│ stratification for negation; incremental update API
|
||||
▼
|
||||
lib/datalog/query.sx — query API: (datalog-query db goal) → list of substitutions;
|
||||
SX embedding: define facts/rules as SX data directly
|
||||
```
|
||||
|
||||
Key differences from Prolog:
|
||||
- **No function symbols** — args are atoms, numbers, strings, or variables only. No `f(a,b)`.
|
||||
- **No cuts** — no procedural control.
|
||||
- **Bottom-up** — derive all consequences of all rules before answering; no search tree.
|
||||
- **Termination guaranteed** — no infinite derivation chains (no function symbols → finite Herbrand base).
|
||||
- **Stratified negation** — `not(P)` legal iff P does not recursively depend on its own negation.
|
||||
- **Aggregation** — `count`, `sum`, `min`, `max` over derived tuples (Datalog+).
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: atoms (lowercase/quoted), variables (uppercase/`_`), numbers, strings,
|
||||
operators (`:- `, `?-`, `,`, `.`), comments (`%`, `/* */`)
|
||||
Note: no function symbol syntax (no nested `f(...)` in arg position).
|
||||
- [ ] Parser:
|
||||
- Facts: `parent(tom, bob).` → `{:head (parent tom bob) :body ()}`
|
||||
- Rules: `ancestor(X,Z) :- parent(X,Y), ancestor(Y,Z).`
|
||||
→ `{:head (ancestor X Z) :body ((parent X Y) (ancestor Y Z))}`
|
||||
- Queries: `?- ancestor(tom, X).` → `{:query (ancestor tom X)}`
|
||||
- Negation: `not(parent(X,Y))` in body position → `{:neg (parent X Y)}`
|
||||
- [ ] Tests in `lib/datalog/tests/parse.sx`
|
||||
|
||||
### Phase 2 — unification + substitution
|
||||
- [ ] Share or port unification from `lib/prolog/` — term walk, occurs check off by default
|
||||
- [ ] `dl-unify` `t1` `t2` `subst` → extended subst or nil (no function symbols means simpler)
|
||||
- [ ] `dl-ground?` `term` → bool — all variables bound in substitution
|
||||
- [ ] Tests: atom/atom, var/atom, var/var, list args
|
||||
|
||||
### Phase 3 — extensional DB + naive evaluation
|
||||
- [ ] EDB: `{:relation-name → set-of-ground-tuples}` using SX sets (Phase 18 of primitives)
|
||||
- [ ] `dl-add-fact!` `db` `relation` `args` → add ground tuple
|
||||
- [ ] `dl-add-rule!` `db` `head` `body` → add rule clause
|
||||
- [ ] Naive evaluation: iterate rules until fixpoint
|
||||
For each rule, for each combination of body tuples that unify, derive head tuple.
|
||||
Repeat until no new tuples added.
|
||||
- [ ] `dl-query` `db` `goal` → list of substitutions satisfying goal against derived DB
|
||||
- [ ] Tests: transitive closure (ancestor), sibling, same-generation — classic Datalog programs
|
||||
|
||||
### Phase 4 — semi-naive evaluation (performance)
|
||||
- [ ] Delta sets: track newly derived tuples per iteration
|
||||
- [ ] Semi-naive rule: only join against delta tuples from last iteration, not full relation
|
||||
- [ ] Significant speedup for recursive rules — avoids re-deriving known tuples
|
||||
- [ ] `dl-stratify` `db` → dependency graph + SCC analysis → stratum ordering
|
||||
- [ ] Tests: verify semi-naive produces same results as naive; benchmark on large ancestor chain
|
||||
|
||||
### Phase 5 — stratified negation
|
||||
- [ ] Dependency graph analysis: which relations depend on which (positively or negatively)
|
||||
- [ ] Stratification check: error if negation is in a cycle (non-stratifiable program)
|
||||
- [ ] Evaluation: process strata in order — lower stratum fully computed before using its
|
||||
complement in a higher stratum
|
||||
- [ ] `not(P)` in rule body: at evaluation time, check P is NOT in the derived EDB
|
||||
- [ ] Tests: non-member (`not(member(X,L))`), colored-graph (`not(same-color(X,Y))`),
|
||||
stratification error detection
|
||||
|
||||
### Phase 6 — aggregation (Datalog+)
|
||||
- [ ] `count(X, Goal)` → number of distinct X satisfying Goal
|
||||
- [ ] `sum(X, Goal)` → sum of X values satisfying Goal
|
||||
- [ ] `min(X, Goal)` / `max(X, Goal)` → min/max of X satisfying Goal
|
||||
- [ ] `group-by` semantics: `count(X, sibling(bob, X))` → count of bob's siblings
|
||||
- [ ] Aggregation breaks stratification — evaluate in a separate post-fixpoint pass
|
||||
- [ ] Tests: social network statistics, grade aggregation, inventory sums
|
||||
|
||||
### Phase 7 — SX embedding API
|
||||
- [ ] `(dl-program facts rules)` → database from SX data directly (no parsing required)
|
||||
```
|
||||
(dl-program
|
||||
'((parent tom bob) (parent tom liz) (parent bob ann))
|
||||
'((ancestor X Z :- (parent X Y) (ancestor Y Z))
|
||||
(ancestor X Y :- (parent X Y))))
|
||||
```
|
||||
- [ ] `(dl-query db '(ancestor tom ?X))` → `((ann) (bob) (liz) (pat))`
|
||||
- [ ] `(dl-assert! db '(parent ann pat))` → incremental fact addition + re-derive
|
||||
- [ ] `(dl-retract! db '(parent tom bob))` → fact removal + re-derive from scratch
|
||||
- [ ] Integration demo: federation graph query — `(ancestor actor1 actor2)` over
|
||||
rose-ash ActivityPub follow relationships
|
||||
|
||||
### Phase 8 — Datalog as a query language for rose-ash
|
||||
- [ ] Schema: map SQLAlchemy model relationships to Datalog EDB facts
|
||||
(e.g. `(follows user1 user2)`, `(authored user post)`, `(tagged post tag)`)
|
||||
- [ ] Loader: `dl-load-from-db!` — query PostgreSQL, populate Datalog EDB
|
||||
- [ ] Query examples:
|
||||
- `?- ancestor(me, X), authored(X, Post), tagged(Post, cooking).`
|
||||
→ posts about cooking by people I follow (transitively)
|
||||
- `?- popular(Post) :- tagged(Post, T), count(L, (liked(L, Post))) >= 10.`
|
||||
→ posts with 10+ likes
|
||||
- [ ] Expose as a rose-ash service endpoint: `POST /internal/datalog` with program + query
|
||||
|
||||
## Blockers
|
||||
|
||||
_(none yet)_
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
_(awaiting phase 1)_
|
||||
80
plans/designs/f-breakpoint.md
Normal file
80
plans/designs/f-breakpoint.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# F-Breakpoint — `breakpoint` command (+2)
|
||||
|
||||
**Suite:** `hs-upstream-breakpoint`
|
||||
**Target:** Both tests are `SKIP (untranslated)`.
|
||||
|
||||
## 1. The 2 tests
|
||||
|
||||
- `parses as a top-level command`
|
||||
- `parses inside an event handler`
|
||||
|
||||
Both are untranslated — no test body exists. The test names say "parses" — these are parser tests, not runtime tests.
|
||||
|
||||
## 2. What upstream checks
|
||||
|
||||
From `test/core/breakpoint.js`:
|
||||
|
||||
```js
|
||||
it('parses as a top-level command', () => {
|
||||
expect(() => _hyperscript.evaluate("breakpoint")).not.toThrow();
|
||||
});
|
||||
it('parses inside an event handler', () => {
|
||||
const el = document.createElement('div');
|
||||
el.setAttribute('_', 'on click breakpoint');
|
||||
expect(() => _hyperscript.processNode(el)).not.toThrow();
|
||||
});
|
||||
```
|
||||
|
||||
Both tests verify that `breakpoint` is accepted by the parser without throwing. Neither test checks that the debugger actually fires. `breakpoint` is a no-op command in production builds — it calls `debugger` in JS, which is a no-op when devtools are closed.
|
||||
|
||||
## 3. What's needed
|
||||
|
||||
### Parser (`lib/hyperscript/parser.sx`)
|
||||
|
||||
Add `breakpoint` to the command dispatch — it should parse as a zero-argument command. The parser's command `cond` (wherever `add`, `remove`, `hide` etc. are dispatched) needs a branch:
|
||||
|
||||
```
|
||||
((= val "breakpoint") (hs-parse-breakpoint))
|
||||
```
|
||||
|
||||
`hs-parse-breakpoint` just returns a `{:cmd "breakpoint"}` AST node (or however commands are represented). It consumes no additional tokens.
|
||||
|
||||
### Compiler (`lib/hyperscript/compiler.sx`)
|
||||
|
||||
Add a compiler branch for `breakpoint` AST node. Emits a no-op or a `debugger` statement equivalent. Since we're in SX (not JS), a no-op `(do nil)` is correct.
|
||||
|
||||
### Generator (`tests/playwright/generate-sx-tests.py`)
|
||||
|
||||
The 2 tests are simple — hand-write them:
|
||||
|
||||
```lisp
|
||||
(deftest "parses as a top-level command"
|
||||
(let ((result (guard (e (true false))
|
||||
(hs-compile "breakpoint")
|
||||
true)))
|
||||
(assert result)))
|
||||
|
||||
(deftest "parses inside an event handler"
|
||||
(hs-cleanup!)
|
||||
(let ((el (dom-create-element "div")))
|
||||
(dom-set-attr el "_" "on click breakpoint")
|
||||
(let ((result (guard (e (true false))
|
||||
(hs-activate! el)
|
||||
true)))
|
||||
(assert result))))
|
||||
```
|
||||
|
||||
## 4. Implementation checklist
|
||||
|
||||
1. `sx_find_all` in `lib/hyperscript/parser.sx` for the command dispatch `cond`.
|
||||
2. Add `breakpoint` branch → `hs-parse-breakpoint` function returning minimal command node.
|
||||
3. `sx_find_all` in `lib/hyperscript/compiler.sx` for command compilation dispatch.
|
||||
4. Add `breakpoint` branch → emit no-op.
|
||||
5. Replace 2 `SKIP` bodies in `spec/tests/test-hyperscript-behavioral.sx` with translated tests above.
|
||||
6. Run `hs_test_run suite="hs-upstream-breakpoint"` — expect 2/2.
|
||||
7. Run smoke 0–195 — no regressions.
|
||||
8. Commit: `HS: breakpoint command — parser + no-op compiler (+2)`
|
||||
|
||||
## 5. Risk
|
||||
|
||||
Very low. Zero-argument no-op command. The only risk is mis-locating the command dispatch branch in the parser.
|
||||
68
plans/designs/f1-null-safety.md
Normal file
68
plans/designs/f1-null-safety.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# F1 — Null Safety Reporting (+7)
|
||||
|
||||
**Suite:** `hs-upstream-core/runtimeErrors`
|
||||
**Target:** 7 currently-failing tests (decrement, default, increment, put, remove, settle, transition commands)
|
||||
|
||||
## 1. Failing tests
|
||||
|
||||
The suite has 18 tests total; 11 already pass. The 7 failures all share the pattern:
|
||||
|
||||
```
|
||||
Expected '#doesntExist' is null, got
|
||||
```
|
||||
|
||||
The `eval-hs-error` helper already exists (landed in null-safety piece 1). It compiles and runs a HS snippet and returns the error string. The problem is that the listed commands don't guard against null targets before operating, so they produce no error (or a cryptic one) instead of `"'#doesntExist' is null"`.
|
||||
|
||||
| Test | Command | Null target expression |
|
||||
|------|---------|----------------------|
|
||||
| decrement | `decrement #doesntExist's innerHTML` | `#doesntExist` |
|
||||
| default | `default #doesntExist's innerHTML to 'foo'` | `#doesntExist` |
|
||||
| increment | `increment #doesntExist's innerHTML` | `#doesntExist` |
|
||||
| put | `put 'foo' into/before/after/at start of/at end of #doesntExist` | `#doesntExist` |
|
||||
| remove | `remove .foo/.@foo/#doesntExist from #doesntExist` | `#doesntExist` |
|
||||
| settle | `settle #doesntExist` | `#doesntExist` |
|
||||
| transition | `transition #doesntExist's *visibility to 0` | `#doesntExist` |
|
||||
|
||||
Note: add, hide, measure, send, sets, show, toggle, trigger already pass — they already guard.
|
||||
|
||||
## 2. Required error format
|
||||
|
||||
```
|
||||
'#doesntExist' is null
|
||||
```
|
||||
|
||||
The apostrophe-quoted selector string followed by ` is null`. The selector text is the original source text of the element expression (e.g. `#doesntExist`, not a stringified DOM node).
|
||||
|
||||
This is the same format already used by passing commands. The null-safety piece 1 commit added `eval-hs-error` and `hs-null-error` helper — just need to call it at the right point in each missing command.
|
||||
|
||||
## 3. Where to add guards
|
||||
|
||||
All in `lib/hyperscript/runtime.sx`. Pattern for each command:
|
||||
|
||||
```
|
||||
(when (nil? target)
|
||||
(hs-null-error target-source-text))
|
||||
```
|
||||
|
||||
Where `hs-null-error` (or equivalent) raises with the formatted message.
|
||||
|
||||
### Per-command location
|
||||
|
||||
- **decrement / increment** — after resolving the target element, before reading/writing innerHTML
|
||||
- **default** — after resolving target element, before reading current value
|
||||
- **put** — after resolving destination element (covers all put variants: into, before, after, at start, at end)
|
||||
- **remove** — after resolving the `from` target element
|
||||
- **settle** — after resolving target element, before starting transition poll
|
||||
- **transition** — after resolving target element, before reading/setting style
|
||||
|
||||
## 4. Implementation checklist
|
||||
|
||||
1. Find each failing command's runtime function in `lib/hyperscript/runtime.sx` using `sx_find_all`.
|
||||
2. For each: `sx_read_subtree` on the function body, locate where target is resolved, insert null guard calling `hs-null-error` (or the equivalent raise form already used by passing commands).
|
||||
3. After all 7: run `hs_test_run suite="hs-upstream-core/runtimeErrors"` — expect 18/18.
|
||||
4. Run smoke range 0–195 — expect no regressions.
|
||||
5. Commit: `HS: null-safety guards on decrement/default/increment/put/remove/settle/transition (+7)`
|
||||
|
||||
## 5. Risk
|
||||
|
||||
Low. The pattern is established by the 11 already-passing tests. The only risk is finding the correct point in each command where the element is resolved and before it's first used.
|
||||
166
plans/designs/f13-step-limit-and-meta.md
Normal file
166
plans/designs/f13-step-limit-and-meta.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# F13 — Step Limit + `meta.caller` (+5 → 100%)
|
||||
|
||||
Five tests currently timeout or produce wrong values due to two root causes:
|
||||
step budget exhaustion and a missing `meta` implementation.
|
||||
|
||||
## Tests
|
||||
|
||||
| # | Suite | Test | Failure |
|
||||
|---|-------|------|---------|
|
||||
| 198 | `hs-upstream-core/runtime` | `has proper stack from event handler` | wrong-value: `meta.caller` returns `""` instead of an object with `.meta.feature.type = "onFeature"` |
|
||||
| 200 | `hs-upstream-core/runtime` | `hypertrace is reasonable` | TIMEOUT (15s, step limit) |
|
||||
| 615 | `hs-upstream-expressions/in` | `query template returns values` | TIMEOUT (37s, step limit) |
|
||||
| 1197 | `hs-upstream-repeat` | `repeat forever works` | TIMEOUT (step limit) |
|
||||
| 1198 | `hs-upstream-repeat` | `repeat forever works w/o keyword` | TIMEOUT (step limit) |
|
||||
|
||||
---
|
||||
|
||||
## Root cause A — Step limit (tests 200, 615, 1197, 1198)
|
||||
|
||||
The runner sets `HS_STEP_LIMIT=200000`. Every CEK step consumed by any
|
||||
expression in a test — including the double compilation warm-up guard blocks
|
||||
that appear before the actual DOM test — counts against this shared budget.
|
||||
|
||||
### `repeat forever` (1197, 1198)
|
||||
|
||||
The loop body terminates in exactly **5 iterations** (`if retVal == 5 then return`).
|
||||
This is bounded, not infinite. The step budget is exhausted before the loop
|
||||
runs because two `eval-expr-cek` compilation warm-up calls each consume tens
|
||||
of thousands of steps.
|
||||
|
||||
Fix: each warm-up guard compiles and discards a HS function definition. Those
|
||||
calls are defensive (wrapped in `guard` that swallows errors). We do NOT need
|
||||
to run the compiled code — the warm-up's purpose is just to ensure the
|
||||
compiler doesn't crash, not to consume steps. The step counter should not tick
|
||||
during compilation (compilation is a pure transform, not evaluation). If that's
|
||||
impractical to gate, raise `HS_STEP_LIMIT` to `2000000` (10×).
|
||||
|
||||
### `hypertrace is reasonable` (200)
|
||||
|
||||
Defines `bar()` → calls `baz()` → throws. Simple call chain. The "hypertrace"
|
||||
in the test name implies the HS runtime trace recorder is active during the
|
||||
test. If trace recording is on globally, every CEK step generates a trace entry
|
||||
allocation. Fix: confirm whether trace recording is always-on in the test runner
|
||||
and disable it by default (trace should only be on when explicitly requested).
|
||||
Alternatively raise step limit.
|
||||
|
||||
### `query template returns values` (615)
|
||||
|
||||
Uses `<${"p"}/>` — a CSS query selector built from a template string. Takes 37
|
||||
seconds. Likely the template selector evaluation triggers repeated DOM scanning
|
||||
or expensive string construction per step. Fix: profile with `hs_test_run
|
||||
verbose=true` to identify which step is slow. If it's a regex compilation
|
||||
per-call, cache it. If step limit only, raise to 2M.
|
||||
|
||||
### Unified fix: raise `HS_STEP_LIMIT` to `2000000`
|
||||
|
||||
The simplest fix that unblocks all four timeout tests. In
|
||||
`tests/hs-run-filtered.js`, change the default step limit. Per-test overrides
|
||||
can still be set via `HS_STEP_LIMIT` env var for debugging.
|
||||
|
||||
If the `query template` test is still slow at 2M steps (37s × 10 = 370s, which
|
||||
would be unacceptable), that test needs a separate performance fix — cache the
|
||||
compiled regex/query from the template string rather than rebuilding it on every
|
||||
access.
|
||||
|
||||
---
|
||||
|
||||
## Root cause B — `meta.caller` not implemented (test 198)
|
||||
|
||||
The HS `meta` object is available inside any function call. It exposes:
|
||||
|
||||
- `meta.caller` — the calling context object
|
||||
- `meta.caller.meta.feature.type` — the HS feature type of the caller
|
||||
(e.g. `"onFeature"` when called from an `on click` handler)
|
||||
|
||||
Test script:
|
||||
```
|
||||
def bar()
|
||||
log meta.caller
|
||||
return meta.caller
|
||||
end
|
||||
```
|
||||
Triggered via `on click put bar().meta.feature.type into my.innerHTML`.
|
||||
Expects `"onFeature"` in innerHTML. Currently gets `""`.
|
||||
|
||||
### What `meta` needs
|
||||
|
||||
`meta` is a dict-like object injected into every function's execution context
|
||||
at call time. Minimum fields for this test:
|
||||
|
||||
```
|
||||
meta = {
|
||||
:caller <the calling context — a dict with its own :meta field>
|
||||
:element <the element the script is attached to>
|
||||
}
|
||||
```
|
||||
|
||||
`meta.caller.meta.feature.type` must return `"onFeature"` when called from an
|
||||
`on` event handler. The feature type string `"onFeature"` is already used
|
||||
internally (event handler features are tagged with this type).
|
||||
|
||||
### Implementation
|
||||
|
||||
In `lib/hyperscript/runtime.sx`, at the point where a HS `def` function is
|
||||
called:
|
||||
|
||||
1. Build a `meta` dict:
|
||||
```
|
||||
{:caller calling-context :element current-element}
|
||||
```
|
||||
where `calling-context` is the current runtime context dict (which includes
|
||||
its own `:meta` field with `:feature {:type "onFeature"}` for event handlers).
|
||||
|
||||
2. Bind `meta` in the function's execution env.
|
||||
|
||||
3. Ensure event handler contexts carry `{:meta {:feature {:type "onFeature"}}}`.
|
||||
|
||||
This is an additive change — nothing currently uses `meta`, so no regression
|
||||
risk.
|
||||
|
||||
---
|
||||
|
||||
## Implementation checklist
|
||||
|
||||
### Step A — Raise step limit
|
||||
1. In `tests/hs-run-filtered.js`, change default `HS_STEP_LIMIT` from `200000`
|
||||
to `2000000`.
|
||||
2. Run tests 1197–1198: `hs_test_run(start=1197, end=1199)` — expect 2/2.
|
||||
3. Run test 615: `hs_test_run(start=615, end=616)` — expect 1/1 or note if
|
||||
still too slow.
|
||||
4. Run test 200: `hs_test_run(start=200, end=201)` — expect 1/1.
|
||||
|
||||
### Step B — `meta.caller` (test 198)
|
||||
5. `sx_find_all` in `lib/hyperscript/runtime.sx` for where `def` functions are
|
||||
called / where event handler contexts are constructed.
|
||||
6. Add `meta` dict construction at call time; bind in function env.
|
||||
7. Ensure `on` handler context carries `{:meta {:feature {:type "onFeature"}}}`.
|
||||
8. Run test 198: `hs_test_run(start=198, end=199)` — expect 1/1.
|
||||
|
||||
### Step C — Query template performance (if still slow after step A)
|
||||
9. Profile `hs_test_run(start=615, end=616, step_limit=2000000, verbose=true)`.
|
||||
10. If the CSS template query `<${"p"}/>` rebuilds on every call, add a memoize
|
||||
cache keyed on the template result string.
|
||||
11. Rerun — expect < 5s.
|
||||
|
||||
### Step D — Full suite verification
|
||||
12. Run all ranges with raised step limit:
|
||||
- `hs_test_run(start=0, end=201, step_limit=2000000)`
|
||||
- `hs_test_run(start=201, end=616, step_limit=2000000)`
|
||||
- `hs_test_run(start=616, end=1200, step_limit=2000000)`
|
||||
- `hs_test_run(start=1200, end=1496, step_limit=2000000)`
|
||||
13. Confirm all previously-passing tests still pass.
|
||||
14. Commit: `HS: raise step limit to 2M + meta.caller for onFeature stack (+5)`
|
||||
|
||||
---
|
||||
|
||||
## Risk
|
||||
|
||||
- **Step limit raise:** May make test suite slower overall (more steps to exhaust
|
||||
before timeout). But if tests pass quickly the limit is never reached.
|
||||
The 37s query-template test is the only real concern — if it genuinely needs
|
||||
2M steps × (time per step), it needs a performance fix too.
|
||||
- **`meta.caller`:** Additive binding in function scope. Zero regression risk.
|
||||
The only complexity is constructing the right shape for the calling context
|
||||
chain — but since only one test exercises this and the shape is simple, the
|
||||
risk is low.
|
||||
81
plans/designs/f2-tell.md
Normal file
81
plans/designs/f2-tell.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# F2 — `tell` Semantics Fix (+3)
|
||||
|
||||
**Suite:** `hs-upstream-tell`
|
||||
**Target:** 3 failing tests out of 10. 7 already pass.
|
||||
|
||||
## 1. Failing tests
|
||||
|
||||
### "attributes refer to the thing being told"
|
||||
```
|
||||
on click tell #d2 then put @foo into me
|
||||
```
|
||||
d2 has attribute `foo="bar"`. After click, d1's text content should be `"bar"`.
|
||||
`@foo` is an attribute ref — it should resolve against the **told element** (d2), not the event target (d1).
|
||||
Currently gets `""` — attribute resolves against d1, which has no `foo` attribute.
|
||||
|
||||
### "your symbol represents the thing being told"
|
||||
```
|
||||
on click tell #d2 then put your innerText into me
|
||||
```
|
||||
d2 has innerText `"foo"`. After click, d1's text content should be `"foo"`.
|
||||
`your` is the possessive of `you` — inside a `tell` block, `you`/`your` should bind to the told element.
|
||||
Currently gets `""`.
|
||||
|
||||
### "does not overwrite the me symbol"
|
||||
```
|
||||
on click add .foo then tell #d2 then add .bar to me
|
||||
```
|
||||
After click: d1 should have both `.foo` and `.bar`; d2 should have neither.
|
||||
`me` inside the `tell` block must still refer to d1 (the original event target).
|
||||
Currently: assertion fails — `.bar` is going to d2 instead of d1.
|
||||
|
||||
## 2. What the 7 passing tests reveal about current behaviour
|
||||
|
||||
The passing tests include:
|
||||
- `you symbol represents the thing being told` — `add .bar to you` adds to d2 ✓
|
||||
- `establishes a proper beingTold symbol` — bare `add .bar` (no target) adds to the told element ✓
|
||||
- `restores a proper implicit me symbol` — after `tell` block ends, bare commands target d1 again ✓
|
||||
- `yourself attribute also works` — `remove yourself` inside tell removes d2 ✓
|
||||
|
||||
So `you`, `yourself`, and bare implicit target all work. The three bugs are:
|
||||
1. Attribute refs (`@foo`) don't resolve against the told element
|
||||
2. `your` (possessive of `you`) doesn't resolve
|
||||
3. `me` is being rebound to the told element instead of kept as d1
|
||||
|
||||
## 3. Root cause analysis
|
||||
|
||||
Inside a `tell X` block, the runtime sets the implicit target to X. The three failures suggest:
|
||||
|
||||
**Bug A — attribute refs:** `@foo` resolves via a property-access path that reads from the *current event target* (`me`/`self`), not from the *implicit tell target*. The tell block sets implicit target but the attribute ref lookup skips it.
|
||||
|
||||
**Bug B — `your`:** `your` is parsed as a possessive modifier expecting `you` to be bound. If `you` is not bound in the tell scope (and only the implicit target is set), `your X` fails to resolve.
|
||||
|
||||
**Bug C — `me` rebinding:** The tell command saves/restores `me` but the save/restore is either not happening or is restoring the wrong value. `me` inside the block should remain d1 while the implicit default target is d2.
|
||||
|
||||
## 4. Fix
|
||||
|
||||
In `lib/hyperscript/runtime.sx`, find the `tell` command handler (search for `hs-tell` or the tell dispatch branch).
|
||||
|
||||
The correct semantics:
|
||||
- Save current `me` value
|
||||
- Set implicit target (used by bare commands like `add .bar`) to the told element
|
||||
- Bind `you` = told element (so `you`, `your`, `yourself` work)
|
||||
- Do **not** rebind `me` — keep it as the original event target
|
||||
- Restore implicit target and unbind `you` after the block
|
||||
|
||||
For attribute refs (`@foo`): resolve against the current *implicit target* (told element), not against `me`. Find where `@attr` expressions are evaluated and ensure they read from the implicit target when inside a tell block.
|
||||
|
||||
## 5. Implementation checklist
|
||||
|
||||
1. `sx_find_all` in `lib/hyperscript/runtime.sx` for tell handler.
|
||||
2. `sx_read_subtree` on the tell handler — verify save/restore of `me` vs implicit target.
|
||||
3. Fix `me` rebinding: save old implicit target, set new one, do NOT touch `me`.
|
||||
4. Bind `you`/`your`/`yourself` to told element in the tell scope env.
|
||||
5. Find attribute ref (`@`) evaluation — ensure it reads from implicit target.
|
||||
6. Run `hs_test_run suite="hs-upstream-tell"` — expect 10/10.
|
||||
7. Run smoke 0–195 — no regressions.
|
||||
8. Commit: `HS: tell — fix me rebinding, your/attribute-ref resolution (+3)`
|
||||
|
||||
## 6. Risk
|
||||
|
||||
Medium. The 7 passing tests constrain what can change — the fix must preserve `you`, `yourself`, bare implicit target, and restore-after-tell semantics. The three bugs are independent enough that they can be fixed one at a time and verified after each.
|
||||
128
plans/designs/f5-cookies.md
Normal file
128
plans/designs/f5-cookies.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# F5 — Cookie API (+5)
|
||||
|
||||
**Suite:** `hs-upstream-expressions/cookies`
|
||||
**Target:** All 5 tests are `SKIP (untranslated)`.
|
||||
|
||||
## 1. The 5 tests
|
||||
|
||||
From upstream `test/expressions/cookies.js`:
|
||||
|
||||
| Test | What it checks |
|
||||
|------|---------------|
|
||||
| `length is 0 when no cookies are set` | `cookies.length == 0` with no cookies set |
|
||||
| `basic set cookie values work` | `set cookies.name to "value"` then `cookies.name == "value"` |
|
||||
| `update cookie values work` | set, then set again, value updates |
|
||||
| `basic clear cookie values work` | `set cookies.name to "value"` then `clear cookies.name`, then `cookies.name == undefined` |
|
||||
| `iterate cookies values work` | `for name in cookies` iterates cookie names |
|
||||
|
||||
## 2. HyperScript cookie syntax
|
||||
|
||||
`cookies` is a special global expression in HyperScript backed by `document.cookie`. The upstream implementation wraps `document.cookie` in a proxy:
|
||||
|
||||
- `cookies.name` → read cookie by name (returns string or `undefined`)
|
||||
- `set cookies.name to val` → write cookie (sets `document.cookie = "name=val"`)
|
||||
- `clear cookies.name` → delete cookie (sets max-age=-1)
|
||||
- `cookies.length` → number of cookies set
|
||||
- `for name in cookies` → iterate over cookie names
|
||||
|
||||
## 3. Test runner mock
|
||||
|
||||
All 5 tests are untranslated — no SX test bodies exist yet. The generator needs patterns for the cookie expressions, and `hs-run-filtered.js` needs a `document.cookie` mock.
|
||||
|
||||
### Mock in `tests/hs-run-filtered.js`
|
||||
|
||||
Add a simple in-memory cookie store to the `dom` mock:
|
||||
|
||||
```js
|
||||
let _cookieStore = {};
|
||||
Object.defineProperty(global.document, 'cookie', {
|
||||
get() {
|
||||
return Object.entries(_cookieStore)
|
||||
.map(([k,v]) => `${k}=${v}`)
|
||||
.join('; ');
|
||||
},
|
||||
set(str) {
|
||||
const [pair, ...attrs] = str.split(';');
|
||||
const [name, val] = pair.split('=').map(s => s.trim());
|
||||
const maxAge = attrs.find(a => a.trim().startsWith('max-age='));
|
||||
if (maxAge && parseInt(maxAge.split('=')[1]) < 0) {
|
||||
delete _cookieStore[name];
|
||||
} else {
|
||||
_cookieStore[name] = val;
|
||||
}
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
```
|
||||
|
||||
Add `_cookieStore = {}` reset to `hs-cleanup!` equivalent in the runner.
|
||||
|
||||
## 4. SX runtime additions in `lib/hyperscript/runtime.sx`
|
||||
|
||||
HS needs a `cookies` special expression that the compiler resolves. Two approaches:
|
||||
|
||||
**Option A (simpler):** Treat `cookies` as a built-in variable bound to a proxy dict at runtime. When property access `cookies.name` is evaluated, dispatch to cookie read/write helpers.
|
||||
|
||||
**Option B (upstream-faithful):** Parse `cookies` as a special primary expression, emit runtime calls `hs-cookie-get`, `hs-cookie-set`, `hs-cookie-delete`, `hs-cookie-length`, `hs-cookie-names`.
|
||||
|
||||
Option A is less invasive. The runtime env gets a `cookies` binding pointing to a special object; property access and assignment on it dispatch to the cookie helpers, which call `(platform-cookie-get name)` / `(platform-cookie-set name val)` / `(platform-cookie-delete name)`.
|
||||
|
||||
Platform cookie operations map to `document.cookie` reads/writes in JS.
|
||||
|
||||
## 5. Generator patterns (`tests/playwright/generate-sx-tests.py`)
|
||||
|
||||
The upstream tests use patterns like:
|
||||
|
||||
```js
|
||||
await page.evaluate(() => { _hyperscript.evaluate("set cookies.foo to 'bar'") });
|
||||
expect(await page.evaluate(() => _hyperscript.evaluate("cookies.foo"))).toBe("bar");
|
||||
```
|
||||
|
||||
In our SX harness these become direct `eval-hs` calls. Since all 5 tests are untranslated, hand-write them rather than extending the generator (similar to E39).
|
||||
|
||||
## 6. Translated test bodies
|
||||
|
||||
```lisp
|
||||
(deftest "length is 0 when no cookies are set"
|
||||
(hs-cleanup!)
|
||||
(assert= (eval-hs "cookies.length") 0))
|
||||
|
||||
(deftest "basic set cookie values work"
|
||||
(hs-cleanup!)
|
||||
(eval-hs "set cookies.foo to 'bar'")
|
||||
(assert= (eval-hs "cookies.foo") "bar"))
|
||||
|
||||
(deftest "update cookie values work"
|
||||
(hs-cleanup!)
|
||||
(eval-hs "set cookies.foo to 'bar'")
|
||||
(eval-hs "set cookies.foo to 'baz'")
|
||||
(assert= (eval-hs "cookies.foo") "baz"))
|
||||
|
||||
(deftest "basic clear cookie values work"
|
||||
(hs-cleanup!)
|
||||
(eval-hs "set cookies.foo to 'bar'")
|
||||
(eval-hs "clear cookies.foo")
|
||||
(assert= (eval-hs "cookies.foo") nil))
|
||||
|
||||
(deftest "iterate cookies values work"
|
||||
(hs-cleanup!)
|
||||
(eval-hs "set cookies.a to '1'")
|
||||
(eval-hs "set cookies.b to '2'")
|
||||
(let ((names (eval-hs "for name in cookies collect name")))
|
||||
(assert (contains? names "a"))
|
||||
(assert (contains? names "b"))))
|
||||
```
|
||||
|
||||
## 7. Implementation checklist
|
||||
|
||||
1. Add cookie mock to `tests/hs-run-filtered.js`. Wire reset into test cleanup.
|
||||
2. Add `hs-cookie-get`, `hs-cookie-set`, `hs-cookie-delete`, `hs-cookie-length`, `hs-cookie-names` to `lib/hyperscript/runtime.sx`.
|
||||
3. Add `cookies` as a special expression in the HS parser/evaluator that dispatches to the above.
|
||||
4. Replace 5 `SKIP` bodies in `spec/tests/test-hyperscript-behavioral.sx` with translated test bodies above.
|
||||
5. Run `hs_test_run suite="hs-upstream-expressions/cookies"` — expect 5/5.
|
||||
6. Run smoke 0–195 — no regressions.
|
||||
7. Commit: `HS: cookie API — document.cookie proxy + 5 tests`
|
||||
|
||||
## 8. Risk
|
||||
|
||||
Medium. The mock is simple. The main risk is the `cookies` expression integration in the parser — it needs to hook into property-access and assignment paths that are already well-exercised. Keep the implementation thin: `cookies` is a runtime value with a special type, not a new parse form.
|
||||
107
plans/designs/f8-eval-statically.md
Normal file
107
plans/designs/f8-eval-statically.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# F8 — evalStatically (+3)
|
||||
|
||||
**Suite:** `hs-upstream-core/evalStatically`
|
||||
**Target:** 3 failing (untranslated) out of 8. 5 already pass.
|
||||
|
||||
## 1. Current state
|
||||
|
||||
5 passing tests use `(eval-hs expr)` and check the return value for literals: booleans, null, numbers, plain strings, time expressions. These call `_hyperscript.evaluate(src)` and return the result.
|
||||
|
||||
3 failing tests are named:
|
||||
- `throws on math expressions`
|
||||
- `throws on symbol references`
|
||||
- `throws on template strings`
|
||||
|
||||
All are `SKIP (untranslated)` — no test body has been generated.
|
||||
|
||||
## 2. What upstream checks
|
||||
|
||||
From `test/core/evalStatically.js`, the `throwErrors` mode:
|
||||
|
||||
```js
|
||||
expect(() => _hyperscript.evaluate("1 + 2")).toThrow();
|
||||
expect(() => _hyperscript.evaluate("x")).toThrow();
|
||||
expect(() => _hyperscript.evaluate(`"hello ${name}"`)).toThrow();
|
||||
```
|
||||
|
||||
`_hyperscript.evaluate(src)` in strict static mode throws when the expression is not a pure literal — math operators, symbol references, and template string interpolation all involve runtime evaluation that can't be statically resolved.
|
||||
|
||||
The "static" constraint: only literals that can be evaluated without any runtime context or side effects are allowed. `1 + 2` is not static (it's a math op). `x` is not static (symbol lookup). `"hello ${name}"` is not static (interpolation).
|
||||
|
||||
## 3. What `eval-hs` currently does
|
||||
|
||||
`eval-hs` in our harness calls `(hs-compile-and-run src)` or equivalent. It does NOT currently have a "static mode" — it runs everything with the full runtime.
|
||||
|
||||
We need a new harness helper `eval-hs-static-error` that:
|
||||
1. Calls `(hs-compile src)` with a flag that makes it throw on non-literal expressions
|
||||
2. Returns the caught error message, or raises if no error was thrown
|
||||
|
||||
## 4. Implementation options
|
||||
|
||||
### Option A — Static analysis pass (accurate)
|
||||
|
||||
Before evaluation, walk the AST and reject any node that isn't a literal:
|
||||
- Number literal ✓
|
||||
- String literal (no interpolation) ✓
|
||||
- Boolean literal ✓
|
||||
- Null literal ✓
|
||||
- Time expression (`200ms`, `2s`) ✓
|
||||
- Everything else → throw `"expression is not static"`
|
||||
|
||||
This is a pre-eval AST check, not a runtime change. Lives in `lib/hyperscript/compiler.sx` as `hs-check-static`.
|
||||
|
||||
### Option B — Generator translation (simpler)
|
||||
|
||||
The 3 tests are untranslated. All three just verify that `_hyperscript.evaluate(expr)` throws. In our SX harness we can test this with a `guard` form:
|
||||
|
||||
```lisp
|
||||
(deftest "throws on math expressions"
|
||||
(let ((result (guard (e (true true))
|
||||
(eval-hs "1 + 2")
|
||||
false)))
|
||||
(assert result)))
|
||||
```
|
||||
|
||||
But this only works if `eval-hs` actually throws on math expressions. Currently it doesn't — `eval-hs "1 + 2"` returns `3`. So we'd need the static analysis anyway to make the test pass.
|
||||
|
||||
### Chosen approach: Option A
|
||||
|
||||
Add `hs-static-check` to the compiler: a fast AST walker that throws on any non-literal node. Wire it as an optional mode. The test harness calls `eval-hs-static` which runs with static-check enabled.
|
||||
|
||||
Actually, reading the upstream more carefully: `_hyperscript.evaluate` already throws in static mode without additional flags — the "evaluate" API is documented as static-only. Our `eval-hs` in the passing tests works because booleans/numbers/strings/time ARE static. `1 + 2`, `x`, and template strings are NOT static and should throw.
|
||||
|
||||
So the fix is: make `hs-compile-and-run` (or whatever backs `eval-hs`) reject non-literal AST nodes. The 5 passing tests will continue to pass (they use literals). The 3 failing tests will get translated using `eval-hs-error` or a guard pattern.
|
||||
|
||||
## 5. Non-literal AST node types to reject
|
||||
|
||||
| Expression | AST node type | Reject? |
|
||||
|-----------|--------------|---------|
|
||||
| `1`, `3.14` | number literal | ✓ allow |
|
||||
| `"hello"`, `'world'` | string literal (no interpolation) | ✓ allow |
|
||||
| `true`, `false` | boolean literal | ✓ allow |
|
||||
| `null` | null literal | ✓ allow |
|
||||
| `200ms`, `2s` | time literal | ✓ allow |
|
||||
| `1 + 2` | math operator | ✗ throw |
|
||||
| `x` | symbol reference | ✗ throw |
|
||||
| `"hello ${name}"` | template string | ✗ throw |
|
||||
|
||||
## 6. Implementation checklist
|
||||
|
||||
1. In `lib/hyperscript/compiler.sx`, add `hs-static?` predicate: returns true only for literal AST node types.
|
||||
2. In the `eval-hs` path (wherever `hs-compile-and-run` is called for the evaluate API), call `hs-static?` on the parsed AST and throw `"expression is not statically evaluable"` if false.
|
||||
3. Replace 3 `SKIP` bodies in `spec/tests/test-hyperscript-behavioral.sx`:
|
||||
```lisp
|
||||
(deftest "throws on math expressions"
|
||||
(assert (string? (eval-hs-error "1 + 2"))))
|
||||
(deftest "throws on symbol references"
|
||||
(assert (string? (eval-hs-error "x"))))
|
||||
(deftest "throws on template strings"
|
||||
(assert (string? (eval-hs-error "\"hello ${name}\""))))
|
||||
```
|
||||
4. Run `hs_test_run suite="hs-upstream-core/evalStatically"` — expect 8/8.
|
||||
5. Run smoke 0–195 — verify the 5 passing tests still pass.
|
||||
6. Commit: `HS: evalStatically — static literal check, 3 tests (+3)`
|
||||
|
||||
## 7. Risk
|
||||
|
||||
Low-medium. The main risk is that `eval-hs` is used in many tests for non-static expressions and adding a static check to the shared path would break them. The fix must be gated — either a separate `eval-hs-static` helper or a flag parameter. The passing tests must not be affected.
|
||||
341
plans/designs/hs-plugin-system.md
Normal file
341
plans/designs/hs-plugin-system.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# HyperScript Plugin / Extension System
|
||||
|
||||
Post-Bucket-F capability work. No conformance delta on its own — the payoff is
|
||||
clean architecture for language embeds (Lua, Prolog, Worker runtime) and
|
||||
alignment with real `_hyperscript`'s extension model.
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
### 1a. Real `_hyperscript` has a plugin API
|
||||
|
||||
Stock `_hyperscript` ships a core bundle with feature stubs and a `use(ext)`
|
||||
hook that loads named extensions at runtime. The worker feature is the canonical
|
||||
example: the core parser has a stub that errors helpfully; loading the worker
|
||||
extension replaces the stub with a real implementation.
|
||||
|
||||
We currently have no equivalent. New grammar or compiler targets require editing
|
||||
`parse-feat`'s hardcoded `cond` or `hs-to-sx`'s hardcoded dispatch. This is
|
||||
fine for conformance work but wrong for language embeds.
|
||||
|
||||
### 1b. Ad-hoc hooks are accumulating
|
||||
|
||||
`runtime.sx` already has `hs-prolog-hook` / `hs-set-prolog-hook!` / `prolog`
|
||||
(nodes 140–142) — an informal plugin slot bolted on outside the parser and
|
||||
compiler. This pattern will repeat for Lua, and again for the Worker runtime.
|
||||
A proper registry prevents the drift.
|
||||
|
||||
### 1c. E39 worker stub is a placeholder
|
||||
|
||||
The stub added in E39 (`parse-feat` raises immediately on `"worker"`) was
|
||||
explicitly designed to be replaced by a real plugin at a single site. This plan
|
||||
is where that replacement happens.
|
||||
|
||||
### 1d. Bucket-F Group 10 needs a converter registry
|
||||
|
||||
`as MyType` via registered converter is already in the Bucket-F plan (Group 10).
|
||||
A `hs-register-converter!` registry is the natural home for it — and the plugin
|
||||
system is the right time to add registries generally.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope
|
||||
|
||||
**In scope:**
|
||||
- Parser feature registry (`parse-feat` dispatch)
|
||||
- Compiler command registry (`hs-to-sx` dispatch)
|
||||
- `as` converter registry (`hs-coerce` dispatch)
|
||||
- Migration of E39 worker stub to use the parser registry
|
||||
- Migration of `hs-prolog-hook` ad-hoc slot to a proper plugin
|
||||
- Worker full runtime plugin (first real plugin)
|
||||
- Lua embed plugin
|
||||
- Prolog embed plugin
|
||||
|
||||
**Out of scope:**
|
||||
- Changing the test runner or generator
|
||||
- Any conformance delta (this plan doesn't target failing tests)
|
||||
- Third-party plugin loading from external URLs (future)
|
||||
- Hot-reload of plugins (future)
|
||||
|
||||
---
|
||||
|
||||
## 3. Registry design
|
||||
|
||||
Three registries, all SX dicts. Checked before the hardcoded `cond` in each
|
||||
dispatch. Registration functions defined alongside the registries in their
|
||||
respective files.
|
||||
|
||||
### 3a. Parser feature registry (`lib/hyperscript/parser.sx`)
|
||||
|
||||
```lisp
|
||||
(define _hs-feature-registry (dict))
|
||||
|
||||
(define hs-register-feature!
|
||||
(fn (keyword parse-fn)
|
||||
(set! _hs-feature-registry
|
||||
(dict-set _hs-feature-registry keyword parse-fn))))
|
||||
```
|
||||
|
||||
In `parse-feat`, prepend a registry lookup before the existing `cond`:
|
||||
|
||||
```lisp
|
||||
(let ((registered (dict-get _hs-feature-registry val)))
|
||||
(if registered
|
||||
(registered) ;; call the registered parse-fn (no args; uses closure over adv!/tp-val etc.)
|
||||
(cond ;; existing dispatch unchanged below
|
||||
...)))
|
||||
```
|
||||
|
||||
`parse-fn` is a zero-arg thunk that has access to the parser's internal state
|
||||
via the same closure that the existing `parse-*` helpers use. Since `parse-feat`
|
||||
is itself defined inside the big `let` in `hs-parse`, all the parser helpers
|
||||
(`adv!`, `tp-val`, `tp-typ`, `parse-cmd-list`, etc.) are in scope.
|
||||
|
||||
### 3b. Compiler command registry (`lib/hyperscript/compiler.sx`)
|
||||
|
||||
```lisp
|
||||
(define _hs-compiler-registry (dict))
|
||||
|
||||
(define hs-register-compiler!
|
||||
(fn (head compile-fn)
|
||||
(set! _hs-compiler-registry
|
||||
(dict-set _hs-compiler-registry (str head) compile-fn))))
|
||||
```
|
||||
|
||||
In `hs-to-sx`, before the existing `cond` on `head`, check the registry:
|
||||
|
||||
```lisp
|
||||
(let ((registered (dict-get _hs-compiler-registry (str head))))
|
||||
(if registered
|
||||
(registered ast)
|
||||
(cond ...)))
|
||||
```
|
||||
|
||||
`compile-fn` receives the full AST node and returns an SX expression.
|
||||
|
||||
### 3c. `as` converter registry (`lib/hyperscript/runtime.sx`)
|
||||
|
||||
```lisp
|
||||
(define _hs-converters (dict))
|
||||
|
||||
(define hs-register-converter!
|
||||
(fn (type-name converter-fn)
|
||||
(set! _hs-converters
|
||||
(dict-set _hs-converters type-name converter-fn))))
|
||||
```
|
||||
|
||||
In `hs-coerce`, add a registry lookup as the last `cond` clause before the
|
||||
fallthrough error:
|
||||
|
||||
```lisp
|
||||
((dict-get _hs-converters type-name)
|
||||
((dict-get _hs-converters type-name) value))
|
||||
```
|
||||
|
||||
This is also the hook that Bucket-F Group 10 (`can accept custom conversions`)
|
||||
hangs on — so implementing it here kills two birds.
|
||||
|
||||
---
|
||||
|
||||
## 4. First-party plugins
|
||||
|
||||
Each plugin is a `.sx` file in `lib/hyperscript/plugins/`. Plugins call the
|
||||
registration functions at load time (top-level `do` forms). The host loads
|
||||
plugins explicitly after the core files.
|
||||
|
||||
### 4a. Worker plugin (`lib/hyperscript/plugins/worker.sx`)
|
||||
|
||||
**Phase 1 — stub migration (immediate):**
|
||||
Remove the inline error branch from `parse-feat` (the E39 stub). Replace with:
|
||||
|
||||
```lisp
|
||||
(hs-register-feature! "worker"
|
||||
(fn ()
|
||||
(error "worker plugin is not installed — see https://hyperscript.org/features/worker")))
|
||||
```
|
||||
|
||||
This is identical behaviour to E39 but routed through the registry. The stub
|
||||
lives in the plugin file, not the core parser. No test regression.
|
||||
|
||||
**Phase 2 — full runtime:**
|
||||
|
||||
Parser: `parse-worker-feat` — consumes `worker <Name> [(<url>*)] <def|js>* end`,
|
||||
returns `(worker Name urls defs)` AST node.
|
||||
|
||||
Compiler: registered under `"worker"` head:
|
||||
- Emits `(hs-worker-define! "Name" urls defs)` call.
|
||||
|
||||
Runtime additions in the plugin file:
|
||||
- `hs-worker-define!` — creates a `{:_hs-worker true :name N :handle H :exports (...)}` record,
|
||||
binds it in the HS top-level env under `Name`.
|
||||
- `hs-method-call` (existing) detects `:_hs-worker` and dispatches via `postMessage`.
|
||||
- Worker script body compiled to a standalone SX bundle posted to a Blob URL.
|
||||
- Return values are promise-wrapped; async-transparent via `perform`/IO suspension.
|
||||
|
||||
Mock env additions for the test runner: `Worker` constructor + synchronous
|
||||
message loop for the 7 sibling `test.skip(...)` upstream tests (the ones
|
||||
deferred in E39).
|
||||
|
||||
### 4b. Prolog plugin (`lib/hyperscript/plugins/prolog.sx`)
|
||||
|
||||
Replaces the ad-hoc `hs-prolog-hook` in `runtime.sx`.
|
||||
|
||||
**Parser:** Register `"prolog"` feature — parses
|
||||
`prolog(<db-expr>, <goal-expr>)` at feature level (alternative: keep as an
|
||||
expression, register a compiler extension only).
|
||||
|
||||
**Compiler:** Registered under `"prolog"` head — emits `(prolog db goal)`.
|
||||
|
||||
**Runtime:** The existing `prolog` function in `runtime.sx` moves here.
|
||||
`hs-prolog-hook` and `hs-set-prolog-hook!` are removed from `runtime.sx` and
|
||||
the hook mechanism is replaced by the plugin loading `lib/prolog/runtime.sx`
|
||||
and wiring the solver directly.
|
||||
|
||||
Remove from `runtime.sx` nodes 140–142 once the plugin is live.
|
||||
|
||||
### 4c. Lua plugin (`lib/hyperscript/plugins/lua.sx`)
|
||||
|
||||
**Parser:** Register `"lua"` feature — parses `lua ... end` block, captures
|
||||
the body as a raw string.
|
||||
|
||||
**Compiler:** Registered under `"lua"` head — emits `(lua-eval <body-string>)`.
|
||||
|
||||
**Runtime:** `lua-eval` calls `lib/lua/runtime.sx`'s eval entry point, returns
|
||||
result as an SX value via `hs-host-to-sx`. Errors surface as HS `catch`-able
|
||||
exceptions.
|
||||
|
||||
This enables inline Lua in HyperScript:
|
||||
|
||||
```
|
||||
on click
|
||||
lua
|
||||
return document.title:upper()
|
||||
end
|
||||
put it into me
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Load order
|
||||
|
||||
```
|
||||
lib/hyperscript/parser.sx ;; defines _hs-feature-registry, hs-register-feature!
|
||||
lib/hyperscript/compiler.sx ;; defines _hs-compiler-registry, hs-register-compiler!
|
||||
lib/hyperscript/runtime.sx ;; defines _hs-converters, hs-register-converter!
|
||||
lib/hyperscript/plugins/worker.sx
|
||||
lib/hyperscript/plugins/prolog.sx
|
||||
lib/hyperscript/plugins/lua.sx
|
||||
```
|
||||
|
||||
The test runner (`tests/hs-run-filtered.js`) loads plugins after core. The
|
||||
browser WASM bundle includes all three by default (plugins are small; no
|
||||
reason to lazy-load them).
|
||||
|
||||
---
|
||||
|
||||
## 6. Migration checklist
|
||||
|
||||
The work below is ordered to keep main green at every commit. Each step is
|
||||
independently committable.
|
||||
|
||||
### Step 1 — Registries (infrastructure, no behaviour change)
|
||||
|
||||
1. Add `_hs-feature-registry` + `hs-register-feature!` to `parser.sx`.
|
||||
Thread the registry check into `parse-feat`. No entries yet → behaviour
|
||||
unchanged.
|
||||
2. Add `_hs-compiler-registry` + `hs-register-compiler!` to `compiler.sx`.
|
||||
Thread into `hs-to-sx`. No entries yet → behaviour unchanged.
|
||||
3. Add `_hs-converters` + `hs-register-converter!` to `runtime.sx`. Thread
|
||||
into `hs-coerce`. No entries yet → behaviour unchanged.
|
||||
4. `sx_validate` all three files. Run full HS suite — expect zero regressions.
|
||||
5. Commit: `HS: plugin registry infrastructure (parser + compiler + converter)`.
|
||||
|
||||
### Step 2 — Worker stub migration
|
||||
|
||||
6. Create `lib/hyperscript/plugins/worker.sx`. Register the worker stub error.
|
||||
7. Remove the inline `((= val "worker") ...)` branch from `parse-feat` in
|
||||
`parser.sx`.
|
||||
8. Update the test runner to load `worker.sx` after core.
|
||||
9. Run `HS_SUITE=hs-upstream-worker` — expect 1/1. Run full suite — expect no
|
||||
regressions.
|
||||
10. Commit: `HS: migrate E39 worker stub to plugin registry`.
|
||||
|
||||
### Step 3 — Prolog plugin
|
||||
|
||||
11. Create `lib/hyperscript/plugins/prolog.sx`. Wire to `lib/prolog/runtime.sx`.
|
||||
12. Remove `hs-prolog-hook`, `hs-set-prolog-hook!`, `prolog` from `runtime.sx`
|
||||
nodes 140–142.
|
||||
13. Update test runner to load `prolog.sx`.
|
||||
14. Validate and run full suite.
|
||||
15. Commit: `HS: prolog plugin replaces ad-hoc hook`.
|
||||
|
||||
### Step 4 — `as` converter registry (bridges Bucket-F Group 10)
|
||||
|
||||
16. Confirm `hs-register-converter!` satisfies the Group 10 test
|
||||
`can accept custom conversions`. If yes, this step may be pulled into
|
||||
Bucket-F Group 10 instead (no duplication — just move step 3 of §6 there).
|
||||
17. Commit: `HS: as-converter registry wired into hs-coerce`.
|
||||
|
||||
### Step 5 — Lua plugin
|
||||
|
||||
18. Create `lib/hyperscript/plugins/lua.sx`.
|
||||
19. Add `lua-eval` to `runtime.sx` or directly in the plugin file.
|
||||
20. Parser: `parse-lua-feat` consuming `lua … end`.
|
||||
21. Compiler: registered `"lua"` head.
|
||||
22. Write 3–5 tests in `spec/tests/test-hyperscript-lua.sx`:
|
||||
- Lua returns a string → HS uses it.
|
||||
- Lua error → HS catch.
|
||||
- Lua reads a passed argument.
|
||||
23. Commit: `HS: Lua plugin — inline lua...end blocks`.
|
||||
|
||||
### Step 6 — Worker full runtime plugin
|
||||
|
||||
24. Extend `worker.sx`: implement `parse-worker-feat`, compiler entry,
|
||||
`hs-worker-define!`, `hs-method-call` worker branch.
|
||||
25. Extend test runner: `Worker` constructor + synchronous message loop.
|
||||
26. Un-skip the 7 sibling worker tests from upstream.
|
||||
27. Target: 7/7 worker suite.
|
||||
28. Commit: `HS: Worker plugin full runtime (+7 tests)`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Risks
|
||||
|
||||
- **`parse-feat` closure scope** — `hs-register-feature!` stores parse-fns
|
||||
that need access to parser-internal helpers (`adv!`, `tp-val`, etc.). These
|
||||
are only in scope inside `hs-parse`'s big `let`. Two options:
|
||||
(a) the registry stores fns that receive a parser-context dict as arg, or
|
||||
(b) the registry is checked *inside* `parse-feat` where helpers are in scope
|
||||
and fns are zero-arg closures captured at registration time.
|
||||
Option (b) is simpler but requires plugins to be loaded while the parser
|
||||
`let` is being evaluated — i.e., plugins must be defined *inside* the parser
|
||||
file or the context dict must be exposed. **Recommended:** expose a
|
||||
`_hs-parser-ctx` dict at the module level that parse-fns receive as their
|
||||
sole argument. This makes the API explicit and plugins independent files.
|
||||
|
||||
- **Worker Blob URL in WASM** — `URL.createObjectURL` is available in browsers
|
||||
but not in the OCaml WASM host. Worker full runtime is browser-only; flag it
|
||||
with a capability check and graceful fallback.
|
||||
|
||||
- **Lua/Prolog mutual recursion** — a Lua block calling back into HS calling
|
||||
back into Lua is theoretically possible via the IO suspension machinery.
|
||||
Don't try to support it initially; raise a clear error if detected.
|
||||
|
||||
- **Plugin load-order sensitivity** — `hs-register-feature!` must be called
|
||||
before any source is parsed. If a plugin is loaded lazily (future), a
|
||||
`worker MyWorker` in the page would hit the stub before the full plugin
|
||||
registers. Acceptable for now; document that plugins must be loaded at boot.
|
||||
|
||||
- **`runtime.sx` cleanup for prolog** — nodes 140–142 are referenced nowhere
|
||||
else in the codebase (grep confirms). Safe to delete once the plugin is live.
|
||||
|
||||
---
|
||||
|
||||
## 8. Non-goals
|
||||
|
||||
- Runtime `use(ext)` API (JS-style dynamic plugin install) — future.
|
||||
- Plugin namespacing / versioning — future.
|
||||
- Any conformance tests other than the 7 worker tests in step 6.
|
||||
- Changing how the WASM bundle is built or split.
|
||||
173
plans/elixir-on-sx.md
Normal file
173
plans/elixir-on-sx.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Elixir-on-SX: Elixir on the CEK/VM
|
||||
|
||||
Compile Elixir source to SX AST; the existing CEK evaluator runs it. The natural companion
|
||||
to `lib/erlang/` — Elixir compiles to the BEAM and most of its runtime semantics are
|
||||
Erlang's. The interesting parts are Elixir-specific: the macro system (`quote`/`unquote`),
|
||||
the pipe operator `|>`, `with` expressions, `defmodule`/`def`/`defp`, protocol dispatch,
|
||||
and the `Stream` lazy evaluation library.
|
||||
|
||||
End-state goal: **core Elixir programs running**, including modules, pattern matching, the
|
||||
pipe operator, macros (`quote`/`unquote`/`defmacro`), protocols, and actor-style processes
|
||||
reusing the Erlang runtime foundation.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/elixir/**` and `plans/elixir-on-sx.md`. Do **not** edit
|
||||
`spec/`, `hosts/`, `shared/`, or other `lib/<lang>/`. Reuse `lib/erlang/` runtime
|
||||
functions where possible — import them, don't duplicate.
|
||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Architecture:** Elixir source → Elixir AST → SX AST. Reuse Erlang runtime for process/
|
||||
message/pattern primitives; add Elixir-specific surface in `lib/elixir/`.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
Elixir source text
|
||||
│
|
||||
▼
|
||||
lib/elixir/tokenizer.sx — atoms (:atom), strings (""), charlists (''), sigils (~r, ~s etc.),
|
||||
│ operators (|>, <>, ++, :::, etc.), do/end blocks
|
||||
▼
|
||||
lib/elixir/parser.sx — Elixir AST: defmodule, def/defp/defmacro, @attribute,
|
||||
│ pattern matching, |> pipe, with, for comprehension, quote/unquote,
|
||||
│ case/cond/if/unless, fn, receive, try/rescue/catch/after
|
||||
▼
|
||||
lib/elixir/transpile.sx — Elixir AST → SX AST
|
||||
│
|
||||
├── lib/erlang/runtime.sx (reused: processes, message passing, pattern match)
|
||||
└── lib/elixir/runtime.sx — Elixir-specific: Kernel, String, Enum, Stream, Map,
|
||||
List, Tuple, IO, protocol dispatch, macro expansion
|
||||
```
|
||||
|
||||
Key semantic mappings (differences from Erlang):
|
||||
- `defmodule M do ... end` → SX `define-library` + module dict `{:module "M" :fns {...}}`
|
||||
- `def f(args) do body end` → named function in module dict, with pattern-match dispatch
|
||||
- `|>` pipe → left-to-right function composition; `a |> f(b)` = `f(a, b)`
|
||||
- `with x <- expr, y <- expr2 do body else patterns end` → chained pattern match with early exit
|
||||
- `for x <- list, filter, do: expr` → list comprehension (SX `map`/`filter`)
|
||||
- `quote do expr end` → returns AST as SX list (homoiconic — Elixir AST IS SX-like)
|
||||
- `unquote(expr)` → evaluate expr and splice into surrounding `quote`
|
||||
- `defmacro` → macro in module; expanded at compile time by calling the SX macro
|
||||
- Protocol → dict of implementations keyed by type name; `defprotocol` defines interface,
|
||||
`defimpl` registers an implementation
|
||||
- `Stream` → lazy sequences using SX promises/coroutines (Phase 9/4 of primitives)
|
||||
- `Agent`/`GenServer` → SX coroutine + message queue (similar to Erlang process model)
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: atoms (`:atom`, `:"atom with spaces"`), strings (`""`), charlists (`''`),
|
||||
numbers (int, float, hex `0xFF`, octal `0o77`, binary `0b11`), booleans (`true`/`false`/`nil`),
|
||||
operators (`|>`, `<>`, `++`, `--`, `:::`, `&&`, `||`, `!`, `..`, `<-`, `=~`),
|
||||
sigils (`~r/regex/`, `~s"string"`, `~w(word list)`), do/end blocks, keywords as args
|
||||
`f(key: val)`, `@module_attribute`
|
||||
- [ ] Parser:
|
||||
- Module: `defmodule Name do ... end` → module AST with body
|
||||
- Functions: `def f(pat) do body end`, `def f(pat) when guard do body end`,
|
||||
multi-clause `def f(a) do ...; def f(b) do ...` → clause list
|
||||
- `defp` (private), `defmacro`, `defmacrop`
|
||||
- `@doc`, `@moduledoc`, `@spec`, `@type`, `@behaviour` module attributes
|
||||
- `case expr do patterns end`, `cond do clauses end`, `if`/`unless`
|
||||
- `with x <- e, y <- e2, do: body, else: [pattern -> body]`
|
||||
- `for x <- list, filter, into: acc, do: expr` comprehension
|
||||
- `fn pat -> body end` anonymous function; capture `&Module.fun/arity`, `&(&1 + 1)`
|
||||
- `receive do patterns after timeout -> body end`
|
||||
- `try do body rescue e -> ... catch type, val -> ... after ... end`
|
||||
- `quote do ... end`, `unquote(expr)`, `unquote_splicing(list)`
|
||||
- `|>` pipe chain: `a |> f |> g(b)` → `g(f(a), b)`
|
||||
- [ ] Tests in `lib/elixir/tests/parse.sx`
|
||||
|
||||
### Phase 2 — transpile: basic Elixir (no macros, no processes)
|
||||
- [ ] `ex-eval-ast` entry
|
||||
- [ ] Arithmetic, string `<>`, list `++`/`--`, comparison, boolean (`and`/`or`/`not`)
|
||||
- [ ] Pattern matching in `=`, function heads, `case` — reuse Erlang pattern engine
|
||||
- [ ] `def`/`defp` → SX `define` with clause dispatch (like Erlang function clauses)
|
||||
- [ ] Module as a dict of named functions; `ModuleName.function(args)` dispatch
|
||||
- [ ] `|>` pipe: desugar `a |> f(b, c)` → `f(a, b, c)` at transpile time
|
||||
- [ ] `with` expression: chain of `<-` bindings, short-circuit on mismatch to `else`
|
||||
- [ ] `for` comprehension: `for x <- list, filter do body end` → `map`/`filter`
|
||||
- [ ] `fn` anonymous functions, `&` capture forms
|
||||
- [ ] `if`/`unless`/`cond`/`case`
|
||||
- [ ] String interpolation: `"Hello #{name}"` → string concat
|
||||
- [ ] Keyword lists `[key: val]` → SX list of `{:key val}` dicts; maps `%{key: val}` → SX dict
|
||||
- [ ] Tuples `{a, b, c}` → SX list (or vector); `elem/2`, `put_elem/3`
|
||||
- [ ] 40+ eval tests in `lib/elixir/tests/eval.sx`
|
||||
|
||||
### Phase 3 — macro system
|
||||
- [ ] `quote do expr end` → returns Elixir AST as SX list structure
|
||||
(Elixir AST is 3-tuples `{name, meta, args}` — map to SX `(list name meta args)`)
|
||||
- [ ] `unquote(expr)` → evaluate and splice into surrounding `quote`
|
||||
- [ ] `unquote_splicing(list)` → splice list into surrounding `quote`
|
||||
- [ ] `defmacro` → define a macro in the module; macro receives AST args, returns AST
|
||||
- [ ] Macro expansion: expand macros before transpiling (two-pass: collect defs, then expand)
|
||||
- [ ] `use Module` → calls `Module.__using__/1` macro, injects code into caller
|
||||
- [ ] `import Module` → bring functions into scope without prefix
|
||||
- [ ] `alias Module, as: M` → short name for module
|
||||
- [ ] Tests: `defmacro unless`, `defmacro my_if`, `use` injection, `__MODULE__`, `__DIR__`
|
||||
|
||||
### Phase 4 — protocols
|
||||
- [ ] `defprotocol P do @spec f(t) :: result end` → defines protocol dict + dispatch fn
|
||||
- [ ] `defimpl P, for: Type do def f(t) do ... end end` → register implementation
|
||||
- [ ] Protocol dispatch: `P.f(value)` → look up type of value, find implementation, call it
|
||||
- [ ] Built-in protocols: `Enumerable`, `Collectable`, `String.Chars`, `Inspect`
|
||||
- [ ] `Enumerable` implementation for lists, maps, ranges — enables `Enum.*` on custom types
|
||||
- [ ] `derive` — automatic protocol implementation for simple structs
|
||||
- [ ] Tests: custom type implementing `Enumerable`, `String.Chars`, protocol fallback
|
||||
|
||||
### Phase 5 — structs + behaviours
|
||||
- [ ] `defstruct [:field1, field2: default]` → defines `%ModuleName{}` struct type
|
||||
Structs are maps with `__struct__: ModuleName` key + defined fields
|
||||
- [ ] Struct pattern matching: `%User{name: n} = user`
|
||||
- [ ] `@behaviour Module` → declares behaviour callbacks; compile-time check
|
||||
- [ ] `@impl true` / `@impl BehaviourName` → marks function as behaviour implementation
|
||||
- [ ] Built-in behaviours: `GenServer`, `Supervisor`, `Agent`, `Task`
|
||||
- [ ] Tests: struct creation, update syntax `%{struct | field: val}`, behaviour callbacks
|
||||
|
||||
### Phase 6 — processes + OTP patterns (reuses Erlang runtime)
|
||||
- [ ] `spawn(fn -> ... end)` / `spawn(M, f, args)` → SX coroutine on scheduler
|
||||
Reuse `lib/erlang/` process + message queue infrastructure
|
||||
- [ ] `send(pid, msg)` / `receive do patterns end` — already in Erlang runtime
|
||||
- [ ] `GenServer` behaviour: `start_link`, `call`, `cast`, `handle_call`, `handle_cast`,
|
||||
`handle_info`, `init` — implement as SX macros expanding to process + message loop
|
||||
- [ ] `Agent` — simple state wrapper over GenServer; `Agent.start_link`, `get`, `update`
|
||||
- [ ] `Task` — async computation; `Task.async`, `Task.await`
|
||||
- [ ] `Supervisor` — child spec, restart strategy (`one_for_one`, `one_for_all`)
|
||||
- [ ] Tests: counter GenServer, bank account Agent, parallel Task, supervised worker
|
||||
|
||||
### Phase 7 — standard library
|
||||
- [ ] `Enum.*` — `map`, `filter`, `reduce`, `each`, `into`, `flat_map`, `zip`, `sort`,
|
||||
`sort_by`, `min_by`, `max_by`, `group_by`, `frequencies`, `count`, `any?`, `all?`,
|
||||
`find`, `take`, `drop`, `take_while`, `drop_while`, `chunk_every`, `chunk_by`,
|
||||
`flat_map_reduce`, `scan`, `uniq`, `uniq_by`, `member?`, `empty?`, `sum`, `product`
|
||||
- [ ] `Stream.*` — lazy versions of Enum; `Stream.map`, `Stream.filter`, `Stream.take`,
|
||||
`Stream.cycle`, `Stream.iterate`, `Stream.unfold`, `Stream.resource`
|
||||
Uses SX promises (Phase 9) for laziness
|
||||
- [ ] `String.*` — `length`, `upcase`, `downcase`, `trim`, `split`, `replace`, `contains?`,
|
||||
`starts_with?`, `ends_with?`, `slice`, `at`, `graphemes`, `codepoints`, `to_integer`,
|
||||
`to_float`, `pad_leading`, `pad_trailing`, `duplicate`, `match?`
|
||||
- [ ] `Map.*` — `new`, `get`, `put`, `delete`, `update`, `merge`, `keys`, `values`,
|
||||
`to_list`, `from_struct`, `has_key?`, `filter`, `map`, `reject`, `take`, `drop`
|
||||
- [ ] `List.*` — `first`, `last`, `flatten`, `zip`, `unzip`, `keystore`, `keyfind`,
|
||||
`wrap`, `duplicate`, `improper?`, `delete`, `insert_at`, `replace_at`
|
||||
- [ ] `Tuple.*` — `to_list`, `from_list`, `append`, `insert_at`, `delete_at`
|
||||
- [ ] `Integer.*` / `Float.*` — `parse`, `to_string`, `digits`, `pow`, `is_odd?`, `is_even?`
|
||||
- [ ] `IO.*` — `puts`, `gets`, `inspect`, `write`, `read` → SX IO perform
|
||||
- [ ] `Kernel.*` — built-in functions: `is_integer?`, `is_binary?`, `length`, `hd`, `tl`,
|
||||
`elem`, `put_elem`, `apply`, `raise`, `exit`, `inspect`
|
||||
- [ ] `inspect/1` / `IO.inspect/2` — debug printing using `Inspect` protocol
|
||||
|
||||
### Phase 8 — conformance target
|
||||
- [ ] Vendor or hand-build 100+ Elixir program tests in `lib/elixir/tests/programs/`
|
||||
- [ ] Drive scoreboard
|
||||
|
||||
## Blockers
|
||||
|
||||
_(none yet)_
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
_(awaiting phase 1)_
|
||||
131
plans/elm-on-sx.md
Normal file
131
plans/elm-on-sx.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# Elm-on-SX: Elm 0.19 on the CEK/VM
|
||||
|
||||
Compile Elm source to SX AST; the existing CEK evaluator runs it. The unique angle: SX's
|
||||
reactive island system (`defisland`, signals, `provide`/`context`) is a natural host for
|
||||
The Elm Architecture — Model/Update/View maps almost directly onto SX's reactive runtime.
|
||||
This is the only language in the set that targets SX's browser-side reactivity rather than
|
||||
the server-side evaluator.
|
||||
|
||||
End-state goal: **core Elm programs running in the browser via SX islands**, with The Elm
|
||||
Architecture wired to SX signals. Not a full Elm compiler — no exhaustiveness checking, no
|
||||
module system, no type inference — but a faithful runtime that can run Elm programs written
|
||||
in idiomatic style.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/elm/**` and `plans/elm-on-sx.md`. Do **not** edit `spec/`,
|
||||
`hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Architecture:** Elm source → Elm AST → SX AST. No standalone Elm evaluator.
|
||||
- **Type system:** defer. Focus on runtime semantics. Type errors surface at eval time.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
Elm source text
|
||||
│
|
||||
▼
|
||||
lib/elm/tokenizer.sx — numbers, strings, idents, operators, indentation-sensitive lexer
|
||||
│
|
||||
▼
|
||||
lib/elm/parser.sx — Elm AST: module, import, type alias, type, let, case, lambda,
|
||||
│ if, list/tuple/record literals, pipe operator |>
|
||||
▼
|
||||
lib/elm/transpile.sx — Elm AST → SX AST
|
||||
│
|
||||
▼
|
||||
lib/elm/runtime.sx — TEA runtime: Program, sandbox, element; Cmd/Sub wrappers;
|
||||
│ Html.* shims; Browser.* shims
|
||||
▼
|
||||
SX island / reactive runtime (browser)
|
||||
```
|
||||
|
||||
Key semantic mappings:
|
||||
- `Model` → SX signal (`make-signal`)
|
||||
- `update : Msg -> Model -> Model` → SX signal updater (called on each message)
|
||||
- `view : Model -> Html Msg` → SX component (re-renders on model signal change)
|
||||
- `Cmd` → SX `perform` IO request
|
||||
- `Sub` → SX event listener registered via `dom-listen`
|
||||
- `Maybe a` → `nil` (Nothing) or value (Just a) — uses ADTs from Phase 6 of primitives
|
||||
- `Result a b` → ADT `(Ok val)` / `(Err err)`
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: keywords (`module`, `import`, `type`, `alias`, `let`, `in`, `if`, `then`,
|
||||
`else`, `case`, `of`, `port`), indentation tokens (indent/dedent/newline), string
|
||||
literals, number literals, operators (`|>`, `>>`, `<<`, `<|`, `++`, `::`), type vars
|
||||
- [ ] Parser: module declaration, imports, type aliases, union types, function definitions
|
||||
with pattern matching, `let`/`in`, `case`/`of`, `if`/`then`/`else`, lambda `\x -> e`,
|
||||
list literals `[1,2,3]`, tuple literals `(a,b)`, record literals `{x=1, y=2}`,
|
||||
record update `{ r | x = 1 }`, pipe operator `|>`
|
||||
- [ ] Skip for phase 1: ports, subscriptions, effects manager, type annotations
|
||||
- [ ] Tests in `lib/elm/tests/parse.sx`
|
||||
|
||||
### Phase 2 — transpile: expressions + pattern matching
|
||||
- [ ] `elm-eval-ast` entry
|
||||
- [ ] Arithmetic, string `++`, comparison, boolean ops
|
||||
- [ ] Lambda → SX `fn`; function application
|
||||
- [ ] `let`/`in` → SX `let`
|
||||
- [ ] `if`/`then`/`else` → SX `if`
|
||||
- [ ] `case`/`of` with constructor, literal, tuple, list, wildcard patterns → SX `cond`
|
||||
using ADT match (Phase 6 primitives)
|
||||
- [ ] List ops: `List.map`, `List.filter`, `List.foldl`, `List.foldr`
|
||||
- [ ] `Maybe` and `Result` as ADTs
|
||||
- [ ] 30+ eval tests in `lib/elm/tests/eval.sx`
|
||||
|
||||
### Phase 3 — The Elm Architecture runtime
|
||||
- [ ] `Browser.sandbox` — pure TEA loop (no Cmds, no Subs)
|
||||
`{ init : model, update : msg -> model -> model, view : model -> Html msg }`
|
||||
Wires to: SX signal for model, SX component for view, message dispatch on user events
|
||||
- [ ] `Html.*` shims: `div`, `p`, `button`, `input`, `text`, `h1`–`h6`, `ul`, `li`, `a`,
|
||||
`span`, `img` — emit SX component calls
|
||||
- [ ] `Html.Attributes.*`: `class`, `id`, `href`, `src`, `type_`, `placeholder`, `value`
|
||||
- [ ] `Html.Events.*`: `onClick`, `onInput`, `onSubmit`, `onBlur`, `onFocus`
|
||||
- [ ] `Browser.element` — adds `init` returning `(model, Cmd msg)`, `subscriptions`
|
||||
- [ ] Demo: counter app (`init=0`, `update Increment m = m+1`, `view` shows count + button)
|
||||
|
||||
### Phase 4 — Cmds and Subs
|
||||
- [ ] `Cmd` — mapped to SX `perform` IO requests. `Cmd.none`, `Cmd.batch`
|
||||
- [ ] `Http.get`/`Http.post` → SX fetch IO
|
||||
- [ ] `Sub` — mapped to SX `dom-listen`. `Sub.none`, `Sub.batch`
|
||||
- [ ] `Browser.Events.onClick`, `onKeyPress`, `onAnimationFrame`
|
||||
- [ ] `Time.every` — periodic subscription via SX timer IO
|
||||
- [ ] `Task.perform`/`Task.attempt` — single-shot async operations
|
||||
|
||||
### Phase 5 — standard library
|
||||
- [ ] `String.*` — `length`, `append`, `concat`, `split`, `join`, `trim`, `toUpper`, `toLower`,
|
||||
`contains`, `startsWith`, `endsWith`, `replace`, `toInt`, `toFloat`, `fromInt`, `fromFloat`
|
||||
- [ ] `List.*` — `map`, `filter`, `foldl`, `foldr`, `head`, `tail`, `isEmpty`, `length`,
|
||||
`reverse`, `append`, `concat`, `member`, `sort`, `sortBy`, `indexedMap`, `range`
|
||||
- [ ] `Dict.*` — SX immutable dict; `fromList`, `toList`, `get`, `insert`, `remove`, `update`,
|
||||
`member`, `keys`, `values`, `map`, `filter`, `foldl`
|
||||
- [ ] `Set.*` — SX set primitive (Phase 18); `fromList`, `toList`, `member`, `insert`,
|
||||
`remove`, `union`, `intersect`, `diff`
|
||||
- [ ] `Maybe.*` — `withDefault`, `map`, `andThen`, `map2`
|
||||
- [ ] `Result.*` — `withDefault`, `map`, `andThen`, `mapError`, `toMaybe`
|
||||
- [ ] `Tuple.*` — `first`, `second`, `pair`, `mapFirst`, `mapSecond`
|
||||
- [ ] `Basics.*` — `identity`, `always`, `not`, `xor`, `modBy`, `remainderBy`, `clamp`,
|
||||
`min`, `max`, `abs`, `sqrt`, `logBase`, `e`, `pi`, `floor`, `ceiling`, `round`,
|
||||
`truncate`, `toFloat`, `isNaN`, `isInfinite`, `compare`
|
||||
- [ ] `Random.*` — seed-based PRNG via SX IO perform
|
||||
|
||||
### Phase 6 — full browser integration
|
||||
- [ ] `Browser.application` — URL routing, `onUrlChange`, `onUrlRequest`
|
||||
- [ ] `Browser.Navigation.*` — `pushUrl`, `replaceUrl`, `back`, `forward`
|
||||
- [ ] `Url.Parser.*` — path segment parsing
|
||||
- [ ] `Json.Decode.*` — JSON decoder combinators
|
||||
- [ ] `Json.Encode.*` — JSON encoder
|
||||
- [ ] `Ports` — `port` keyword; JS interop via SX `host-call`
|
||||
|
||||
## Blockers
|
||||
|
||||
_(none yet)_
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
_(awaiting phase 1)_
|
||||
@@ -69,347 +69,36 @@ Representation:
|
||||
- [x] Tests in `lib/forth/tests/test-phase2.sx` — 26/26 pass
|
||||
|
||||
### Phase 3 — control flow + first Hayes tests green
|
||||
- [x] `IF`, `ELSE`, `THEN` — compile to SX `if`
|
||||
- [x] `BEGIN`, `UNTIL`, `WHILE`, `REPEAT`, `AGAIN` — compile to loops
|
||||
- [x] `DO`, `LOOP`, `+LOOP`, `I`, `J`, `LEAVE` — counted loops (needs a return stack)
|
||||
- [x] Return stack: `>R`, `R>`, `R@`, `2>R`, `2R>`, `2R@`
|
||||
- [x] Vendor John Hayes' test suite to `lib/forth/ans-tests/`
|
||||
- [x] `lib/forth/conformance.sh` + runner; `scoreboard.json` + `scoreboard.md`
|
||||
- [x] Baseline: probably 30-50% Core passing after phase 3
|
||||
- [ ] `IF`, `ELSE`, `THEN` — compile to SX `if`
|
||||
- [ ] `BEGIN`, `UNTIL`, `WHILE`, `REPEAT`, `AGAIN` — compile to loops
|
||||
- [ ] `DO`, `LOOP`, `+LOOP`, `I`, `J`, `LEAVE` — counted loops (needs a return stack)
|
||||
- [ ] Return stack: `>R`, `R>`, `R@`, `2>R`, `2R>`, `2R@`
|
||||
- [ ] Vendor John Hayes' test suite to `lib/forth/ans-tests/`
|
||||
- [ ] `lib/forth/conformance.sh` + runner; `scoreboard.json` + `scoreboard.md`
|
||||
- [ ] Baseline: probably 30-50% Core passing after phase 3
|
||||
|
||||
### Phase 4 — strings + more Core
|
||||
- [x] `S"`, `C"`, `."`, `TYPE`, `COUNT`, `CMOVE`, `FILL`, `BLANK`
|
||||
- [x] `CHAR`, `[CHAR]`, `KEY`, `ACCEPT`
|
||||
- [x] `BASE` manipulation: `DECIMAL`, `HEX`
|
||||
- [x] `DEPTH`, `SP@`, `SP!`
|
||||
- [x] Drive Hayes Core pass-rate up
|
||||
- [ ] `S"`, `C"`, `."`, `TYPE`, `COUNT`, `CMOVE`, `FILL`, `BLANK`
|
||||
- [ ] `CHAR`, `[CHAR]`, `KEY`, `ACCEPT`
|
||||
- [ ] `BASE` manipulation: `DECIMAL`, `HEX`
|
||||
- [ ] `DEPTH`, `SP@`, `SP!`
|
||||
- [ ] Drive Hayes Core pass-rate up
|
||||
|
||||
### Phase 5 — Core Extension + optional word sets
|
||||
- [x] Memory: `CREATE`, `HERE`, `ALLOT`, `,`, `C,`, `CELL+`, `CELLS`, `ALIGN`, `ALIGNED`, `2!`, `2@`
|
||||
- [x] Unsigned compare: `U<`, `U>`
|
||||
- [x] Mixed/double-cell math: `S>D`, `M*`, `UM*`, `UM/MOD`, `FM/MOD`, `SM/REM`, `*/`, `*/MOD`
|
||||
- [x] Double-cell ops: `D+`, `D-`, `D=`, `D<`, `D0=`, `2DUP`, `2DROP`, `2OVER`, `2SWAP` (already), plus `D>S`, `DABS`, `DNEGATE`
|
||||
- [x] Number formatting: `<#`, `#`, `#S`, `#>`, `HOLD`, `SIGN`, `.R`, `U.`, `U.R`
|
||||
- [x] Parsing/dictionary: `WORD`, `FIND`, `EXECUTE`, `'`, `[']`, `LITERAL`, `POSTPONE`, `>BODY` (DOES> deferred — needs runtime-rebind of last CREATE)
|
||||
- [x] Source/state: `EVALUATE`, `STATE`, `[`, `]` (`SOURCE`/`>IN` stubbed; tokenized input means the exact byte/offset semantics aren't useful here)
|
||||
- [x] Misc Core: `WITHIN`, `MAX`/`MIN` (already), `ABORT`, `ABORT"`, `EXIT`, `UNLOOP`
|
||||
- [x] File Access word set (in-memory — `read-file` is not reachable from the epoch eval env)
|
||||
- [x] String word set (`SLITERAL`, `COMPARE`, `SEARCH`)
|
||||
- [x] Target: 100% Hayes Core (97% achieved — remaining 5 errors all in `GI5`'s multi-`WHILE`-per-`BEGIN` non-standard pattern, plus one stuck `dict-set!` chunk and 14 numeric-edge fails)
|
||||
- [ ] Full Core + Core Extension
|
||||
- [ ] File Access word set (via SX IO)
|
||||
- [ ] String word set (`SLITERAL`, `COMPARE`, `SEARCH`)
|
||||
- [ ] Target: 100% Hayes Core
|
||||
|
||||
### Phase 6 — speed
|
||||
- [x] Inline primitive calls during compile (skip dict lookup)
|
||||
- [x] Tail-call optimise colon-def endings
|
||||
- [x] JIT cooperation: mark compiled colon-defs as VM-eligible
|
||||
- [ ] Inline primitive calls during compile (skip dict lookup)
|
||||
- [ ] Tail-call optimise colon-def endings
|
||||
- [ ] JIT cooperation: mark compiled colon-defs as VM-eligible
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
- **Post-phase-6 conformance fixes — Hayes 628→632/638 (99%).** Round 2:
|
||||
fixed `forth-pic-step` (used by `#S`) to use the same precise two-step
|
||||
16-bit division as `#`, and rewrote `UM/MOD` using two-phase 16-bit long
|
||||
division to avoid `mod_float` vs `floor-division` inconsistency at integer
|
||||
boundaries. Fixes GP6 / GN1 (pictured output), and the UM/MOD remainder bug.
|
||||
|
||||
- **Post-phase-6 conformance fixes — Hayes 618→628/638 (98%).** Round 1:
|
||||
fixed multi-WHILE compiler bug (REPEAT was consuming back-pc instead of
|
||||
WHILE-target dicts — added `forth-drain-cstack-dicts`); fixed `+LOOP` exit
|
||||
test by clipping increment to 32-bit signed; rewrote `M*`/`UM*` using
|
||||
16-bit half-multiply (`forth-umul32`) to avoid float64 precision loss near
|
||||
2^62; rewrote `#` with two-step division. Eliminated all 6 errors; 10 fails
|
||||
remain (SOURCE/>IN tracking and CHAR " require deeper plumbing changes).
|
||||
|
||||
- **Phase 6 close — JIT cooperation hooks (Hayes unchanged at 618/638).**
|
||||
Every word record now carries `:vm-eligible? true` and a
|
||||
`:call-count` counter that `forth-execute-word` bumps on every
|
||||
invocation. The flag is a hint for downstream JIT consumers — our
|
||||
bodies are plain SX lambdas already, so the existing SX VM's
|
||||
on-first-call JIT lifts them into bytecode automatically; the
|
||||
metadata just makes that fact discoverable. Added
|
||||
`forth-hot-words state threshold` returning `(name count)`
|
||||
pairs above a threshold so a future tracing JIT can pick out
|
||||
hot definitions to specialise. Phase 6 boxes all ticked.
|
||||
All 306 internal tests green; Hayes Core stays at 618/638.
|
||||
|
||||
- **Phase 6 — TCO at colon-def endings (Hayes unchanged at 618/638).**
|
||||
`forth-run-body` now special-cases the final op when it's a plain
|
||||
function (not a branch dict): we call it in tail position with no
|
||||
pc-increment and no recursive `forth-run-body` call. This means
|
||||
the SX CEK can collapse the continuation frame, so chains like
|
||||
`: A ... B ; : B ... C ; …` and `RECURSE` deep-recursion test
|
||||
cases run without piling up frames at each colon-def boundary.
|
||||
All 306 internal tests still green; verified 5000-deep
|
||||
`COUNTDOWN RECURSE` still terminates fine.
|
||||
|
||||
- **Phase 6 — inline primitive calls (Hayes unchanged at 618/638).**
|
||||
`forth-compile-call` now appends the looked-up word's body fn
|
||||
directly to the colon-def body instead of wrapping it in
|
||||
`(fn (s) (forth-execute-word s w))`. `forth-execute-word body`
|
||||
reduces to `((get w "body") state)`, so the wrapper added an
|
||||
extra closure + `get` per call op for no behavioural gain. Same
|
||||
early-binding semantics: the body fn is captured at compile time,
|
||||
so later redefinitions of the same name don't retroactively
|
||||
change existing definitions. All 306 internal tests still green;
|
||||
Hayes Core stays at 618/638. Pure optimisation.
|
||||
|
||||
- **Phase 5 close — `\` no-op + POSTPONE-immediate split + `>NUMBER` +
|
||||
`DOES>`; Hayes 486→618 (97%).** Big closing-out iteration.
|
||||
Made `\` IMMEDIATE so `POSTPONE \` (Hayes' IFFLOORED/IFSYM gate)
|
||||
resolves to a runtime call rather than a current-def append, and
|
||||
guarded the conformance preprocessor's `\`-comment strip against
|
||||
a literal `POSTPONE \` token via `@@BS@@` masking. Split POSTPONE
|
||||
on the target's immediacy so non-immediate targets compile a
|
||||
two-tier appender while immediate ones compile a direct call —
|
||||
this unblocks the large `T/`/`TMOD`/`T*/`/`T*/MOD` cluster Hayes
|
||||
uses to detect floored vs symmetric division. `>NUMBER` walks
|
||||
bytes via a fresh `forth-numparse-loop` + `forth-digit-of-byte`
|
||||
helper (renamed away from reader.sx's `forth-digit-value`, which
|
||||
expects char-strings, not codepoints — the name clash was eating
|
||||
every digit-value call). Implemented `DOES>` by:
|
||||
1) tracking the last CREATE on `state.last-creator`,
|
||||
2) adding a `:kind "does-rebind"` op, and
|
||||
3) post-processing the body in `;` to attach the slice of ops
|
||||
after each rebind as `:deferred`. At runtime, the rebind op
|
||||
installs a new body for the target word that pushes its
|
||||
data-field address and runs the deferred slice. Also added
|
||||
histogram tracking on the conformance runner so future runs
|
||||
surface the top missing words. Hayes: 618/638 pass (97%),
|
||||
14 fail, 6 error (5× GI5 multi-WHILE, 1× dict-set! chunk).
|
||||
|
||||
- **Phase 5 — String word set `COMPARE`/`SEARCH`/`SLITERAL` (+9).**
|
||||
`COMPARE` walks bytes via the new `forth-compare-bytes-loop`,
|
||||
returning -1/0/1 with standard prefix semantics (shorter string
|
||||
compares less than its extension). `SEARCH` scans the haystack
|
||||
with a helper `forth-search-bytes` and `forth-match-at`, returning
|
||||
the tail after the first match or the original string with flag=0.
|
||||
Empty needle returns at offset 0 with flag=-1 per ANS. `SLITERAL`
|
||||
is IMMEDIATE: pops `(c-addr u)` at compile time, copies the bytes
|
||||
into a fresh allocation, and emits the two pushes so the compiled
|
||||
word yields the interned string at runtime.
|
||||
|
||||
- **Phase 5 — File Access word set (in-memory backing; +4).**
|
||||
`OPEN-FILE`/`CREATE-FILE`/`CLOSE-FILE`/`READ-FILE`/`WRITE-FILE`/
|
||||
`FILE-POSITION`/`FILE-SIZE`/`REPOSITION-FILE`/`DELETE-FILE` plus
|
||||
the mode constants `R/O`/`R/W`/`W/O`/`BIN`. File handles live on
|
||||
`state.files` (fileid → {content, pos, path}) with a
|
||||
`state.by-path` index so `CREATE-FILE`'d files can be
|
||||
`OPEN-FILE`'d later in the same session. Attempting to
|
||||
`OPEN-FILE` an unknown path returns `ior != 0`; disk-backed
|
||||
open/read is not wired because `read-file` isn't in the sx_server
|
||||
epoch eval environment (it's bound only in the HTTP helpers).
|
||||
Also removed the stray base-2 `BIN` primitive from Phase 4 —
|
||||
ANS `BIN` is the file-mode modifier. Hayes Core unchanged at
|
||||
486/638 since core.fr doesn't exercise file words.
|
||||
|
||||
- **Phase 5 — `WITHIN`/`ABORT`/`ABORT"`/`EXIT`/`UNLOOP` (+7;
|
||||
Hayes 477→486, 76%).** `WITHIN` uses the ANS two's-complement
|
||||
trick: `(n1-n2) U< (n3-n2)`. `ABORT` wipes the data/return/control
|
||||
stacks and raises — the conformance runner catches it at the
|
||||
chunk boundary. `ABORT"` parses its message like `S"`, then at
|
||||
runtime pops a flag and raises only if truthy. `EXIT` adds a new
|
||||
`:kind "exit"` op that the PC-driven body runner treats as a
|
||||
jump-to-end; added a matching cond clause in `forth-step-op`.
|
||||
`UNLOOP` pops two from the return stack — usable paired with
|
||||
`EXIT` to bail from inside `DO`/`LOOP`.
|
||||
|
||||
- **Phase 5 — `[`, `]`, `STATE`, `EVALUATE` (+5; Hayes 463→477, 74%).**
|
||||
`[` (IMMEDIATE) clears `state.compiling`, `]` sets it. `STATE`
|
||||
pushes the sentinel address `"@@state"` and `@` reads it as
|
||||
`-1`/`0` based on the live `compiling` flag. `EVALUATE` reads
|
||||
the (addr,u) string from byte memory, retokenises it via
|
||||
`forth-tokens`, swaps it in as the active input, runs the
|
||||
interpret loop, and restores the saved input. `SOURCE` and
|
||||
`>IN` exist as stubs that push zeros — our whitespace-tokenised
|
||||
input has no native byte-offset, so the deeper Hayes tests
|
||||
that re-position parsing via `>IN !` stay marked as errors
|
||||
rather than silently misbehaving.
|
||||
|
||||
- **Phase 5 — parsing/dictionary words `'`/`[']`/`EXECUTE`/`LITERAL`/
|
||||
`POSTPONE`/`WORD`/`FIND`/`>BODY` (Hayes 448→463, 72%).** xt is
|
||||
represented as the SX dict reference of the word record, so
|
||||
`'`/`[']` push the looked-up record and `EXECUTE` calls
|
||||
`forth-execute-word` on the popped value. `LITERAL` (IMMEDIATE)
|
||||
pops a value at compile time and emits a push-op. `POSTPONE`
|
||||
(IMMEDIATE) compiles into the *outer* def an op that, when run
|
||||
during a *later* compile, appends a call-w op to whatever def is
|
||||
current — the standard two-tier compile semantic. Added
|
||||
`state.last-defined` tracked by every primitive/colon definition
|
||||
so `IMMEDIATE` can target the most-recent word even after `;`
|
||||
closes the def. CREATE now stashes its data-field address on the
|
||||
word record so `>BODY` can recover it. `WORD`/`FIND` use the byte
|
||||
memory and counted-string layout already in place.
|
||||
`DOES>` is deferred — needs a runtime mechanism to rebind the
|
||||
last-CREATE'd word's action.
|
||||
|
||||
- **Phase 5 — pictured numeric output: `<#`/`#`/`#S`/`#>`/`HOLD`/`SIGN` +
|
||||
`U.`/`U.R`/`.R` (+9; Hayes 446→448, 70%).** Added a `state.hold`
|
||||
list of single-character strings — `<#` resets it, `HOLD` and
|
||||
`SIGN` prepend, `#` divides ud by BASE and prepends one digit,
|
||||
`#S` loops `#` until ud is zero (running once even on zero),
|
||||
`#>` drops ud and copies the joined hold buffer into mem,
|
||||
pushing `(addr, len)`. `U.` / `.R` / `U.R` use a separate
|
||||
`forth-num-to-string` for one-shot decimal/hex output and
|
||||
`forth-spaces-str` for right-justify padding.
|
||||
|
||||
- **Phase 5 — double-cell ops `D+`/`D-`/`DNEGATE`/`DABS`/`D=`/`D<`/`D0=`/
|
||||
`D0<`/`DMAX`/`DMIN` (+18; Hayes unchanged).** Doubles get rebuilt
|
||||
from `(lo, hi)` cells via `forth-double-from-cells-s`, the op runs
|
||||
in bignum, and we push back via `forth-double-push-s`. Hayes Core
|
||||
doesn't exercise D-words (those live in Gerry Jackson's separate
|
||||
`doublest.fth` Double word-set tests we have not vendored), so the
|
||||
scoreboard stays at 446/638 — but the words now exist for any
|
||||
consumer that needs them.
|
||||
|
||||
- **Phase 5 — mixed/double-cell math; Hayes 342→446 (69%).** Added
|
||||
`S>D`, `D>S`, `M*`, `UM*`, `UM/MOD`, `FM/MOD`, `SM/REM`, `*/`, `*/MOD`.
|
||||
Doubles ride on the stack as `(lo, hi)` with `hi` on top.
|
||||
Helpers `forth-double-push-{u,s}` / `forth-double-from-cells-{u,s}`
|
||||
split & rebuild via 32-bit unsigned mod/div, picking the negative
|
||||
path explicitly so we don't form `2^64 + small` (float precision
|
||||
drops at ULP=2^12 once you cross 2^64). `M*`/`UM*` use bignum
|
||||
multiply then split; `*/`/`*/MOD` use bignum intermediate and
|
||||
truncated division. Hayes: 446 pass / 185 error / 7 fail.
|
||||
|
||||
- **Phase 5 — memory primitives + unsigned compare; Hayes 268→342 (53%).**
|
||||
Added `CREATE`/`HERE`/`ALLOT`/`,`/`C,`/`CELL+`/`CELLS`/`ALIGN`/`ALIGNED`/
|
||||
`2!`/`2@`/`U<`/`U>`. Generalised `@`/`!`/`+!` to dispatch on address
|
||||
type: string addresses still go through `state.vars` (VARIABLE/VALUE
|
||||
cells) while integer addresses now fall through to `state.mem` —
|
||||
letting CREATE-allocated cells coexist with existing variables.
|
||||
Decomposed the original "Full Core + Core Extension" box into
|
||||
smaller unticked sub-bullets so iterations land per cluster.
|
||||
Hayes: 342 pass / 292 error / 4 fail (53%). 237/237 internal.
|
||||
|
||||
- **Phase 4 close — LSHIFT/RSHIFT, 32-bit arith truncation, early
|
||||
binding; Hayes 174→268 (42%).** Added `LSHIFT` / `RSHIFT` as logical
|
||||
shifts on 32-bit unsigned values, converted through
|
||||
`forth-to-unsigned`/`forth-from-unsigned`. All arithmetic
|
||||
primitives (`+` `-` `*` `/` `MOD` `NEGATE` `ABS` `1+` `1-` `2+`
|
||||
`2-` `2*` `2/`) now clip results to 32-bit signed via a new
|
||||
`forth-clip` helper, so loop idioms that rely on `2*` shifting the
|
||||
MSB out (e.g. Hayes' `BITS` counter) actually terminate.
|
||||
Changed colon-def call compilation from late-binding to early
|
||||
binding: `forth-compile-call` now resolves the target word at
|
||||
compile time, which makes `: GDX 123 ; : GDX GDX 234 ;` behave
|
||||
per ANS (inner `GDX` → old def, not infinite recursion). `RECURSE`
|
||||
keeps its late-binding thunk via the new `forth-compile-recurse`
|
||||
helper. Raised `MAX_CHUNKS` default to 638 (full `core.fr`) now
|
||||
that the BITS and COUNT-BITS loops terminate. Hayes: 268 pass /
|
||||
368 error / 2 fail.
|
||||
|
||||
- **Phase 4 — `SP@`/`SP!` (+4; Hayes unchanged; `DEPTH` was already present).**
|
||||
`SP@` pushes the current data-stack depth (our closest analogue to a
|
||||
stack pointer — SX lists have no addressable backing). `SP!` pops a
|
||||
target depth and truncates the stack via `drop` on the dstack list.
|
||||
This preserves the save/restore idiom `SP@ … SP!` even though the
|
||||
returned "pointer" is really a count.
|
||||
|
||||
- **Phase 4 — `BASE`/`DECIMAL`/`HEX`/`BIN`/`OCTAL` (+9; Hayes unchanged).**
|
||||
Moved `base` from its top-level state slot into `state.vars["base"]`
|
||||
so the regular `@`/`!`/VARIABLE machinery works on it.
|
||||
`BASE` pushes the sentinel address `"base"`; `DECIMAL`/`HEX`/`BIN`/
|
||||
`OCTAL` are thin primitives that write into that slot. Parser
|
||||
reads through `vars` now. Hayes unchanged because the runner had
|
||||
already been stubbing `HEX`/`DECIMAL` — now real words, stubs
|
||||
removed from `hayes-runner.sx`.
|
||||
|
||||
- **Phase 4 — `CHAR`/`[CHAR]`/`KEY`/`ACCEPT` (+7 / Hayes 168→174).**
|
||||
`CHAR` parses the next token and pushes the first-char code. `[CHAR]`
|
||||
is IMMEDIATE: in compile mode it embeds the code as a compiled push
|
||||
op, in interpret mode it pushes inline. `KEY`/`ACCEPT` read from an
|
||||
optional `state.keybuf` string — empty buffer makes `KEY` raise
|
||||
`"no input available"` (matches ANS when stdin is closed) and
|
||||
`ACCEPT` returns `0`. Enough for Hayes to get past CHAR-gated
|
||||
clusters; real interactive IO lands later.
|
||||
|
||||
- **Phase 4 — strings: `S"`/`C"`/`."`/`TYPE`/`COUNT`/`CMOVE`/`CMOVE>`/`MOVE`/`FILL`/`BLANK`/`C@`/`C!`/`CHAR+`/`CHARS` (+16 / Hayes 165→168).**
|
||||
Added a byte-addressable memory model to state: `mem` (dict keyed by
|
||||
stringified address → integer byte) and `here` (next-free integer
|
||||
addr). Helpers `forth-alloc-bytes!` / `forth-mem-write-string!` /
|
||||
`forth-mem-read-string`. `S"`/`C"`/`."` are IMMEDIATE parsing words
|
||||
that consume tokens until one ends with `"`, then either copy content
|
||||
into memory at compile time (and emit a push of `addr`/`addr len` for
|
||||
the colon-def body) or do it inline in interpret mode. `TYPE` emits
|
||||
`u` bytes from `addr` via `char-from-code`. `COUNT` reads the length
|
||||
byte at a counted-string address and pushes (`addr+1`, `u`). `FILL`,
|
||||
`BLANK` (FILL with space), `CMOVE` (forward), `CMOVE>` (backward),
|
||||
and `MOVE` (auto-directional) mutate the byte dict. 193/193 internal
|
||||
tests, Hayes 168/590 (+3).
|
||||
|
||||
- **Phase 3 — Hayes conformance runner + baseline scoreboard (165/590, 28%).**
|
||||
`lib/forth/conformance.sh` preprocesses `ans-tests/core.fr` (strips `\`
|
||||
and `( ... )` comments + `TESTING` lines), splits the source on every
|
||||
`}T` so each Hayes test plus the small declaration blocks between
|
||||
them are one safe-resume chunk, and emits an SX driver that feeds
|
||||
the chunks through `lib/forth/hayes-runner.sx`. The runner registers
|
||||
`T{`/`->`/`}T` as Forth primitives that snapshot the dstack depth on
|
||||
`T{`, record actual on `->`, compare on `}T`, and install stub
|
||||
`HEX`/`DECIMAL`/`TESTING` so metadata doesn't halt the stream. Errors
|
||||
raised inside a chunk are caught by `guard` and the state is reset,
|
||||
so one bad test does not break the rest. Outputs
|
||||
`scoreboard.json` + `scoreboard.md`.
|
||||
|
||||
First-run baseline: 165 pass / 425 error / 0 fail on the first 590
|
||||
chunks. The default cap sits at 590 because `core.fr` chunks beyond
|
||||
that rely on unsigned-integer wrap-around (e.g. `COUNT-BITS` with
|
||||
`BEGIN DUP WHILE … 2* REPEAT`) which never terminates on our
|
||||
bignum-based Forth; raise `MAX_CHUNKS` once those tests unblock.
|
||||
Majority of errors are missing Phase-4 words (`RSHIFT`, `LSHIFT`,
|
||||
`CELLS`, `S"`, `CHAR`, `SOURCE`, etc.) — each one implemented should
|
||||
convert a cluster of errors to passes.
|
||||
|
||||
- **Phase 3 — vendor Gerry Jackson's forth2012-test-suite.** Added
|
||||
`lib/forth/ans-tests/{tester.fr, core.fr, coreexttest.fth}` from
|
||||
https://github.com/gerryjackson/forth2012-test-suite (master, fetched
|
||||
2026-04-24). `tester.fr` is Hayes' `T{ ... -> ... }T` harness; `core.fr`
|
||||
is the ~1000-line Core word tests; `coreexttest.fth` is Core Ext
|
||||
(parked for later phases). Files are pristine — the conformance runner
|
||||
(next iteration) will consume them.
|
||||
|
||||
- **Phase 3 — `DO`/`LOOP`/`+LOOP`/`I`/`J`/`LEAVE` + return stack words (+16).**
|
||||
Counted loops compile onto the same PC-driven body runner. DO emits an
|
||||
enter-op (pops limit+start from data stack, pushes them to rstack) and
|
||||
pushes a `{:kind "do" :back PC :leaves ()}` marker onto cstack. LOOP/+LOOP
|
||||
emit a dict op (`:kind "loop"`/`"+loop"` with target=back-cell). The step
|
||||
handler pops index & reads limit, increments, and either restores the
|
||||
updated index + jumps back, or drops the frame and advances. LEAVE walks
|
||||
cstack for the innermost DO marker, emits a `:kind "leave"` dict op with
|
||||
a fresh target cell, and registers it on the marker's leaves list. LOOP
|
||||
patches all registered leave-targets to the exit PC and drops the marker.
|
||||
The leave op pops two from rstack (unloop) and branches. `I` peeks rtop;
|
||||
`J` reads rstack index 2 (below inner frame). Added non-immediate
|
||||
return-stack words `>R`, `R>`, `R@`, `2>R`, `2R>`, `2R@`. Nested
|
||||
DO/LOOP with J tested; LEAVE in nested loops exits only the inner.
|
||||
177/177 green.
|
||||
|
||||
- **Phase 3 — `BEGIN`/`UNTIL`/`WHILE`/`REPEAT`/`AGAIN` (+9).** Indefinite-loop
|
||||
constructs built on the same PC-driven body runner introduced for `IF`.
|
||||
BEGIN records the current body length on `state.cstack` (a plain numeric
|
||||
back-target). UNTIL/AGAIN pop that back-target and emit a `bif`/`branch`
|
||||
op whose target cell is set to the recorded PC. WHILE emits a forward
|
||||
`bif` with a fresh target cell and pushes it on the cstack *above* the
|
||||
BEGIN marker; REPEAT pops both (while-target first, then back-pc), emits
|
||||
an unconditional branch back to BEGIN, then patches the while-target to
|
||||
the current body length — so WHILE's false flag jumps past the REPEAT.
|
||||
Mixed compile-time layout (numeric back-targets + dict forward targets
|
||||
on the same cstack) is OK because the immediate words pop them in the
|
||||
order they expect. AGAIN works structurally but lacks a test without a
|
||||
usable mid-loop exit; revisit once `EXIT` lands. 161/161 green.
|
||||
|
||||
- **Phase 3 start — `IF`/`ELSE`/`THEN` (+18).** `lib/forth/compiler.sx`
|
||||
+ `tests/test-phase3.sx`. Colon-def body switched from `for-each` to
|
||||
a PC-driven runner so branch ops can jump: ops now include dict tags
|
||||
`{"kind" "bif"|"branch" "target" cell}` alongside the existing
|
||||
`(fn (s) ...)` shape. IF compiles a `bif` with a fresh target cell
|
||||
pushed to `state.cstack`; ELSE emits an unconditional `branch`,
|
||||
patches the IF's target to the instruction after this branch, and
|
||||
pushes the new target; THEN patches the most recent target to the
|
||||
current body length. Nested IF/ELSE/THEN works via the cstack.
|
||||
Also fixed `EMIT`: `code-char` → `char-from-code` (spec-correct
|
||||
primitive name) so Phase 1/2 tests run green on sx_server.
|
||||
152/152 (Phase 1 + 2 + 3) green.
|
||||
|
||||
- **Phase 2 complete — colon defs, compile mode, VARIABLE/CONSTANT/VALUE/TO, @/!/+! (+26).**
|
||||
`lib/forth/compiler.sx` plus `tests/test-phase2.sx`.
|
||||
Colon-def body is a list of ops (one per source token) wrapped in a single
|
||||
|
||||
145
plans/go-on-sx.md
Normal file
145
plans/go-on-sx.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Go-on-SX: Go on the CEK/VM
|
||||
|
||||
Compile Go source to SX AST; the existing CEK evaluator runs it. The unique angle: Go's
|
||||
goroutines and channels map cleanly onto SX's IO suspension machinery (`perform`/`cek-resume`)
|
||||
— a goroutine is a `cek-step-loop` running in a cooperative scheduler, a channel send/receive
|
||||
is a `perform` that suspends until the other end is ready.
|
||||
|
||||
End-state goal: **core Go programs running**, including goroutines, channels, defer/panic/recover,
|
||||
interfaces, and structs. Not a full Go compiler — no generics, no CGo, no full stdlib — but
|
||||
a faithful runtime for idiomatic Go concurrent programs.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/go/**` and `plans/go-on-sx.md`. Do **not** edit `spec/`,
|
||||
`hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Architecture:** Go source → Go AST → SX AST. No standalone Go evaluator.
|
||||
- **Concurrency model:** cooperative, not preemptive. Goroutines yield at channel ops and
|
||||
`time.Sleep`. A round-robin scheduler in SX drives them.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
Go source text
|
||||
│
|
||||
▼
|
||||
lib/go/tokenizer.sx — Go tokens: keywords, idents, string/rune/number literals,
|
||||
│ operators, semicolon insertion rules
|
||||
▼
|
||||
lib/go/parser.sx — Go AST: package, import, var, const, type, func, struct,
|
||||
│ interface, goroutine, channel ops, defer, select, for range
|
||||
▼
|
||||
lib/go/transpile.sx — Go AST → SX AST
|
||||
│
|
||||
▼
|
||||
lib/go/runtime.sx — goroutine scheduler, channel primitives, defer stack,
|
||||
│ panic/recover, interface dispatch, slice/map ops
|
||||
▼
|
||||
CEK / VM
|
||||
```
|
||||
|
||||
Key semantic mappings:
|
||||
- `go fn()` → spawn new coroutine (SX coroutine primitive, Phase 4 of primitives)
|
||||
- `ch <- v` (send) → `perform` that suspends until receiver ready; scheduler picks next goroutine
|
||||
- `v := <-ch` (receive) → `perform` that suspends until sender ready
|
||||
- `select { case ... }` → scheduler checks all channel readiness, picks first ready
|
||||
- `defer fn()` → push onto a per-goroutine defer stack; run on return/panic
|
||||
- `panic(v)` → `raise` the value; `recover()` catches it in deferred function
|
||||
- `interface{}` → any SX value (duck typed)
|
||||
- `struct { ... }` → SX hash table with field names as keys
|
||||
- `slice` → SX vector with length + capacity metadata
|
||||
- `map[K]V` → SX mutable hash table (Phase 10 of primitives)
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — tokenizer + parser
|
||||
- [ ] Tokenizer: keywords (`package`, `import`, `func`, `var`, `const`, `type`, `struct`,
|
||||
`interface`, `go`, `chan`, `select`, `defer`, `return`, `if`, `else`, `for`, `range`,
|
||||
`switch`, `case`, `default`, `break`, `continue`, `goto`, `fallthrough`, `map`,
|
||||
`make`, `new`, `nil`, `true`, `false`), automatic semicolon insertion, string literals
|
||||
(interpreted + raw `` `...` ``), rune literals `'a'`, number literals (int, float, hex,
|
||||
octal, binary, complex), operators, slices `[:]`
|
||||
- [ ] Parser: package clause, imports, top-level `func`/`var`/`const`/`type`; function
|
||||
bodies: short variable decl `:=`, assignments, `if`/`else`, `for`/`range`, `switch`,
|
||||
`return`, struct literals, slice literals, map literals, composite literals, type
|
||||
assertions `v.(T)`, method calls `v.Method(args)`, goroutine `go`, channel ops
|
||||
`<-ch`, `ch <- v`, `defer`, `select`
|
||||
- [ ] Tests in `lib/go/tests/parse.sx`
|
||||
|
||||
### Phase 2 — transpile: basic Go (no goroutines)
|
||||
- [ ] `go-eval-ast` entry
|
||||
- [ ] Arithmetic, string ops, comparison, boolean
|
||||
- [ ] Variables, short decl, assignment, multiple assignment
|
||||
- [ ] `if`/`else if`/`else`
|
||||
- [ ] `for` (C-style), `for range` over slice/map/string
|
||||
- [ ] Functions: named + anonymous, multiple return values (SX multiple values, Phase 8)
|
||||
- [ ] Structs → SX hash tables; field access `.field`; struct literals `T{f: v}`
|
||||
- [ ] Slices → SX vectors; `len`, `cap`, `append`, `copy`, slice expressions `s[a:b]`
|
||||
- [ ] Maps → SX hash tables; `make(map[K]V)`, `m[k]`, `m[k] = v`, `delete(m, k)`,
|
||||
comma-ok `v, ok := m[k]`
|
||||
- [ ] Pointers — modelled as single-element mutable vectors; `&x` creates wrapper, `*p` dereferences
|
||||
- [ ] `fmt.Println`/`fmt.Printf`/`fmt.Sprintf` → SX IO perform (print)
|
||||
- [ ] 40+ eval tests in `lib/go/tests/eval.sx`
|
||||
|
||||
### Phase 3 — defer / panic / recover
|
||||
- [ ] Defer stack per function frame — SX list of thunks, run LIFO on return
|
||||
- [ ] `defer` statement pushes thunk; transpiler wraps function body in try/finally equivalent
|
||||
- [ ] `panic(v)` → `raise` with Go panic wrapper
|
||||
- [ ] `recover()` → catches panic value inside a deferred function; returns nil otherwise
|
||||
- [ ] Panic propagation across call stack until recovered or fatal
|
||||
- [ ] Tests: defer ordering, panic/recover, panic in goroutine without recover
|
||||
|
||||
### Phase 4 — goroutines + channels
|
||||
- [ ] Coroutine-based goroutine type using SX coroutine primitive (Phase 4 of primitives)
|
||||
- [ ] Round-robin scheduler in `lib/go/runtime.sx`: maintains run queue, steps each
|
||||
goroutine one turn at a time, suspends at channel ops
|
||||
- [ ] Unbuffered channels: `make(chan T)` → rendezvous point; send suspends until receive
|
||||
and vice versa. Implemented as a pair of waiting queues + `cek-resume`.
|
||||
- [ ] Buffered channels: `make(chan T, n)` → circular buffer; send only blocks when full,
|
||||
receive only blocks when empty
|
||||
- [ ] `close(ch)` — mark channel closed; receivers drain then get zero value + `false`
|
||||
- [ ] `select` — scheduler inspects all cases, picks a ready one (random if multiple),
|
||||
blocks if none ready until at least one becomes ready
|
||||
- [ ] `go fn(args)` — spawns new goroutine on run queue
|
||||
- [ ] `time.Sleep(d)` — yields current goroutine, re-queues after d milliseconds
|
||||
(simulated with IO perform timer)
|
||||
- [ ] Tests: ping-pong, fan-out, fan-in, select with default, range over channel
|
||||
|
||||
### Phase 5 — interfaces
|
||||
- [ ] Interface type → SX dict `{:type "T" :methods {...}}` dispatch table
|
||||
- [ ] `interface{}` / `any` → any SX value (already implicit)
|
||||
- [ ] Type assertion `v.(T)` → check `:type` field, panic if mismatch
|
||||
- [ ] Type switch `switch v.(type) { case T: ... }` → dispatches on `:type`
|
||||
- [ ] Method sets — structs implement interfaces implicitly if they have the right methods
|
||||
- [ ] Value vs pointer receivers — pointer receiver gets the mutable vector wrapper
|
||||
- [ ] Built-in interfaces: `error` (`Error() string`), `Stringer` (`String() string`)
|
||||
- [ ] Tests: interface satisfaction, type assertion, type switch, error interface
|
||||
|
||||
### Phase 6 — standard library subset
|
||||
- [ ] `fmt` — `Println`, `Printf`, `Sprintf`, `Fprintf`, `Errorf`, `Stringer` dispatch
|
||||
- [ ] `strings` — `Contains`, `HasPrefix`, `HasSuffix`, `Split`, `Join`, `TrimSpace`,
|
||||
`ToUpper`, `ToLower`, `Replace`, `Index`, `Count`, `Repeat`
|
||||
- [ ] `strconv` — `Itoa`, `Atoi`, `FormatFloat`, `ParseFloat`, `ParseInt`, `FormatInt`
|
||||
- [ ] `math` — full surface via SX math primitives (Phase 15)
|
||||
- [ ] `sort` — `sort.Slice`, `sort.Ints`, `sort.Strings`
|
||||
- [ ] `errors` — `errors.New`, `errors.Is`, `errors.As`
|
||||
- [ ] `sync` — `sync.Mutex` (cooperative — just a boolean flag + goroutine queue),
|
||||
`sync.WaitGroup`, `sync.Once`
|
||||
- [ ] `io` — `io.Reader`/`io.Writer` interfaces; `io.ReadAll`; `strings.NewReader`
|
||||
|
||||
### Phase 7 — full conformance target
|
||||
- [ ] Vendor a Go test suite or hand-build 100+ program tests in `lib/go/tests/programs/`
|
||||
- [ ] Drive scoreboard
|
||||
|
||||
## Blockers
|
||||
|
||||
_(none yet)_
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
_(awaiting phase 1)_
|
||||
351
plans/hs-bucket-f.md
Normal file
351
plans/hs-bucket-f.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# HS Conformance — Bucket F Plan
|
||||
|
||||
Based on a full suite run on 2026-04-26. Current score: **~1297/1489 covered** (~87%).
|
||||
Skipped from runs: tests 197–200 (hypertrace, slow), 615 (slow), 1197–1198 (repeat-forever timeouts).
|
||||
|
||||
**⚠ Updated 2026-04-26:** The hs-loop completed significant Bucket D work before being stopped.
|
||||
`hs-f` branches from `loops/hs` HEAD which already includes:
|
||||
- MutationObserver mock + `on mutation` dispatch (+7) → **Group 4 likely done**
|
||||
- Cookie API partial (+3/5) → **Group 5 partially done**
|
||||
- `elsewhere`/`from elsewhere` + count filters (+7) → **Group 3a/3c partially done**
|
||||
- Namespaced `def` (+3) → already done
|
||||
- SourceInfo E38 (+4) + WebWorker E39 (+1) → already merged
|
||||
|
||||
**The Bucket F agent must run `hs_test_run` on each group's suite before implementing,
|
||||
to verify what's actually still failing. Skip any group that already passes.**
|
||||
|
||||
Total remaining failures: ~193. Broken into groups below.
|
||||
|
||||
---
|
||||
|
||||
## Group 0 — Bucket E payoff (~47 tests, will land automatically)
|
||||
|
||||
These are already implemented or in-flight on Bucket E branches. Once merged they close ~47 tests.
|
||||
|
||||
| Suite | Tests | Status |
|
||||
|-------|------:|-------|
|
||||
| `hs-upstream-core/tokenizer` | 17 | E37 in progress |
|
||||
| `hs-upstream-socket` | 16 | E36 in progress |
|
||||
| `hs-upstream-fetch` | 8 | E40 in progress |
|
||||
| `hs-upstream-core/sourceInfo` | 4 | E38 done, not yet merged |
|
||||
| `hs-upstream-worker` | 1 | E39 done, not yet merged |
|
||||
| E37 string interpolation bug | 1 | E37 |
|
||||
|
||||
**Do not plan these — they resolve when Bucket E merges.**
|
||||
|
||||
---
|
||||
|
||||
## Group 1 — Null safety reporting (+7)
|
||||
|
||||
**Suite:** `hs-upstream-core/runtimeErrors`
|
||||
**Failures:** 7 tests, all "Expected `'#doesntExist' is null`, got ``"
|
||||
**What's needed:** When a command like `put`, `increment`, `decrement`, `default`, `remove`, `settle`, `transition` receives a null element (e.g. `#doesntExist`), HS must throw a structured null-safety error with the element reference in the message. The null check + error format is already designed in Bucket D #31 (cluster 31 of `hs-conformance-to-100.md`).
|
||||
|
||||
**Estimate:** +7. Straightforward — null guard at command dispatch entry.
|
||||
|
||||
---
|
||||
|
||||
## Group 2 — `tell` semantics (+3)
|
||||
|
||||
**Suite:** `hs-upstream-tell`
|
||||
**Failures:**
|
||||
- `attributes refer to the thing being told` — Expected `bar`, got ``
|
||||
- `your symbol represents the thing being told` — Expected `foo`, got ``
|
||||
- `does not overwrite the me symbol` — assertion fail
|
||||
|
||||
**What's needed:** Inside a `tell X` block, `you`/`your` must resolve to X, attribute refs must resolve against X, and `me` must retain its original value (not be rebound to X). Currently `tell` rebinds `me` instead of introducing a separate `you` binding.
|
||||
|
||||
**Estimate:** +3. Scoping fix in the `tell` command handler.
|
||||
|
||||
---
|
||||
|
||||
## Group 3 — `on` event handler features (+19, skip-list)
|
||||
|
||||
**Suite:** `hs-upstream-on`
|
||||
**34 tests on skip-list.** Prioritise tractable subsets:
|
||||
|
||||
### 3a — Event filtering by count (+6)
|
||||
- `can filter events based on count`
|
||||
- `can filter events based on count range`
|
||||
- `can filter events based on unbounded count range`
|
||||
- `can mix ranges`
|
||||
- `on first click fires only once`
|
||||
- `multiple event handlers at a time are allowed to execute with the every keyword`
|
||||
|
||||
The `on (N)`, `on (N to M)`, `on first`, `every` modifiers. Parser + runtime counter state per handler.
|
||||
|
||||
### 3b — `finally` blocks (+6)
|
||||
- `basic finally blocks work`
|
||||
- `async basic finally blocks work`
|
||||
- `exceptions in finally block don't kill the event queue`
|
||||
- `async exceptions in finally block don't kill the event queue`
|
||||
- `finally blocks work when exception thrown in catch`
|
||||
- `async finally blocks work when exception thrown in catch`
|
||||
|
||||
`on … catch … finally` analogous to JS try/catch/finally. Needs a finally-frame in the CEK machine (similar to dynamic-wind).
|
||||
|
||||
### 3c — `elsewhere` modifier (+2)
|
||||
- `supports "elsewhere" modifier`
|
||||
- `supports "from elsewhere" modifier`
|
||||
|
||||
`on click elsewhere` = click outside the element. Needs a global listener + target exclusion check.
|
||||
|
||||
### 3d — Exception events (+3)
|
||||
- `rethrown exceptions trigger 'exception' event`
|
||||
- `uncaught exceptions trigger 'exception' event`
|
||||
- `can catch exceptions thrown in hyperscript functions`
|
||||
- `can catch exceptions thrown in js functions`
|
||||
|
||||
When an unhandled exception escapes an `on` handler, HS must dispatch an `exception` CustomEvent on the element.
|
||||
|
||||
### 3e — Element removal cleanup (+2)
|
||||
- `listeners on other elements are removed when the registering element is removed`
|
||||
- `listeners on self are not removed when the element is removed`
|
||||
|
||||
Cleanup hook via MutationObserver watching for element removal.
|
||||
|
||||
### Deferred (skip-list, complex):
|
||||
- `can be in a top level script tag` — requires script tag re-initialisation
|
||||
- `can ignore when target doesn't exist` — target null guard
|
||||
- `can handle an or after a from clause` — parser edge case
|
||||
- `each behavior installation has its own event queue` — behavior isolation
|
||||
|
||||
---
|
||||
|
||||
## Group 4 — MutationObserver / `on mutation` (+10)
|
||||
|
||||
**Suite:** `hs-upstream-on` (mutation subset, skip-list)
|
||||
**Tests:**
|
||||
- `can listen for attribute mutations`
|
||||
- `can listen for attribute mutations on other elements`
|
||||
- `can listen for childList mutations`
|
||||
- `can listen for general mutations`
|
||||
- `can listen for multiple mutations`
|
||||
- `can listen for multiple mutations 2`
|
||||
- `can listen for specific attribute mutations`
|
||||
- `can pick event properties out by name`
|
||||
- `can pick detail fields out by name`
|
||||
- `attribute observers are persistent (not recreated on re-run)` (hs-upstream-when)
|
||||
|
||||
**What's needed:** MutationObserver mock in the test runner (`hs-run-filtered.js`) + `on mutation` command in the parser/runtime. Already prototyped in Bucket D #32.
|
||||
|
||||
**Estimate:** +10.
|
||||
|
||||
---
|
||||
|
||||
## Group 5 — Cookie API (+5)
|
||||
|
||||
**Suite:** `hs-upstream-expressions/cookies`
|
||||
All 5 tests untranslated. Cookie read/write as an expression: `cookies.name`, `set cookies.name to val`, `cookies.name is undefined`. Needs `document.cookie` mock in runner + cookie-expression parse path.
|
||||
|
||||
**Estimate:** +5. Self-contained.
|
||||
|
||||
---
|
||||
|
||||
## Group 6 — Block literals (+4)
|
||||
|
||||
**Suite:** `hs-upstream-expressions/blockLiteral`
|
||||
All 4 untranslated. Syntax: `[x | x + 1]` — an inline lambda. Used as a first-class value passable to `map`, `filter` etc.
|
||||
|
||||
- `basic block literals work`
|
||||
- `basic identity works`
|
||||
- `basic two arg identity works`
|
||||
- `can map an array`
|
||||
|
||||
**Estimate:** +4. Parser addition + runtime callable wrapping.
|
||||
|
||||
---
|
||||
|
||||
## Group 7 — Async logical operators (+5)
|
||||
|
||||
**Suite:** `hs-upstream-expressions/logicalOperator`
|
||||
Promise-aware `and`/`or`:
|
||||
- `and short-circuits when lhs promise resolves to false`
|
||||
- `or short-circuits when lhs promise resolves to true`
|
||||
- `or evaluates rhs when lhs promise resolves to false`
|
||||
- `should short circuit with and expression`
|
||||
- `should short circuit with or expression`
|
||||
|
||||
**What's needed:** `and`/`or` must await promise operands before short-circuiting. Currently they evaluate eagerly without awaiting.
|
||||
|
||||
**Estimate:** +5. Async await integration in logical operator eval.
|
||||
|
||||
---
|
||||
|
||||
## Group 8 — `evalStatically` (+3)
|
||||
|
||||
**Suite:** `hs-upstream-core/evalStatically`
|
||||
- `throws on math expressions`
|
||||
- `throws on symbol references`
|
||||
- `throws on template strings`
|
||||
|
||||
`_hyperscript.evaluate(src, {}, { throwErrors: true })` must throw synchronously for expressions with side-effects or unresolved symbols. Currently the static evaluator doesn't gate on `throwErrors`.
|
||||
|
||||
**Estimate:** +3. Flag-gated error throw path.
|
||||
|
||||
---
|
||||
|
||||
## Group 9 — Parse error API (+6)
|
||||
|
||||
**Suite:** `hs-upstream-core/parser` + `hs-upstream-core/bootstrap`
|
||||
- `basic parse error messages work`
|
||||
- `fires hyperscript:parse-error event with all errors`
|
||||
- `parse error at EOF on trailing newline does not crash`
|
||||
- `_hyperscript() evaluate API still throws on first error`
|
||||
- `fires hyperscript:before:init and hyperscript:after:init` (bootstrap)
|
||||
- `hyperscript:before:init can cancel initialization` (bootstrap)
|
||||
|
||||
**What's needed:**
|
||||
- Parser must emit a `hyperscript:parse-error` CustomEvent on `document` when compilation fails, with the error list as detail.
|
||||
- `hyperscript:before:init` / `hyperscript:after:init` lifecycle events dispatched around element initialization.
|
||||
- `before:init` can cancel (return false / `event.preventDefault()`).
|
||||
|
||||
**Estimate:** +6. Event dispatch hooks in the bootstrap/init path.
|
||||
|
||||
---
|
||||
|
||||
## Group 10 — `as` expression conversions (+8)
|
||||
|
||||
**Suite:** `hs-upstream-expressions/asExpression`
|
||||
Currently 30/42 = 12 failures. Tractable subset:
|
||||
|
||||
- `converts a NodeList into HTML` — NodeList → outerHTML join
|
||||
- `converts strings into fragments` — string → DocumentFragment
|
||||
- `converts elements into fragments` — element → DocumentFragment
|
||||
- `converts arrays into fragments` — array of elements → DocumentFragment
|
||||
- `converts array as Set` — array → Set (dedup)
|
||||
- `converts object as Map` — object → Map
|
||||
- `can accept custom conversions` — `as MyType` via registered converter
|
||||
- `can use the a modifier if you like` — `as a Number` synonym
|
||||
|
||||
Two already-broken non-skip failures:
|
||||
- `converts a complete form into Values` — Expected `dog`, got ``
|
||||
- `converts multiple selects with programmatically changed selections` — Expected `cat`, got `dog`
|
||||
|
||||
**Estimate:** +8 for the tractable subset. Custom converters and Map/Set require runtime additions.
|
||||
|
||||
---
|
||||
|
||||
## Group 11 — Miscellaneous runtime bugs (+12)
|
||||
|
||||
Small scattered failures, each 1–3 tests:
|
||||
|
||||
| Suite | Failure | Likely cause |
|
||||
|-------|---------|-------------|
|
||||
| `hs-upstream-put` | `properly processes hyperscript` ×3 (got 40, expected 42) | Off-by-one in `put ... before/after` reprocessing |
|
||||
| `hs-upstream-put` | `waits on promises` | Promise await missing from put target eval |
|
||||
| `hs-upstream-js` | `can return values to _hyperscript` | JS block return value not threaded back |
|
||||
| `hs-upstream-js` | `can do both of the above` | Same |
|
||||
| `hs-upstream-js` | `handles rejected promises without hanging` | Rejected promise in js block uncaught |
|
||||
| `hs-upstream-set` | `set waits on promises` | Same as put |
|
||||
| `hs-upstream-set` | `can set into indirect style ref 3` | Indirect style ref path bug |
|
||||
| `hs-upstream-hide` | `retain original display` | `none` vs `block` display tracking |
|
||||
| `hs-upstream-toggle` | `toggle for fixed time` | Timed toggle assertion timing |
|
||||
| `hs-upstream-transition` | `initial value` | `initial` keyword not restoring computed value |
|
||||
| `hs-upstream-expressions/arrayLiteral` | `objects with _order` | `_order` internal key leaking into equality check |
|
||||
| `hs-upstream-core/bootstrap` | 4 bugs | Event handler bugs in reinit, cleanup, respond |
|
||||
| `hs-upstream-expressions/closest` | `where clause` | `where` consumed by `closest` instead of outer |
|
||||
| `hs-upstream-core/scoping` | 2 bugs | Pseudo-possessive, built-in variable clash |
|
||||
|
||||
**Estimate:** +12 once individually triaged.
|
||||
|
||||
---
|
||||
|
||||
## Group 12 — Formerly "hard floor" — now in scope
|
||||
|
||||
Initial assessment was wrong — these are medium difficulty, not genuinely hard. All 16 are worth attempting.
|
||||
|
||||
| Suite | Tests | Actual difficulty | What's needed |
|
||||
|-------|------:|-------------------|---------------|
|
||||
| `hs-upstream-breakpoint` | 2 | **Trivial** | No-op parser command + generator translation. Design: `plans/designs/f-breakpoint.md` |
|
||||
| `hs-upstream-expressions/logicalOperator` (unparenthesized error) | 2 | Low | Parser strictness: `1 + 2 + 3` should throw "ambiguous operator precedence" |
|
||||
| `hs-upstream-core/security` | 1 | Medium | `_hyperscript.config.disableScripting = true` guard at `hs-activate!` time |
|
||||
| `hs-upstream-expressions/asExpression` (Date, custom dynamic) | 3 | Medium | `as a Date` → `new Date(val)`; custom converters via `_hyperscript.addType` registry |
|
||||
| `hs-upstream-on` (remaining skip-list) | ~8 | Medium | Script tag reinit (MutationObserver on `<script>` changes); behavior isolation queue |
|
||||
|
||||
**Breakpoint** — both tests just check that `breakpoint` *parses* without throwing. No devtools. See design doc.
|
||||
|
||||
**Security** — test creates a div with `_="on click add .foo"`, activates it, clicks, asserts `.foo` is NOT added. This is a `disableScripting` config flag: when set, `hs-activate!` skips initialisation. One guard at activation entry.
|
||||
|
||||
**Unparenthesized operator error** — `1 + 2 + 3` in HS is ambiguous (no defined associativity for chained operators). Parser should throw a parse error rather than silently picking left-associativity. Needs a "multiple operators at same precedence level" check after parsing a binary expression.
|
||||
|
||||
**Sequence these last** — after groups 1–11 are done. Breakpoint is a 30-min job and should be pulled into the quick-wins batch.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Group | Tests | Difficulty | Design doc |
|
||||
|-------|------:|-----------|-----------|
|
||||
| 0 — Bucket E payoff | ~47 | Free | (E branches) |
|
||||
| 1 — Null safety | +7 | Low | `f1-null-safety.md` |
|
||||
| 2 — `tell` semantics | +3 | Low | `f2-tell.md` |
|
||||
| 3 — `on` event features | +19 | Medium | (TBD) |
|
||||
| 4 — MutationObserver | +10 | Medium | (TBD) |
|
||||
| 5 — Cookie API | +5 | Low | `f5-cookies.md` |
|
||||
| 6 — Block literals | +4 | Medium | (TBD) |
|
||||
| 7 — Async logical ops | +5 | Medium | (TBD) |
|
||||
| 8 — evalStatically | +3 | Low | `f8-eval-statically.md` |
|
||||
| 9 — Parse error API | +6 | Medium | (TBD) |
|
||||
| 10 — `as` conversions | +8 | Medium | (TBD) |
|
||||
| 11 — Misc bugs | +12 | Low–Medium | (TBD) |
|
||||
| 12 — Breakpoint | +2 | Trivial | `f-breakpoint.md` |
|
||||
| 12 — Security config | +1 | Medium | (TBD) |
|
||||
| 12 — Unparenthesized op error | +2 | Low | (TBD) |
|
||||
| 12 — `as` Date + custom | +3 | Medium | (TBD) |
|
||||
| 12 — `on` remaining | +8 | Medium | (TBD) |
|
||||
| **Total recoverable** | **~145** | | |
|
||||
|
||||
## Group 13 — Step limit + `meta.caller` (+5 → 100%)
|
||||
|
||||
Design doc: `plans/designs/f13-step-limit-and-meta.md`
|
||||
|
||||
| Test | Failure | Fix |
|
||||
|------|---------|-----|
|
||||
| `repeat forever works` (×2) | Step limit — loop terminates in 5 iterations but two compilation warm-up guards eat the budget first | Raise `HS_STEP_LIMIT` to 2,000,000 in `hs-run-filtered.js` |
|
||||
| `hypertrace is reasonable` | Step limit — trace recorder may be on globally inflating step count | Raise step limit; disable global trace if on |
|
||||
| `query template returns values` | Step limit (37s) — CSS template query `<${"p"}/>` may rebuild on every call | Raise step limit; cache compiled template query if still slow |
|
||||
| `has proper stack from event handler` | Wrong value — `meta.caller.meta.feature.type` returns `""` instead of `"onFeature"` | Implement `meta` dict in `def` function call scope; wire `{:feature {:type "onFeature"}}` into event handler contexts |
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Group | Tests | Difficulty | Design doc |
|
||||
|-------|------:|-----------|-----------|
|
||||
| 0 — Bucket E payoff | ~47 | Free | (E branches) |
|
||||
| 1 — Null safety | +7 | Low | `f1-null-safety.md` |
|
||||
| 2 — `tell` semantics | +3 | Low | `f2-tell.md` |
|
||||
| 3 — `on` event features | +19 | Medium | (TBD) |
|
||||
| 4 — MutationObserver | +10 | Medium | (TBD) |
|
||||
| 5 — Cookie API | +5 | Low | `f5-cookies.md` |
|
||||
| 6 — Block literals | +4 | Medium | (TBD) |
|
||||
| 7 — Async logical ops | +5 | Medium | (TBD) |
|
||||
| 8 — evalStatically | +3 | Low | `f8-eval-statically.md` |
|
||||
| 9 — Parse error API | +6 | Medium | (TBD) |
|
||||
| 10 — `as` conversions | +8 | Medium | (TBD) |
|
||||
| 11 — Misc bugs | +12 | Low–Medium | (TBD) |
|
||||
| 12 — Breakpoint | +2 | Trivial | `f-breakpoint.md` |
|
||||
| 12 — Security config | +1 | Medium | (TBD) |
|
||||
| 12 — Unparenthesized op error | +2 | Low | (TBD) |
|
||||
| 12 — `as` Date + custom | +3 | Medium | (TBD) |
|
||||
| 12 — `on` remaining | +8 | Medium | (TBD) |
|
||||
| 13 — Step limit + meta.caller | +5 | Low | `f13-step-limit-and-meta.md` |
|
||||
| **Total recoverable** | **~150** | | |
|
||||
|
||||
**Projected ceiling: ~1299 + 47 + 150 = 1496/1496 = 100%**
|
||||
|
||||
---
|
||||
|
||||
## Suggested sequencing for Bucket F loop
|
||||
|
||||
1. Groups 1, 2, 5, 8 + breakpoint — quick wins, design docs ready, ~20 tests
|
||||
2. Groups 11 misc bugs — isolate and fix one suite at a time
|
||||
3. Group 9 parse error API — hooks into bootstrap, needs care
|
||||
4. Groups 3a, 3b (on-count + finally) — medium, self-contained
|
||||
5. Groups 4 (MutationObserver) + 3c/3d/3e (elsewhere, exceptions, cleanup)
|
||||
6. Groups 6, 7 (block literals, async logical ops) — new syntax
|
||||
7. Group 10 (as conversions) — additive, low regression risk
|
||||
8. Group 12 remainder — security config, unparenthesized op error, as-Date, on remaining
|
||||
|
||||
Each group should get a design doc in `plans/designs/f<N>-<name>.md` before implementation starts.
|
||||
229
plans/koka-on-sx.md
Normal file
229
plans/koka-on-sx.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Koka-on-SX: Koka on the CEK/VM
|
||||
|
||||
Implement a Koka interpreter on SX. The unique angle: Koka's algebraic effects and
|
||||
handlers map directly onto SX's `perform`/`cek-resume` machinery — this is the language
|
||||
that will stress-test whether SX's effect system is principled enough, and expose any
|
||||
gaps. Every other language in the set works around effects ad-hoc; Koka makes them the
|
||||
primary abstraction.
|
||||
|
||||
End-state goal: **core Koka programs running on the SX CEK evaluator**, with algebraic
|
||||
effect handlers wired through `perform`/`cek-resume`. Not a full Koka compiler — no type
|
||||
inference, no row-polymorphic effect types, no LLVM backend — but a faithful runtime for
|
||||
idiomatic Koka programs.
|
||||
|
||||
## What Koka adds that nothing else covers
|
||||
|
||||
- **Structured effect declarations**: `effect state<s> { fun get() : s; fun set(s) : () }`
|
||||
— named, typed effect operations, not just untyped `perform` tokens
|
||||
- **Resumable handlers**: `handler { return(x) -> x; get() -> resume(0); set(x) -> resume(()) }`
|
||||
— multi-shot continuations, handlers as first-class values
|
||||
- **Effect polymorphism**: functions declare their effect set (`a -> <state<int>,console> b`)
|
||||
— exposes whether SX can track which effects are in scope
|
||||
- **Tail-resumptive handlers**: most practical handlers resume exactly once, which should
|
||||
be optimisable — tests whether the CEK machine can detect and collapse this
|
||||
- **Algebraic data types as the foundation**: `type maybe<a> { Nothing; Just(value: a) }`
|
||||
— exercises the Phase 6 ADT primitive directly
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/koka/**` and `plans/koka-on-sx.md`. Do **not** edit `spec/`,
|
||||
`hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Architecture:** Koka source → Koka AST → interpret directly via CEK. No separate
|
||||
Koka evaluator — host the semantics in SX, run on the existing CEK machine.
|
||||
- **Effect types:** defer type inference entirely. Track effects at runtime only — an
|
||||
unhandled effect at the top level raises a runtime error, not a type error.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Koka source text
|
||||
│
|
||||
▼
|
||||
lib/koka/tokenizer.sx — keywords, operators, indent-sensitivity, type-level syntax
|
||||
│
|
||||
▼
|
||||
lib/koka/parser.sx — Koka AST: fun, val, effect, handler, with, match, resume,
|
||||
│ return clause, ADT definitions, basic type expressions
|
||||
▼
|
||||
lib/koka/eval.sx — Koka AST → CEK evaluation via SX primitives:
|
||||
│ ADT (define-type/match from Phase 6)
|
||||
│ Effects (perform/cek-resume from spec/evaluator.sx)
|
||||
│ Coroutines optional (Phase 4 primitives)
|
||||
▼
|
||||
SX CEK evaluator (both JS and OCaml hosts)
|
||||
```
|
||||
|
||||
Key semantic mappings:
|
||||
|
||||
| Koka construct | SX mapping |
|
||||
|---------------|-----------|
|
||||
| `fun f(x) body` | `(define (f x) body)` |
|
||||
| `val x = expr` | `(let ((x expr)) ...)` |
|
||||
| `effect E { fun op() : t }` | register effect tag `E/op` in effect env |
|
||||
| `op()` inside handler scope | `(perform (list "E" "op" args))` |
|
||||
| `handler { return(x)->e; op()->resume(v) }` | `(guard ...)` + `cek-resume` |
|
||||
| `with handler { body }` | install handler for duration of body |
|
||||
| `match x { Nothing -> e1; Just(v) -> e2 }` | SX `(match x ...)` via Phase 6 ADT |
|
||||
| `type maybe<a> { Nothing; Just(value:a) }` | `(define-type maybe (Nothing) (Just value))` |
|
||||
| `resume(v)` in handler | `(cek-resume k v)` where k is captured continuation |
|
||||
| `return(x) -> expr` | final-value clause when no effect fires |
|
||||
|
||||
## Koka semantics in brief
|
||||
|
||||
### Effects and handlers
|
||||
|
||||
```koka
|
||||
effect console
|
||||
fun println(s : string) : ()
|
||||
|
||||
fun greet(name : string) : <console> ()
|
||||
println("Hello, " ++ name)
|
||||
|
||||
fun main()
|
||||
with handler
|
||||
return(x) -> x
|
||||
println(s) -> { print-string(s ++ "\n"); resume(()) }
|
||||
greet("world")
|
||||
```
|
||||
|
||||
- `effect console` declares an effect with one operation `println`
|
||||
- `greet` uses `console` — any call to `println` inside will look up the nearest
|
||||
enclosing handler
|
||||
- `with handler { ... }` installs a handler; `resume(())` continues the suspended
|
||||
computation
|
||||
|
||||
### Multi-shot resumption
|
||||
|
||||
```koka
|
||||
effect choice
|
||||
fun choose() : bool
|
||||
|
||||
fun xor(p : bool, q : bool) : <choice> bool
|
||||
val a = choose()
|
||||
val b = choose()
|
||||
(a || b) && !(a && b)
|
||||
|
||||
fun all-results()
|
||||
with handler
|
||||
return(x) -> [x]
|
||||
choose() -> resume(True) ++ resume(False)
|
||||
xor(True, False)
|
||||
// => [True, True, False, True]
|
||||
```
|
||||
|
||||
This is the test that exposes whether `cek-resume` supports multi-shot (calling the
|
||||
same continuation twice). SX's delimited continuations do support this — Koka will
|
||||
verify it end-to-end.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — Tokenizer + parser (core expressions)
|
||||
|
||||
- [ ] Tokenizer: keywords (`fun`, `val`, `effect`, `handler`, `with`, `match`, `return`,
|
||||
`resume`, `type`, `alias`, `if`, `then`, `else`, `fn`), operators (`++`, `->`,
|
||||
`|>`, `:`, `<`, `>`, `,`), identifiers, numbers, strings, booleans
|
||||
- [ ] Parser — expressions:
|
||||
- literals: int, float, bool (`True`/`False`), string
|
||||
- `val x = e` bindings
|
||||
- `fun f(x, y) body` definitions
|
||||
- `if c then e1 else e2`
|
||||
- `match x { Pat -> e; ... }`
|
||||
- lambda `fn(x) -> e`
|
||||
- function application `f(x, y)`
|
||||
- infix operators: `++`, `+`, `-`, `*`, `/`, `==`, `!=`, `<`, `>`, `&&`, `||`
|
||||
- pipe `|>`: `x |> f` = `f(x)`
|
||||
- [ ] Tests: `lib/koka/tests/parse.sx` — 40+ parse round-trip tests
|
||||
|
||||
### Phase 2 — ADT definitions + match
|
||||
|
||||
- [ ] Parser: `type name<a> { Ctor1; Ctor2(field: t); ... }` declarations
|
||||
- [ ] Eval: map to SX `define-type` + `match` (requires Phase 6 primitives)
|
||||
- [ ] Built-in: `maybe<a>` (Nothing / Just), `result<a,e>` (Ok / Error), `list<a>` (Nil / Cons)
|
||||
- [ ] Tests: ADT construction, matching, nested patterns — 25+ tests
|
||||
|
||||
### Phase 3 — Core evaluator
|
||||
|
||||
- [ ] `koka-eval` entry: walks Koka AST, evaluates in SX env
|
||||
- [ ] Arithmetic, string `++`, comparison, boolean ops
|
||||
- [ ] `val`/`let` binding
|
||||
- [ ] Function definitions and application (first-class functions)
|
||||
- [ ] `if`/`then`/`else`
|
||||
- [ ] `match` with constructor, literal, variable, wildcard patterns
|
||||
- [ ] Basic list ops: `map`, `filter`, `foldl`, `length`, `head`, `tail`
|
||||
- [ ] Tests: `lib/koka/tests/eval.sx` — 40+ tests, pure expressions only
|
||||
|
||||
### Phase 4 — Effect system
|
||||
|
||||
- [ ] Effect declaration: `(koka-declare-effect! "console" (list "println"))`
|
||||
registers operations in a global effect registry
|
||||
- [ ] Effect operation call: when `println(s)` is evaluated inside a handler scope,
|
||||
emit `(perform (list :effect "console" :op "println" :args (list s)))`
|
||||
- [ ] Handler installation: `with handler { return(x)->e; println(s)->resume(v) }`
|
||||
installs a `guard`-like frame that catches `perform` signals matching the effect,
|
||||
binds arguments, and exposes `resume` as a callable that invokes `cek-resume`
|
||||
- [ ] `resume(v)`: calls `(cek-resume captured-k v)` where `captured-k` is the
|
||||
continuation captured at the `perform` point
|
||||
- [ ] `return(x) -> e` clause: handles the normal return value when no effect fires
|
||||
- [ ] Tests: `lib/koka/tests/effects.sx` — 30+ tests:
|
||||
- basic handler (state, console, exception)
|
||||
- unhandled effect → runtime error
|
||||
- nested handlers (inner shadows outer)
|
||||
- multi-shot resumption (choice effect — the key test)
|
||||
- tail-resumptive handler (resumes exactly once — verify no extra allocation)
|
||||
|
||||
### Phase 5 — Standard effect library
|
||||
|
||||
- [ ] `console` effect: `println`, `print`, `readline` (mock)
|
||||
- [ ] `exn` effect: `throw`, `catch` wrappers
|
||||
- [ ] `state<s>` effect: `get`, `set`, `modify`
|
||||
- [ ] `async` effect: `await` mapped to SX `perform` IO suspension
|
||||
- [ ] Tests: programs using each stdlib effect — 20+ tests
|
||||
|
||||
### Phase 6 — Classic Koka programs as integration tests
|
||||
|
||||
- [ ] `counter.koka` — stateful counter via state effect
|
||||
- [ ] `choice.koka` — multi-shot choice generating all results
|
||||
- [ ] `iterator.koka` — yield-based iteration via a custom effect
|
||||
- [ ] `exception.koka` — structured exception handling
|
||||
- [ ] `coroutine.koka` — producer/consumer via two interleaved effects
|
||||
- [ ] Each as a self-contained test in `lib/koka/tests/programs.sx`
|
||||
|
||||
## Key blockers / dependencies
|
||||
|
||||
- **Phase 6 ADT primitive** (`define-type`/`match`) — required before Phase 2.
|
||||
Track: `plans/agent-briefings/primitives-loop.md` Phase 6.
|
||||
- **Multi-shot continuations** — `cek-resume` must support calling the same
|
||||
continuation multiple times. Verify with: `(let ((k #f)) (perform 'x) ...)` called
|
||||
twice. This should already work given the multi-shot delimited continuation work.
|
||||
- **Effect handler stack** — SX's `guard` is not quite the right primitive for
|
||||
deep-handler semantics. May need `(with-handler effect-tag handler-fn body)` as a
|
||||
new evaluator form, or can be emulated via `guard` + `perform` reshaping.
|
||||
|
||||
## Comparison to other languages in the set
|
||||
|
||||
| Language | Effect model |
|
||||
|----------|-------------|
|
||||
| Lua | none (errors only) |
|
||||
| Prolog | none (cuts only) |
|
||||
| Erlang | message-passing (not algebraic) |
|
||||
| Haskell | IO monad (monadic, not algebraic) |
|
||||
| JS | promise/async-await (one-shot) |
|
||||
| Ruby | exceptions + fibers |
|
||||
| **Koka** | **algebraic effects + multi-shot handlers** |
|
||||
|
||||
Koka is the only language that uses SX's effect system as its *primary* computational
|
||||
model. It will expose whether `perform`/`cek-resume` is sufficient or needs typed effect
|
||||
tagging, scoping rules, and a handler stack distinct from `guard`.
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
- _(none yet)_
|
||||
|
||||
## Blockers
|
||||
|
||||
- ADT primitive (Phase 6 of primitives-loop) must land before Phase 2 starts.
|
||||
138
plans/minikanren-on-sx.md
Normal file
138
plans/minikanren-on-sx.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# miniKanren-on-SX: relational programming on the CEK/VM
|
||||
|
||||
miniKanren is not a language to parse — it is an **embedded DSL** implemented as a library
|
||||
of SX functions. No tokenizer, no transpiler. The entire system is a set of `define` forms
|
||||
in `lib/minikanren/`. Programs are SX expressions using the miniKanren API.
|
||||
|
||||
The unique angle: SX's delimited continuation machinery (`perform`/`cek-resume`, call/cc)
|
||||
maps almost perfectly to the search monad miniKanren needs. Backtracking is cooperative
|
||||
suspension, not a separate trail machine. This is the cleanest possible host for miniKanren.
|
||||
|
||||
End-state goal: **full core miniKanren** (`run`, `fresh`, `==`, `conde`, `condu`, `onceo`,
|
||||
`project`, `matche`) + **core.logic-style relations** (`appendo`, `membero`, `listo`,
|
||||
`numbero`, etc.) + **arithmetic constraints** (`fd` domain, `CLP(FD)` subset).
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/minikanren/**` and `plans/minikanren-on-sx.md`. Do **not**
|
||||
edit `spec/`, `hosts/`, `shared/`, or other `lib/<lang>/`.
|
||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Architecture:** pure library — no source parser. Programs are written in SX using the API.
|
||||
- **Reference:** *The Reasoned Schemer* (Friedman/Byrd/Kiselyov) + Byrd's dissertation.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
SX program using miniKanren API
|
||||
│
|
||||
├── lib/minikanren/unify.sx — terms, variables, walk, unification, occurs check
|
||||
├── lib/minikanren/substitution.sx — substitution as association list / hash table
|
||||
├── lib/minikanren/stream.sx — lazy streams of substitutions (via delay/force)
|
||||
├── lib/minikanren/goals.sx — == / fresh / conde / condu / onceo / project / matche
|
||||
├── lib/minikanren/run.sx — run* / run n — drive the search, extract answers
|
||||
├── lib/minikanren/relations.sx — standard relations: appendo, membero, listo, etc.
|
||||
└── lib/minikanren/clpfd.sx — arithmetic constraints (CLP(FD) subset)
|
||||
```
|
||||
|
||||
Key semantic mappings:
|
||||
- **Logic variable** → SX vector of length 1 (mutable box); `make-var` creates fresh one;
|
||||
`walk` follows the substitution chain
|
||||
- **Substitution** → SX association list (or hash table for performance) mapping var → term
|
||||
- **Stream of substitutions** → lazy list using `delay`/`force` (Phase 9 of primitives)
|
||||
- **Goal** → SX function `substitution → stream-of-substitutions`
|
||||
- **`==`** → unifies two terms, extending substitution or failing (empty stream)
|
||||
- **`fresh`** → introduces new logic variables; `(fresh (x y) goal)` → goal with x, y bound
|
||||
- **`conde`** → interleave streams from multiple goal clauses (depth-first with interleaving)
|
||||
- **`run n`** → drive the stream, collect first n substitutions, reify answers
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — variables + unification
|
||||
- [ ] `make-var` → fresh logic variable (unique mutable box)
|
||||
- [ ] `var?` `v` → bool — is this a logic variable?
|
||||
- [ ] `walk` `term` `subst` → follow substitution chain to ground term or unbound var
|
||||
- [ ] `walk*` `term` `subst` → deep walk (recurse into lists/dicts)
|
||||
- [ ] `unify` `u` `v` `subst` → extended substitution or `#f` (failure)
|
||||
Handles: var/var, var/term, term/var, list unification, number/string/symbol equality.
|
||||
No occurs check by default; `unify-check` with occurs check as opt-in.
|
||||
- [ ] Empty substitution `empty-s` = `(list)` (empty assoc list)
|
||||
- [ ] Tests in `lib/minikanren/tests/unify.sx`: ground terms, vars, lists, failure, occurs
|
||||
|
||||
### Phase 2 — streams + goals
|
||||
- [ ] Stream type: `mzero` (empty stream = `nil`), `unit s` (singleton = `(list s)`),
|
||||
`mplus` (interleave two streams), `bind` (apply goal to stream)
|
||||
- [ ] Lazy streams via `delay`/`force` — mature pairs for depth-first, immature for lazy
|
||||
- [ ] `==` goal: `(fn (s) (let ((s2 (unify u v s))) (if s2 (unit s2) mzero)))`
|
||||
- [ ] `succeed` / `fail` — trivial goals
|
||||
- [ ] `fresh` — `(fn (f) (fn (s) ((f (make-var)) s)))` — introduces one var; `fresh*` for many
|
||||
- [ ] `conde` — interleaving disjunction of goal lists
|
||||
- [ ] `condu` — committed choice (soft-cut): only explores first successful clause
|
||||
- [ ] `onceo` — succeeds at most once
|
||||
- [ ] Tests: basic goal composition, backtracking, interleaving
|
||||
|
||||
### Phase 3 — run + reification
|
||||
- [ ] `run*` `goal` → list of all answers (reified)
|
||||
- [ ] `run n` `goal` → list of first n answers
|
||||
- [ ] `reify` `term` `subst` → replace unbound vars with `_0`, `_1`, ... names
|
||||
- [ ] `reify-s` → builds reification substitution for naming unbound vars consistently
|
||||
- [ ] `fresh` with multiple variables: `(fresh (x y z) goal)` sugar
|
||||
- [ ] Query variable conventions: `q` as canonical query variable
|
||||
- [ ] Tests: classic miniKanren programs — `(run* q (== q 1))` → `(1)`,
|
||||
`(run* q (conde ((== q 1)) ((== q 2))))` → `(1 2)`,
|
||||
Peano arithmetic, `appendo` preview
|
||||
|
||||
### Phase 4 — standard relations
|
||||
- [ ] `appendo` `l` `s` `ls` — list append, runs forwards and backwards
|
||||
- [ ] `membero` `x` `l` — x is a member of l
|
||||
- [ ] `listo` `l` — l is a proper list
|
||||
- [ ] `nullo` `l` — l is empty
|
||||
- [ ] `pairo` `p` — p is a pair (cons cell)
|
||||
- [ ] `caro` `p` `a` — car of pair
|
||||
- [ ] `cdro` `p` `d` — cdr of pair
|
||||
- [ ] `conso` `a` `d` `p` — cons
|
||||
- [ ] `firsto` / `resto` — aliases for caro/cdro
|
||||
- [ ] `reverseo` `l` `r` — reverse of list
|
||||
- [ ] `flatteno` `l` `f` — flatten nested lists
|
||||
- [ ] `permuteo` `l` `p` — permutation of list
|
||||
- [ ] `lengtho` `l` `n` — length as a relation (Peano or integer)
|
||||
- [ ] Tests: run each relation forwards and backwards; generate from partial inputs
|
||||
|
||||
### Phase 5 — `project` + `matche` + negation
|
||||
- [ ] `project` `(x ...) body` — access reified values of logic vars inside a goal;
|
||||
escapes to ground values for arithmetic or string ops
|
||||
- [ ] `matche` — pattern matching over logic terms (extension from core.logic)
|
||||
`(matche l ((head . tail) goal) (() goal))`
|
||||
- [ ] `conda` — soft-cut disjunction (like Prolog `->`)
|
||||
- [ ] `condu` — committed choice (already in phase 2; refine semantics here)
|
||||
- [ ] `nafc` — negation as finite failure with constraint
|
||||
- [ ] Tests: Zebra puzzle, N-queens, Sudoku via `project`, family relations via `matche`
|
||||
|
||||
### Phase 6 — arithmetic constraints CLP(FD)
|
||||
- [ ] Finite domain variables: `fd-var` with domain `[lo..hi]`
|
||||
- [ ] `in` `x` `domain` — constrain x to domain
|
||||
- [ ] `fd-eq` `x` `y` — x = y (constraint propagation)
|
||||
- [ ] `fd-neq` `x` `y` — x ≠ y
|
||||
- [ ] `fd-lt` `fd-lte` `fd-gt` `fd-gte` — ordering constraints
|
||||
- [ ] `fd-plus` `x` `y` `z` — x + y = z (constraint)
|
||||
- [ ] `fd-times` `x` `y` `z` — x * y = z
|
||||
- [ ] Arc consistency propagation — when domain narrows, propagate to constrained vars
|
||||
- [ ] Labelling: `fd-run` drives search by splitting domains when propagation stalls
|
||||
- [ ] Tests: send-more-money, N-queens with CLP(FD), map coloring, cryptarithmetic
|
||||
|
||||
### Phase 7 — tabling (memoization of relations)
|
||||
- [ ] `tabled` annotation: memoize calls to a relation using a hash table
|
||||
- [ ] Prevents infinite loops in recursive relations like `patho` on cyclic graphs
|
||||
- [ ] Producer/consumer scheduling for tabled relations (variant of SLG resolution)
|
||||
- [ ] Tests: cyclic graph reachability, mutual recursion, Fibonacci via tabling
|
||||
|
||||
## Blockers
|
||||
|
||||
_(none yet)_
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
_(awaiting phase 1)_
|
||||
315
plans/ocaml-on-sx.md
Normal file
315
plans/ocaml-on-sx.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# OCaml-on-SX: OCaml + ReasonML + Dream on the CEK/VM
|
||||
|
||||
The meta-circular demo: SX's native evaluator is OCaml, so implementing OCaml on top of
|
||||
SX closes the loop — the source language of the host is running inside the host it
|
||||
compiles to. Beyond the elegance, it's practically useful: once OCaml expressions run on
|
||||
the SX CEK/VM you get Dream (a clean OCaml web framework) almost for free, and ReasonML
|
||||
is a syntax variant that shares the same transpiler output.
|
||||
|
||||
End-state goal: **OCaml programs running on the SX CEK/VM**, with enough of the standard
|
||||
library to support Dream's middleware model. Dream-on-SX is the integration target —
|
||||
a `handler`/`middleware`/`router` API that feels idiomatic while running purely in SX.
|
||||
ReasonML (Phase 8) adds an alternative syntax frontend that targets the same transpiler.
|
||||
|
||||
## What this covers that nothing else in the set does
|
||||
|
||||
- **Strict ML semantics** — unlike Haskell, OCaml is call-by-value with explicit `Lazy.t`
|
||||
for laziness. Pattern match is exhaustive. Polymorphic variants. Structural equality.
|
||||
- **First-class modules and functors** — modules as values (phase 4); functors as SX
|
||||
higher-order functions over module records. Unlike Haskell typeclasses, OCaml's module
|
||||
system is explicit and compositional.
|
||||
- **Mutable state without monads** — `ref`, `:=`, `!` are primitives. Arrays. `Hashtbl`.
|
||||
The IO model is direct; `Lwt`/Dream map to `perform`/`cek-resume` for async.
|
||||
- **Dream's composable HTTP model** — `handler = request -> response promise`,
|
||||
`middleware = handler -> handler`. Algebraically clean; `@@` composition maps to SX
|
||||
function composition trivially.
|
||||
- **ReasonML** — same semantics, JS-friendly surface syntax. JSX variant pairs with SX
|
||||
component rendering.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **Scope:** only touch `lib/ocaml/**`, `lib/dream/**`, `lib/reasonml/**`, and
|
||||
`plans/ocaml-on-sx.md`. Do **not** edit `spec/`, `hosts/`, `shared/`, or other
|
||||
`lib/<lang>/`.
|
||||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix here.
|
||||
- **SX files:** use `sx-tree` MCP tools only.
|
||||
- **Architecture:** OCaml source → AST → SX AST → CEK. No standalone OCaml evaluator.
|
||||
The OCaml AST is walked by an `ocaml-eval` function in SX that produces SX values.
|
||||
- **Type system:** deferred until Phase 5. Phases 1–4 are intentionally untyped —
|
||||
get the evaluator right first, then layer HM inference on top.
|
||||
- **Dream:** implemented as a library in Phase 7; no separate build step. `Dream.run`
|
||||
wraps SX's existing HTTP server machinery via `perform`/`cek-resume`.
|
||||
- **Commits:** one feature per commit. Keep `## Progress log` updated and tick boxes.
|
||||
|
||||
## Architecture sketch
|
||||
|
||||
```
|
||||
OCaml source text
|
||||
│
|
||||
▼
|
||||
lib/ocaml/tokenizer.sx — keywords, operators, string/char literals, comments
|
||||
│
|
||||
▼
|
||||
lib/ocaml/parser.sx — OCaml AST: let/let rec, fun, match, if, begin/end,
|
||||
│ module/struct/functor, type decls, expressions
|
||||
▼
|
||||
lib/ocaml/desugar.sx — surface → core: tuple patterns, or-patterns,
|
||||
│ sequence (;) → (do), when guards, field punning
|
||||
▼
|
||||
lib/ocaml/transpile.sx — OCaml AST → SX AST
|
||||
│
|
||||
▼
|
||||
lib/ocaml/runtime.sx — ADT constructors, module primitives, ref/array ops,
|
||||
│ Stdlib shims, Dream server (phase 7)
|
||||
▼
|
||||
SX CEK evaluator (both JS and OCaml hosts)
|
||||
```
|
||||
|
||||
## Semantic mappings
|
||||
|
||||
| OCaml construct | SX mapping |
|
||||
|----------------|-----------|
|
||||
| `let x = e` (top-level) | `(define x e)` |
|
||||
| `let f x y = e` | `(define (f x y) e)` |
|
||||
| `let rec f x = e` | `(define (f x) e)` — SX define is already recursive |
|
||||
| `fun x -> e` | `(fn (x) e)` |
|
||||
| `e1 \|> f` | `(f e1)` — pipe desugars to reverse application |
|
||||
| `e1; e2` | `(do e1 e2)` |
|
||||
| `begin e1; e2; e3 end` | `(do e1 e2 e3)` |
|
||||
| `if c then e1 else e2` | `(if c e1 e2)` |
|
||||
| `match x with \| P -> e` | `(match x (P e) ...)` via Phase 6 ADT primitive |
|
||||
| `type t = A \| B of int` | `(define-type t (A) (B v))` |
|
||||
| `module M = struct ... end` | SX dict `{:let-bindings ...}` — module as record |
|
||||
| `functor (M : S) -> ...` | `(fn (M) ...)` — functor as SX lambda over module record |
|
||||
| `open M` | inject M's bindings into scope via `env-merge` |
|
||||
| `M.field` | `(get M :field)` |
|
||||
| `{ r with f = v }` | `(dict-set r :f v)` |
|
||||
| `ref x` | `(make-ref x)` — mutable cell |
|
||||
| `!r` | `(deref-ref r)` |
|
||||
| `r := v` | `(set-ref! r v)` |
|
||||
| `(a, b, c)` | tagged list `(:tuple a b c)` |
|
||||
| `[1; 2; 3]` | `(list 1 2 3)` |
|
||||
| `[| 1; 2; 3 |]` | `(make-array 1 2 3)` (Phase 6) |
|
||||
| `try e with \| Ex -> h` | `(guard (fn (ex) h) e)` via SX exception system |
|
||||
| `raise Ex` | `(perform (:raise Ex))` |
|
||||
| `Printf.printf "%d" x` | `(perform (:print (format "%d" x)))` |
|
||||
|
||||
## Dream semantic mappings (Phase 7)
|
||||
|
||||
| Dream construct | SX mapping |
|
||||
|----------------|-----------|
|
||||
| `handler = request -> response promise` | `(fn (req) (perform (:http-respond ...)))` |
|
||||
| `middleware = handler -> handler` | `(fn (next) (fn (req) ...))` |
|
||||
| `Dream.router [routes]` | `(ocaml-dream-router routes)` — dispatch on method+path |
|
||||
| `Dream.get "/path" h` | route record `{:method "GET" :path "/path" :handler h}` |
|
||||
| `Dream.scope "/p" [ms] [rs]` | prefix mount with middleware chain |
|
||||
| `Dream.param req "name"` | path param extracted during routing |
|
||||
| `m1 @@ m2 @@ handler` | `(m1 (m2 handler))` — left-fold composition |
|
||||
| `Dream.session_field req "k"` | `(perform (:session-get req "k"))` |
|
||||
| `Dream.set_session_field req "k" v` | `(perform (:session-set req "k" v))` |
|
||||
| `Dream.flash req` | `(perform (:flash-get req))` |
|
||||
| `Dream.form req` | `(perform (:form-parse req))` — returns Ok/Error ADT |
|
||||
| `Dream.websocket handler` | `(perform (:websocket handler))` |
|
||||
| `Dream.run handler` | starts SX HTTP server with handler as root |
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Phase 1 — Tokenizer + parser
|
||||
|
||||
- [ ] **Tokenizer:** keywords (`let`, `rec`, `in`, `fun`, `function`, `match`, `with`,
|
||||
`type`, `of`, `module`, `struct`, `end`, `functor`, `sig`, `open`, `include`,
|
||||
`if`, `then`, `else`, `begin`, `try`, `exception`, `raise`, `mutable`,
|
||||
`for`, `while`, `do`, `done`, `and`, `as`, `when`), operators (`->`, `|>`,
|
||||
`<|`, `@@`, `@`, `:=`, `!`, `::`, `**`, `:`, `;`, `;;`), identifiers (lower,
|
||||
upper/ctor, labels `~label:`, optional `?label:`), char literals `'c'`,
|
||||
string literals (escaped + heredoc `{|...|}`), int/float literals,
|
||||
line comments `(*` nested block comments `*)`.
|
||||
- [ ] **Parser:** top-level `let`/`let rec`/`type`/`module`/`exception`/`open`/`include`
|
||||
declarations; expressions: literals, identifiers, constructor application,
|
||||
lambda, application (left-assoc), binary ops with precedence table,
|
||||
`if`/`then`/`else`, `match`/`with`, `try`/`with`, `let`/`in`, `begin`/`end`,
|
||||
`fun`/`function`, tuples, list literals, record literals/updates, field access,
|
||||
sequences `;`, unit `()`.
|
||||
- [ ] **Patterns:** constructor, literal, variable, wildcard `_`, tuple, list cons `::`,
|
||||
list literal, record, `as`, or-pattern `P1 | P2`, `when` guard.
|
||||
- [ ] OCaml is **not** indentation-sensitive — no layout algorithm needed.
|
||||
- [ ] Tests in `lib/ocaml/tests/parse.sx` — 50+ round-trip parse tests.
|
||||
|
||||
### Phase 2 — Core evaluator (untyped)
|
||||
|
||||
- [ ] `ocaml-eval` entry: walks OCaml AST, produces SX values.
|
||||
- [ ] `let`/`let rec`/`let ... in` (mutually recursive with `and`).
|
||||
- [ ] Lambda + application (curried by default — auto-curry multi-param defs).
|
||||
- [ ] `fun`/`function` (single-arg lambda with immediate match on arg).
|
||||
- [ ] `if`/`then`/`else`, `begin`/`end`, sequence `;`.
|
||||
- [ ] Arithmetic, comparison, boolean ops, string `^`, `mod`.
|
||||
- [ ] Unit `()` value; `ignore`.
|
||||
- [ ] References: `ref`, `!`, `:=`.
|
||||
- [ ] Mutable record fields.
|
||||
- [ ] `for i = lo to hi do ... done` loop; `while cond do ... done`.
|
||||
- [ ] `try`/`with` — maps to SX `guard`; `raise` via perform.
|
||||
- [ ] Tests in `lib/ocaml/tests/eval.sx` — 50+ tests, pure + imperative.
|
||||
|
||||
### Phase 3 — ADTs + pattern matching
|
||||
|
||||
- [ ] `type` declarations: `type t = A | B of t1 * t2 | C of { x: int }`.
|
||||
- [ ] Constructors as tagged lists: `A` → `(:A)`, `B(1, "x")` → `(:B 1 "x")`.
|
||||
- [ ] `match`/`with`: constructor, literal, variable, wildcard, tuple, list cons/nil,
|
||||
`as` binding, or-patterns, nested patterns, `when` guard.
|
||||
- [ ] Exhaustiveness: runtime error on incomplete match (no compile-time check yet).
|
||||
- [ ] Built-in types: `option` (`None`/`Some`), `result` (`Ok`/`Error`),
|
||||
`list` (nil/cons), `bool`, `unit`, `exn`.
|
||||
- [ ] `exception` declarations; built-in: `Not_found`, `Invalid_argument`,
|
||||
`Failure`, `Match_failure`.
|
||||
- [ ] Polymorphic variants (surface syntax `\`Tag value`; runtime same tagged list).
|
||||
- [ ] Tests in `lib/ocaml/tests/adt.sx` — 40+ tests: ADTs, match, option/result.
|
||||
|
||||
### Phase 4 — Modules + functors
|
||||
|
||||
- [ ] `module M = struct let x = 1 let f y = x + y end` → SX dict `{:x 1 :f <fn>}`.
|
||||
- [ ] `module type S = sig val x : int val f : int -> int end` → interface record
|
||||
(runtime stub; typed checking in Phase 5).
|
||||
- [ ] `module M : S = struct ... end` — coercive sealing (runtime: pass-through).
|
||||
- [ ] `functor (M : S) -> struct ... end` → SX `(fn (M) ...)`.
|
||||
- [ ] `module F = Functor(Base)` — functor application.
|
||||
- [ ] `open M` — merge M's dict into current env (`env-merge`).
|
||||
- [ ] `include M` — same as open at structure level.
|
||||
- [ ] `M.name` — dict get via `:name` key.
|
||||
- [ ] First-class modules (pack/unpack) — deferred to Phase 5.
|
||||
- [ ] Standard module hierarchy: `List`, `Option`, `Result`, `String`, `Char`,
|
||||
`Int`, `Float`, `Bool`, `Unit`, `Printf`, `Format` (stubs, filled in Phase 6).
|
||||
- [ ] Tests in `lib/ocaml/tests/modules.sx` — 30+ tests.
|
||||
|
||||
### Phase 5 — Hindley-Milner type inference
|
||||
|
||||
- [ ] Algorithm W: `gen`/`inst`, `unify`, `infer-expr`, `infer-decl`.
|
||||
- [ ] Type variables: `'a`, `'b`; unification with occur-check.
|
||||
- [ ] Let-polymorphism: generalise at let-bindings.
|
||||
- [ ] ADT types: `type 'a option = None | Some of 'a`.
|
||||
- [ ] Function types, tuple types, record types.
|
||||
- [ ] Type signatures: `val f : int -> int` — verify against inferred type.
|
||||
- [ ] Module type checking: seal against `sig` (Phase 4 stubs become real checks).
|
||||
- [ ] Error reporting: position-tagged errors with expected vs actual types.
|
||||
- [ ] First-class modules: `(module M : S)` pack; `(val m : (module S))` unpack.
|
||||
- [ ] No rank-2 polymorphism, no GADTs (out of scope).
|
||||
- [ ] Tests in `lib/ocaml/tests/types.sx` — 60+ inference tests.
|
||||
|
||||
### Phase 6 — Standard library
|
||||
|
||||
- [ ] `List`: `map`, `filter`, `fold_left`, `fold_right`, `length`, `rev`, `append`,
|
||||
`concat`, `flatten`, `iter`, `iteri`, `mapi`, `for_all`, `exists`, `find`,
|
||||
`find_opt`, `mem`, `assoc`, `assq`, `sort`, `stable_sort`, `nth`, `hd`, `tl`,
|
||||
`init`, `combine`, `split`, `partition`.
|
||||
- [ ] `Option`: `map`, `bind`, `fold`, `get`, `value`, `join`, `iter`, `to_list`,
|
||||
`to_result`, `is_none`, `is_some`.
|
||||
- [ ] `Result`: `map`, `bind`, `fold`, `get_ok`, `get_error`, `map_error`,
|
||||
`to_option`, `is_ok`, `is_error`.
|
||||
- [ ] `String`: `length`, `get`, `sub`, `concat`, `split_on_char`, `trim`,
|
||||
`uppercase_ascii`, `lowercase_ascii`, `contains`, `starts_with`, `ends_with`,
|
||||
`index_opt`, `replace_all` (non-stdlib but needed).
|
||||
- [ ] `Char`: `code`, `chr`, `escaped`, `lowercase_ascii`, `uppercase_ascii`.
|
||||
- [ ] `Int`/`Float`: arithmetic, `to_string`, `of_string_opt`, `min_int`, `max_int`.
|
||||
- [ ] `Hashtbl`: `create`, `add`, `replace`, `find`, `find_opt`, `remove`, `mem`,
|
||||
`iter`, `fold`, `length` — backed by SX mutable dict.
|
||||
- [ ] `Map.Make` functor — balanced BST backed by SX sorted dict.
|
||||
- [ ] `Set.Make` functor.
|
||||
- [ ] `Printf`: `sprintf`, `printf`, `eprintf` — format strings via `(format ...)`.
|
||||
- [ ] `Sys`: `argv`, `getenv_opt`, `getcwd` — via `perform` IO.
|
||||
- [ ] Scoreboard runner: `lib/ocaml/conformance.sh` + `scoreboard.json`.
|
||||
- [ ] Target: 150+ tests across all stdlib modules.
|
||||
|
||||
### Phase 7 — Dream web framework (`lib/dream/`)
|
||||
|
||||
The five types: `request`, `response`, `handler = request -> response`,
|
||||
`middleware = handler -> handler`, `route`. Everything else is a function over these.
|
||||
|
||||
- [ ] **Core types** in `lib/dream/types.sx`: request/response records, route record.
|
||||
- [ ] **Router** in `lib/dream/router.sx`:
|
||||
- `dream-get path handler`, `dream-post path handler`, etc. for all HTTP methods.
|
||||
- `dream-scope prefix middlewares routes` — prefix mount with middleware chain.
|
||||
- `dream-router routes` — dispatch tree, returns handler; no match → 404.
|
||||
- Path param extraction: `:name` segments, `**` wildcard.
|
||||
- `dream-param req name` — retrieve matched path param.
|
||||
- [ ] **Middleware** in `lib/dream/middleware.sx`:
|
||||
- `dream-pipeline middlewares handler` — compose middleware left-to-right.
|
||||
- `dream-no-middleware` — identity.
|
||||
- Logger: `(dream-logger next req)` — logs method, path, status, timing.
|
||||
- Content-type sniffer.
|
||||
- [ ] **Sessions** in `lib/dream/session.sx`:
|
||||
- Cookie-backed session middleware.
|
||||
- `dream-session-field req key`, `dream-set-session-field req key val`.
|
||||
- `dream-invalidate-session req`.
|
||||
- [ ] **Flash messages** in `lib/dream/flash.sx`:
|
||||
- `dream-flash-middleware` — single-request cookie store.
|
||||
- `dream-add-flash-message req category msg`.
|
||||
- `dream-flash-messages req` — returns list of `(category, msg)`.
|
||||
- [ ] **Forms + CSRF** in `lib/dream/form.sx`:
|
||||
- `dream-form req` — returns `(Ok fields)` or `(Err :csrf-token-invalid)`.
|
||||
- `dream-multipart req` — streaming multipart form data.
|
||||
- CSRF middleware: stateless signed tokens, session-scoped.
|
||||
- `dream-csrf-tag req` — returns hidden input fragment for SX templates.
|
||||
- [ ] **WebSockets** in `lib/dream/websocket.sx`:
|
||||
- `dream-websocket handler` — upgrades request; handler `(fn (ws) ...)`.
|
||||
- `dream-send ws msg`, `dream-receive ws`, `dream-close ws`.
|
||||
- [ ] **Static files:** `dream-static root-path` — serves files, ETags, range requests.
|
||||
- [ ] **`dream-run`**: wires root handler into SX's `perform (:http-listen ...)`.
|
||||
- [ ] **Demos** in `lib/dream/demos/`:
|
||||
- `hello.ml` → `lib/dream/demos/hello.sx`: "Hello, World!" route.
|
||||
- `counter.ml` → `lib/dream/demos/counter.sx`: in-memory counter with sessions.
|
||||
- `chat.ml` → `lib/dream/demos/chat.sx`: multi-room WebSocket chat.
|
||||
- `todo.ml` → `lib/dream/demos/todo.sx`: CRUD list with forms + CSRF.
|
||||
- [ ] Tests in `lib/dream/tests/`: routing dispatch, middleware composition,
|
||||
session round-trip, CSRF accept/reject, flash read-after-write — 60+ tests.
|
||||
|
||||
### Phase 8 — ReasonML syntax variant (`lib/reasonml/`)
|
||||
|
||||
ReasonML is OCaml with a JS-friendly surface: semicolons, `let` with `=` everywhere,
|
||||
`=>` for lambdas, `switch` for match, `{j|...|j}` string interpolation. Same semantics —
|
||||
different tokenizer + parser, same `lib/ocaml/transpile.sx` output.
|
||||
|
||||
- [ ] **Tokenizer** in `lib/reasonml/tokenizer.sx`:
|
||||
- `let x = e;` binding syntax (semicolons required).
|
||||
- `(x, y) => e` arrow function syntax.
|
||||
- `switch (x) { | Pat => e | ... }` for match.
|
||||
- JSX: `<Comp prop=val />`, `<div>children</div>`.
|
||||
- String interpolation: `{j|hello $(name)|j}`.
|
||||
- Type annotations: `x : int`, `let f : int => int = x => x + 1`.
|
||||
- [ ] **Parser** in `lib/reasonml/parser.sx`:
|
||||
- Produce same OCaml AST nodes as `lib/ocaml/parser.sx`.
|
||||
- JSX → SX component calls: `<Comp x=1 />` → `(~comp :x 1)`.
|
||||
- Multi-arg functions: `(x, y) => e` → auto-curried pair.
|
||||
- [ ] Shared transpiler: `lib/reasonml/transpile.sx` delegates to
|
||||
`lib/ocaml/transpile.sx` (parse → ReasonML AST → OCaml AST → SX AST).
|
||||
- [ ] Tests in `lib/reasonml/tests/`: tokenizer, parser, eval, JSX — 40+ tests.
|
||||
- [ ] ReasonML Dream demos: translate Phase 7 demos to ReasonML syntax.
|
||||
|
||||
## The meta-circular angle
|
||||
|
||||
SX is bootstrapped to OCaml (`hosts/ocaml/`). Running OCaml inside SX running on OCaml is
|
||||
the "mother tongue" closure: OCaml → SX → OCaml. This means:
|
||||
|
||||
- The OCaml host's native pattern matching and ADTs are exact reference semantics for
|
||||
the SX-level implementation — any mismatch is a bug.
|
||||
- The SX `match` / `define-type` primitives (Phase 6 of the primitives roadmap) were
|
||||
built knowing OCaml was the intended target.
|
||||
- When debugging the transpiler, the OCaml REPL is always available as oracle.
|
||||
- Dream running in SX can serve the sx.rose-ash.com docs site — the framework that
|
||||
describes the runtime it runs on.
|
||||
|
||||
## Key dependencies
|
||||
|
||||
- **Phase 6 ADT primitive** (`define-type`/`match`) — required before Phase 3.
|
||||
- **`perform`/`cek-resume`** IO suspension — required before Phase 7 (Dream async).
|
||||
- **HO forms** and first-class lambdas — already in spec, no blocker.
|
||||
- **Module system** (Phase 4) is independent of type inference (Phase 5) — can overlap.
|
||||
- **ReasonML** (Phase 8) can start once OCaml parser is stable (after Phase 2).
|
||||
|
||||
## Progress log
|
||||
|
||||
_Newest first._
|
||||
|
||||
_(awaiting phase 1)_
|
||||
|
||||
## Blockers
|
||||
|
||||
_(none yet)_
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user