diff --git a/lib/tcl/runtime.sx b/lib/tcl/runtime.sx new file mode 100644 index 00000000..395bb22b --- /dev/null +++ b/lib/tcl/runtime.sx @@ -0,0 +1,279 @@ +;; lib/tcl/runtime.sx — Tcl primitives on SX +;; +;; Provides Tcl-idiomatic wrappers over SX built-ins. +;; Primitives used: +;; make-regexp/regexp-match/regexp-match-all/... (Phase 19) +;; make-set/set-add!/set-member?/set-remove!/set->list (Phase 18) +;; call/cc (core evaluator) +;; quotient/remainder (Phase 15 / builtin) +;; string->list/list->string/char->integer (Phase 13) + +;; --------------------------------------------------------------------------- +;; 1. String buffer — Tcl append / string accumulation +;; --------------------------------------------------------------------------- + +(define + (tcl-sb-new) + (let + ((sb (dict))) + (dict-set! sb "_tcl_sb" true) + (dict-set! sb "_buf" "") + sb)) + +(define (tcl-sb? v) (and (dict? v) (dict-has? v "_tcl_sb"))) + +(define + (tcl-sb-append! sb s) + (dict-set! sb "_buf" (str (get sb "_buf") s)) + sb) + +(define (tcl-sb-value sb) (get sb "_buf")) + +(define (tcl-sb-clear! sb) (dict-set! sb "_buf" "") sb) + +(define (tcl-sb-length sb) (len (get sb "_buf"))) + +;; --------------------------------------------------------------------------- +;; 2. String port (channel) — Tcl channel abstraction +;; Read channel: created from a string, supports gets/read. +;; Write channel: accumulates puts output, queryable via tcl-chan-string. +;; --------------------------------------------------------------------------- + +(define + (tcl-chan-in-new str) + (let + ((c (dict))) + (dict-set! c "_tcl_chan" true) + (dict-set! c "_mode" "read") + (dict-set! c "_chars" (string->list str)) + (dict-set! c "_pos" 0) + c)) + +(define + (tcl-chan-out-new) + (let + ((c (dict))) + (dict-set! c "_tcl_chan" true) + (dict-set! c "_mode" "write") + (dict-set! c "_buf" "") + c)) + +(define (tcl-chan? v) (and (dict? v) (dict-has? v "_tcl_chan"))) + +(define + (tcl-chan-eof? c) + (and + (= (get c "_mode") "read") + (>= (get c "_pos") (len (get c "_chars"))))) + +(define + (tcl-chan-read-char c) + (if + (tcl-chan-eof? c) + nil + (let + ((ch (nth (get c "_chars") (get c "_pos")))) + (dict-set! c "_pos" (+ (get c "_pos") 1)) + ch))) + +;; gets — read one line (up to newline or EOF), return without trailing newline +(define + (tcl-chan-gets c) + (letrec + ((go (fn (acc) (let ((ch (tcl-chan-read-char c))) (cond ((= ch nil) (list->string (reverse acc))) ((= (char->integer ch) 10) (list->string (reverse acc))) (else (go (cons ch acc)))))))) + (go (list)))) + +;; read — read all remaining chars +(define + (tcl-chan-read c) + (letrec + ((go (fn (acc) (let ((ch (tcl-chan-read-char c))) (if (= ch nil) (list->string (reverse acc)) (go (cons ch acc))))))) + (go (list)))) + +;; puts — write string to write channel (no newline) +(define + (tcl-chan-puts! c s) + (when + (= (get c "_mode") "write") + (dict-set! c "_buf" (str (get c "_buf") s))) + c) + +;; puts-line — write string + newline (Tcl default puts behaviour) +(define (tcl-chan-puts-line! c s) (tcl-chan-puts! c (str s "\n"))) + +;; string — get accumulated content of write channel +(define (tcl-chan-string c) (get c "_buf")) + +;; tell — current read position +(define (tcl-chan-tell c) (get c "_pos")) + +;; --------------------------------------------------------------------------- +;; 3. Regexp — Tcl regexp / regsub wrappers +;; --------------------------------------------------------------------------- + +(define (tcl-re-new pattern) (make-regexp pattern "")) + +(define (tcl-re-new-flags pattern flags) (make-regexp pattern flags)) + +(define (tcl-re? v) (regexp? v)) + +(define (tcl-re-match? rx str) (not (= (regexp-match rx str) nil))) + +(define (tcl-re-match rx str) (regexp-match rx str)) + +(define (tcl-re-match-all rx str) (regexp-match-all rx str)) + +(define (tcl-re-sub rx str replacement) (regexp-replace rx str replacement)) + +(define + (tcl-re-sub-all rx str replacement) + (regexp-replace-all rx str replacement)) + +(define (tcl-re-split rx str) (regexp-split rx str)) + +;; --------------------------------------------------------------------------- +;; 4. Format — Tcl format command (%s %d %f %x %o %%) +;; tcl-format takes a format string and a list of arguments. +;; Example: (tcl-format "%s is %d" (list "Alice" 30)) → "Alice is 30" +;; --------------------------------------------------------------------------- + +;; Digit characters for base conversion +(define tcl-hex-chars (string->list "0123456789abcdef")) + +(define + (tcl-digits-for-base n base digit-chars) + (let + ((abs-n (if (< n 0) (- 0 n) n))) + (letrec + ((go (fn (n acc) (if (= n 0) (if (= (len acc) 0) "0" (list->string acc)) (go (quotient n base) (cons (nth digit-chars (remainder n base)) acc)))))) + (let + ((unsigned (go abs-n (list)))) + (if (< n 0) (str "-" unsigned) unsigned))))) + +(define + (tcl-format-hex n) + (tcl-digits-for-base (truncate n) 16 tcl-hex-chars)) + +(define + (tcl-format-oct n) + (tcl-digits-for-base (truncate n) 8 (string->list "01234567"))) + +(define + (tcl-format fmt args) + (letrec + ((chars (string->list fmt)) + (go + (fn + (cs arg-list result) + (if + (= (len cs) 0) + result + (let + ((c-int (char->integer (first cs)))) + (if + (= c-int 37) + (if + (= (len (rest cs)) 0) + result + (let + ((spec-int (char->integer (first (rest cs))))) + (cond + ((= spec-int 37) + (go (rest (rest cs)) arg-list (str result "%"))) + ((= spec-int 115) + (go + (rest (rest cs)) + (rest arg-list) + (str result (str (first arg-list))))) + ((= spec-int 100) + (go + (rest (rest cs)) + (rest arg-list) + (str result (str (truncate (first arg-list)))))) + ((= spec-int 102) + (go + (rest (rest cs)) + (rest arg-list) + (str result (str (+ 0 (first arg-list)))))) + ((= spec-int 120) + (go + (rest (rest cs)) + (rest arg-list) + (str result (tcl-format-hex (first arg-list))))) + ((= spec-int 111) + (go + (rest (rest cs)) + (rest arg-list) + (str result (tcl-format-oct (first arg-list))))) + (else + (go + (rest (rest cs)) + arg-list + (str + result + "%" + (list->string (list (first (rest cs)))))))))) + (go + (rest cs) + arg-list + (str result (list->string (list (first cs))))))))))) + (go chars args ""))) + +;; --------------------------------------------------------------------------- +;; 5. Coroutine — Tcl-style coroutine using call/cc +;; tcl-co-yield works reliably when called from top-level fns. +;; Avoid calling tcl-co-yield from letrec-bound lambdas (JIT limitation). +;; --------------------------------------------------------------------------- + +(define tcl-current-co nil) + +(define + (tcl-co-new body) + (let + ((co (dict))) + (dict-set! co "_tcl_co" true) + (dict-set! co "_state" "new") + (dict-set! co "_cont" nil) + (dict-set! co "_resumer" nil) + (dict-set! co "_parent" nil) + (dict-set! + co + "_body" + (fn + () + (let + ((result (body))) + (dict-set! co "_state" "dead") + (set! tcl-current-co (get co "_parent")) + ((get co "_resumer") result)))) + co)) + +(define (tcl-co? v) (and (dict? v) (dict-has? v "_tcl_co"))) + +(define (tcl-co-alive? co) (not (= (get co "_state") "dead"))) + +(define + (tcl-co-yield val) + (call/cc + (fn + (resume-k) + (let + ((cur tcl-current-co)) + (dict-set! cur "_cont" resume-k) + (dict-set! cur "_state" "suspended") + (set! tcl-current-co (get cur "_parent")) + ((get cur "_resumer") val))))) + +(define + (tcl-co-resume co) + (call/cc + (fn + (return-k) + (dict-set! co "_parent" tcl-current-co) + (dict-set! co "_resumer" return-k) + (set! tcl-current-co co) + (dict-set! co "_state" "running") + (if + (= (get co "_cont") nil) + ((get co "_body")) + ((get co "_cont") nil))))) diff --git a/lib/tcl/test.sh b/lib/tcl/test.sh new file mode 100755 index 00000000..74c1d16a --- /dev/null +++ b/lib/tcl/test.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# lib/tcl/test.sh — smoke-test the Tcl runtime layer. + +set -uo pipefail +cd "$(git rev-parse --show-toplevel)" + +SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" +if [ ! -x "$SX_SERVER" ]; then + SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" +fi +if [ ! -x "$SX_SERVER" ]; then + echo "ERROR: sx_server.exe not found." + exit 1 +fi + +TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT + +cat > "$TMPFILE" << 'EPOCHS' +(epoch 1) +(load "lib/tcl/runtime.sx") +(epoch 2) +(load "lib/tcl/tests/runtime.sx") +(epoch 3) +(eval "(list tcl-test-pass tcl-test-fail)") +EPOCHS + +OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) + +LINE=$(echo "$OUTPUT" | awk '/^\(ok-len 3 / {getline; print; exit}') +if [ -z "$LINE" ]; then + LINE=$(echo "$OUTPUT" | grep -E '^\(ok 3 \([0-9]+ [0-9]+\)\)' | tail -1 \ + | sed -E 's/^\(ok 3 //; s/\)$//') +fi +if [ -z "$LINE" ]; then + echo "ERROR: could not extract summary" + echo "$OUTPUT" | tail -20 + exit 1 +fi + +P=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\1/') +F=$(echo "$LINE" | sed -E 's/^\(([0-9]+) ([0-9]+)\).*/\2/') +TOTAL=$((P + F)) + +if [ "$F" -eq 0 ]; then + echo "ok $P/$TOTAL lib/tcl tests passed" +else + echo "FAIL $P/$TOTAL passed, $F failed" + TMPFILE2=$(mktemp) + cat > "$TMPFILE2" << 'EPOCHS2' +(epoch 1) +(load "lib/tcl/runtime.sx") +(epoch 2) +(load "lib/tcl/tests/runtime.sx") +(epoch 3) +(eval "(map (fn (f) (list (get f :name) (get f :got) (get f :expected))) tcl-test-fails)") +EPOCHS2 + FAILS=$(timeout 60 "$SX_SERVER" < "$TMPFILE2" 2>/dev/null | grep -E '^\(ok-len 3' -A1 | tail -1 || true) + echo " Details: $FAILS" + rm -f "$TMPFILE2" +fi + +[ "$F" -eq 0 ] diff --git a/lib/tcl/tests/runtime.sx b/lib/tcl/tests/runtime.sx new file mode 100644 index 00000000..ccf81461 --- /dev/null +++ b/lib/tcl/tests/runtime.sx @@ -0,0 +1,146 @@ +;; lib/tcl/tests/runtime.sx — Tests for lib/tcl/runtime.sx + +(define tcl-test-pass 0) +(define tcl-test-fail 0) +(define tcl-test-fails (list)) + +(define + (tcl-test name got expected) + (if + (= got expected) + (set! tcl-test-pass (+ tcl-test-pass 1)) + (begin + (set! tcl-test-fail (+ tcl-test-fail 1)) + (set! tcl-test-fails (append tcl-test-fails (list {:got got :expected expected :name name})))))) + +;; --------------------------------------------------------------------------- +;; 1. String buffer +;; --------------------------------------------------------------------------- + +(define sb1 (tcl-sb-new)) +(tcl-test "sb? new" (tcl-sb? sb1) true) +(tcl-test "sb? non-sb" (tcl-sb? "hello") false) +(tcl-test "sb value empty" (tcl-sb-value sb1) "") +(tcl-test "sb length empty" (tcl-sb-length sb1) 0) +(tcl-sb-append! sb1 "hello") +(tcl-test "sb value after append" (tcl-sb-value sb1) "hello") +(tcl-sb-append! sb1 " ") +(tcl-sb-append! sb1 "world") +(tcl-test "sb value after multi-append" (tcl-sb-value sb1) "hello world") +(tcl-test "sb length" (tcl-sb-length sb1) 11) +(tcl-sb-clear! sb1) +(tcl-test "sb value after clear" (tcl-sb-value sb1) "") +(tcl-test "sb length after clear" (tcl-sb-length sb1) 0) + +;; --------------------------------------------------------------------------- +;; 2. String port (channel) +;; --------------------------------------------------------------------------- + +(define chin1 (tcl-chan-in-new "hello\nworld\nfoo")) +(tcl-test "chan? read" (tcl-chan? chin1) true) +(tcl-test "chan eof? no" (tcl-chan-eof? chin1) false) +(tcl-test "chan gets line1" (tcl-chan-gets chin1) "hello") +(tcl-test "chan gets line2" (tcl-chan-gets chin1) "world") +(tcl-test "chan gets line3" (tcl-chan-gets chin1) "foo") +(tcl-test "chan eof? yes" (tcl-chan-eof? chin1) true) +(tcl-test "chan gets at eof" (tcl-chan-gets chin1) "") + +(define chin2 (tcl-chan-in-new "abcdef")) +(tcl-test "chan read all" (tcl-chan-read chin2) "abcdef") +(tcl-test "chan read empty" (tcl-chan-read chin2) "") + +(define chout1 (tcl-chan-out-new)) +(tcl-test "chan? write" (tcl-chan? chout1) true) +(tcl-chan-puts! chout1 "hello") +(tcl-chan-puts! chout1 " world") +(tcl-test "chan string" (tcl-chan-string chout1) "hello world") +(tcl-chan-puts-line! chout1 "!") +(tcl-test "chan string with newline" (tcl-chan-string chout1) "hello world!\n") + +(define chout2 (tcl-chan-out-new)) +(tcl-chan-puts-line! chout2 "line1") +(tcl-chan-puts-line! chout2 "line2") +(tcl-test "chan multi-line" (tcl-chan-string chout2) "line1\nline2\n") + +;; --------------------------------------------------------------------------- +;; 3. Regexp +;; --------------------------------------------------------------------------- + +(define rx1 (tcl-re-new "hel+o")) +(tcl-test "re? yes" (tcl-re? rx1) true) +(tcl-test "re? no" (tcl-re? "hello") false) +(tcl-test "re match? yes" (tcl-re-match? rx1 "say hello") true) +(tcl-test "re match? no" (tcl-re-match? rx1 "goodbye") false) + +(define m1 (tcl-re-match rx1 "say hello world")) +(tcl-test "re match result" (get m1 "match") "hello") + +(define rx2 (tcl-re-new "[0-9]+")) +(define all (tcl-re-match-all rx2 "a1b22c333")) +(tcl-test "re match-all count" (len all) 3) +(tcl-test "re match-all last" (get (nth all 2) "match") "333") + +(tcl-test "re sub" (tcl-re-sub rx2 "a1b2" "N") "aNb2") +(tcl-test "re sub-all" (tcl-re-sub-all rx2 "a1b2" "N") "aNbN") + +(define rx3 (tcl-re-new "[ ,]+")) +(tcl-test "re split" (tcl-re-split rx3 "a b,c") (list "a" "b" "c")) + +;; --------------------------------------------------------------------------- +;; 4. Format +;; --------------------------------------------------------------------------- + +(tcl-test "format %s" (tcl-format "hello %s" (list "world")) "hello world") +(tcl-test "format %d" (tcl-format "n=%d" (list 42)) "n=42") +(tcl-test "format %d truncates float" (tcl-format "n=%d" (list 3.9)) "n=3") +(tcl-test + "format %s %d" + (tcl-format "%s is %d" (list "age" 30)) + "age is 30") +(tcl-test "format %%" (tcl-format "100%% done" (list)) "100% done") +(tcl-test "format %x" (tcl-format "%x" (list 255)) "ff") +(tcl-test "format %x 16" (tcl-format "0x%x" (list 16)) "0x10") +(tcl-test "format %o" (tcl-format "%o" (list 8)) "10") +(tcl-test "format %o 255" (tcl-format "%o" (list 255)) "377") +(tcl-test "format no spec" (tcl-format "plain text" (list)) "plain text") +(tcl-test + "format multiple" + (tcl-format "%s=%d (0x%x)" (list "val" 255 255)) + "val=255 (0xff)") + +;; --------------------------------------------------------------------------- +;; 5. Coroutine +;; tcl-co-yield works from top-level helper functions. +;; --------------------------------------------------------------------------- + +(define + co1 + (tcl-co-new + (fn () (tcl-co-yield 1) (tcl-co-yield 2) 3))) + +(tcl-test "co? yes" (tcl-co? co1) true) +(tcl-test "co? no" (tcl-co? 42) false) +(tcl-test "co alive? before" (tcl-co-alive? co1) true) +(define cor1 (tcl-co-resume co1)) +(tcl-test "co resume 1" cor1 1) +(tcl-test "co alive? mid" (tcl-co-alive? co1) true) +(define cor2 (tcl-co-resume co1)) +(tcl-test "co resume 2" cor2 2) +(define cor3 (tcl-co-resume co1)) +(tcl-test "co resume 3 completion" cor3 3) +(tcl-test "co alive? dead" (tcl-co-alive? co1) false) + +;; Top-level helper for recursive yield (avoids JIT letrec limitation) +(define + (tcl-co-count-down i) + (when + (>= i 1) + (tcl-co-yield i) + (tcl-co-count-down (- i 1)))) + +(define co2 (tcl-co-new (fn () (tcl-co-count-down 3) "done"))) +(tcl-test "co loop 3" (tcl-co-resume co2) 3) +(tcl-test "co loop 2" (tcl-co-resume co2) 2) +(tcl-test "co loop 1" (tcl-co-resume co2) 1) +(tcl-test "co loop done" (tcl-co-resume co2) "done") +(tcl-test "co loop dead" (tcl-co-alive? co2) false)