6 Commits

Author SHA1 Message Date
c8d7fdd59a tcl: Phase 2 core commands — if/while/for/foreach/switch/break/continue/return/error/expr (+20 tests, 107 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 16s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 14:40:48 +00:00
82da16e4bb tcl: Phase 2 eval engine — tcl-eval-script + set/puts/incr/append (+20 tests, 87 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 17s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 14:02:52 +00:00
35aa998fcc tcl: tick Phase 1 parser checkboxes, update progress log
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 11s
2026-04-25 18:47:45 +00:00
6ee052593c tcl: Phase 1 parser — word-simple? + word-literal helpers (+15 tests, 67 total) 2026-04-25 18:47:34 +00:00
1a17d8d232 tcl: tick Phase 1 tokenizer, add progress log entry
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
2026-04-25 18:22:25 +00:00
666e29d5f0 tcl: Phase 1 tokenizer — Dodekalogue (52 tests green) 2026-04-25 18:22:10 +00:00
7 changed files with 1389 additions and 6 deletions

41
lib/tcl/parser.sx Normal file
View File

@@ -0,0 +1,41 @@
; Tcl parser — thin layer over tcl-tokenize
; Adds tcl-parse entry point and word utility fns
; Entry point: parse Tcl source to a list of commands.
; Returns same structure as tcl-tokenize.
(define tcl-parse (fn (src) (tcl-tokenize src)))
; True if word has no substitutions — value can be read statically.
; braced words are always simple. compound words are simple when all
; parts are plain text with no var/cmd parts.
(define tcl-word-simple?
(fn (word)
(cond
((= (get word :type) "braced") true)
((= (get word :type) "compound")
(let ((parts (get word :parts)))
(every? (fn (p) (= (get p :type) "text")) parts)))
(else false))))
; Concatenate text parts of a simple word into a single string.
; For braced words returns :value directly.
; For compound words with only text parts, joins them.
; Returns nil for words with substitutions.
(define tcl-word-literal
(fn (word)
(cond
((= (get word :type) "braced") (get word :value))
((= (get word :type) "compound")
(if (tcl-word-simple? word)
(join "" (map (fn (p) (get p :value)) (get word :parts)))
nil))
(else nil))))
; Number of words in a parsed command.
(define tcl-cmd-len
(fn (cmd) (len (get cmd :words))))
; Nth word literal from a command (index 0 = command name).
; Returns nil if word has substitutions.
(define tcl-nth-literal
(fn (cmd n) (tcl-word-literal (nth (get cmd :words) n))))

570
lib/tcl/runtime.sx Normal file
View File

@@ -0,0 +1,570 @@
; Tcl-on-SX runtime evaluator
; State: {:frame frame :commands cmd-table :result last-result :output accumulated-output}
(define make-frame (fn (level parent) {:level level :locals {} :parent parent}))
(define
frame-lookup
(fn
(frame name)
(if
(nil? frame)
nil
(let
((val (get (get frame :locals) name)))
(if (nil? val) (frame-lookup (get frame :parent) name) val)))))
(define
frame-set-top
(fn
(frame name val)
(assoc frame :locals (assoc (get frame :locals) name val))))
(define make-tcl-interp (fn () {:result "" :output "" :code 0 :frame (make-frame 0 nil) :commands {}}))
(define
tcl-register
(fn
(interp name f)
(assoc interp :commands (assoc (get interp :commands) name f))))
(define
tcl-var-get
(fn
(interp name)
(let
((val (frame-lookup (get interp :frame) name)))
(if
(nil? val)
(error (str "can't read \"" name "\": no such variable"))
val))))
(define
tcl-var-set
(fn
(interp name val)
(assoc interp :frame (frame-set-top (get interp :frame) name val))))
(define
tcl-eval-parts
(fn
(parts interp)
(reduce
(fn
(acc part)
(let
((type (get part :type)) (cur-interp (get acc :interp)))
(cond
((equal? type "text") {:values (append (get acc :values) (list (get part :value))) :interp cur-interp})
((equal? type "var") {:values (append (get acc :values) (list (tcl-var-get cur-interp (get part :name)))) :interp cur-interp})
((equal? type "var-arr")
(let
((key-acc (tcl-eval-parts (get part :key) cur-interp)))
(let
((key (join "" (get key-acc :values)))
(next-interp (get key-acc :interp)))
{:values (append (get acc :values) (list (tcl-var-get next-interp (str (get part :name) "(" key ")")))) :interp next-interp})))
((equal? type "cmd")
(let
((new-interp (tcl-eval-string cur-interp (get part :src))))
{:values (append (get acc :values) (list (get new-interp :result))) :interp new-interp}))
(else (error (str "tcl: unknown part type: " type))))))
{:values (quote ()) :interp interp}
parts)))
(define
tcl-eval-word
(fn
(word interp)
(let
((type (get word :type)))
(cond
((equal? type "braced") {:interp interp :value (get word :value)})
((equal? type "compound")
(let
((result (tcl-eval-parts (get word :parts) interp)))
{:interp (get result :interp) :value (join "" (get result :values))}))
((equal? type "expand") (tcl-eval-word (get word :word) interp))
(else (error (str "tcl: unknown word type: " type)))))))
(define
tcl-list-split
(fn
(s)
(define chars (split s ""))
(define len-s (len chars))
(define
go
(fn
(i acc cur-item depth)
(if
(>= i len-s)
(if (> (len cur-item) 0) (append acc (list cur-item)) acc)
(let
((c (nth chars i)))
(cond
((equal? c "{")
(if
(= depth 0)
(go (+ i 1) acc "" (+ depth 1))
(go (+ i 1) acc (str cur-item c) (+ depth 1))))
((equal? c "}")
(if
(= depth 1)
(go (+ i 1) (append acc (list cur-item)) "" 0)
(go (+ i 1) acc (str cur-item c) (- depth 1))))
((equal? c " ")
(if
(and (= depth 0) (> (len cur-item) 0))
(go (+ i 1) (append acc (list cur-item)) "" 0)
(go
(+ i 1)
acc
(if (> depth 0) (str cur-item c) cur-item)
depth)))
(else (go (+ i 1) acc (str cur-item c) depth)))))))
(go 0 (list) "" 0)))
(define
tcl-eval-words
(fn
(words interp)
(reduce
(fn
(acc w)
(let
((cur-interp (get acc :interp)))
(if
(equal? (get w :type) "expand")
(let
((wr (tcl-eval-word (get w :word) cur-interp)))
{:values (append (get acc :values) (tcl-list-split (get wr :value))) :interp (get wr :interp)})
(let ((wr (tcl-eval-word w cur-interp))) {:values (append (get acc :values) (list (get wr :value))) :interp (get wr :interp)}))))
{:values (quote ()) :interp interp}
words)))
(define
tcl-eval-cmd
(fn
(interp cmd)
(let
((wr (tcl-eval-words (get cmd :words) interp)))
(let
((words (get wr :values)) (cur-interp (get wr :interp)))
(if
(= 0 (len words))
cur-interp
(let
((cmd-name (first words)) (cmd-args (rest words)))
(let
((cmd-fn (get (get cur-interp :commands) cmd-name)))
(if
(nil? cmd-fn)
(error (str "unknown command: \"" cmd-name "\""))
(cmd-fn cur-interp cmd-args)))))))))
(define
tcl-eval-script
(fn
(interp cmds)
(if
(or (= 0 (len cmds)) (not (= 0 (get interp :code))))
interp
(tcl-eval-script (tcl-eval-cmd interp (first cmds)) (rest cmds)))))
(define
tcl-eval-string
(fn (interp src) (tcl-eval-script interp (tcl-parse src))))
(define
tcl-cmd-set
(fn
(interp args)
(if
(= (len args) 1)
(assoc interp :result (tcl-var-get interp (first args)))
(let
((val (nth args 1)))
(assoc (tcl-var-set interp (first args) val) :result val)))))
(define
tcl-cmd-puts
(fn
(interp args)
(let
((text (last args))
(no-nl
(and
(> (len args) 1)
(equal? (first args) "-nonewline"))))
(let
((line (if no-nl text (str text "\n"))))
(assoc interp :output (str (get interp :output) line))))))
(define
tcl-cmd-incr
(fn
(interp args)
(let
((name (first args))
(delta
(if
(> (len args) 1)
(parse-int (nth args 1))
1)))
(let
((new-val (str (+ (parse-int (tcl-var-get interp name)) delta))))
(assoc (tcl-var-set interp name new-val) :result new-val)))))
(define
tcl-cmd-append
(fn
(interp args)
(let
((name (first args)) (suffix (join "" (rest args))))
(let
((cur (let ((v (frame-lookup (get interp :frame) name))) (if (nil? v) "" v))))
(let
((new-val (str cur suffix)))
(assoc (tcl-var-set interp name new-val) :result new-val))))))
(define
tcl-true?
(fn
(s)
(not
(or (equal? s "0") (equal? s "") (equal? s "false") (equal? s "no")))))
(define tcl-false? (fn (s) (not (tcl-true? s))))
(define
tcl-expr-compute
(fn
(tokens)
(let
((n (len tokens)))
(cond
((= n 1) (first tokens))
((= n 2)
(let
((op (first tokens)) (x (nth tokens 1)))
(if
(equal? op "!")
(if (tcl-false? x) "1" "0")
(error (str "expr: unknown unary op: " op)))))
((= n 3)
(let
((l (first tokens)) (op (nth tokens 1)) (r (nth tokens 2)))
(cond
((equal? op "+") (str (+ (parse-int l) (parse-int r))))
((equal? op "-") (str (- (parse-int l) (parse-int r))))
((equal? op "*") (str (* (parse-int l) (parse-int r))))
((equal? op "/") (str (/ (parse-int l) (parse-int r))))
((equal? op "%") (str (mod (parse-int l) (parse-int r))))
((equal? op "==") (if (equal? l r) "1" "0"))
((equal? op "!=") (if (equal? l r) "0" "1"))
((equal? op "<")
(if (< (parse-int l) (parse-int r)) "1" "0"))
((equal? op ">")
(if (> (parse-int l) (parse-int r)) "1" "0"))
((equal? op "<=")
(if (<= (parse-int l) (parse-int r)) "1" "0"))
((equal? op ">=")
(if (>= (parse-int l) (parse-int r)) "1" "0"))
((equal? op "&&")
(if (and (tcl-true? l) (tcl-true? r)) "1" "0"))
((equal? op "||")
(if (or (tcl-true? l) (tcl-true? r)) "1" "0"))
(else (error (str "expr: unknown op: " op))))))
(else (error (str "expr: complex expr not yet supported")))))))
(define
tcl-expr-eval
(fn
(interp s)
(let
((cmds (tcl-parse s)))
(if
(= 0 (len cmds))
{:result "0" :interp interp}
(let
((wr (tcl-eval-words (get (first cmds) :words) interp)))
{:result (tcl-expr-compute (get wr :values)) :interp (get wr :interp)})))))
(define tcl-cmd-break (fn (interp args) (assoc interp :code 3)))
(define tcl-cmd-continue (fn (interp args) (assoc interp :code 4)))
(define
tcl-cmd-return
(fn
(interp args)
(let
((val (if (> (len args) 0) (last args) "")))
(assoc (assoc interp :result val) :code 2))))
(define
tcl-cmd-error
(fn
(interp args)
(let
((msg (if (> (len args) 0) (first args) "error")))
(assoc (assoc interp :result msg) :code 1))))
(define
tcl-cmd-unset
(fn
(interp args)
(reduce
(fn
(i name)
(let
((frame (get i :frame)))
(let
((new-locals (reduce (fn (acc k) (if (equal? k name) acc (assoc acc k (get (get frame :locals) k)))) {} (keys (get frame :locals)))))
(assoc i :frame (assoc frame :locals new-locals)))))
interp
args)))
(define
tcl-cmd-lappend
(fn
(interp args)
(let
((name (first args)) (items (rest args)))
(let
((cur (let ((v (frame-lookup (get interp :frame) name))) (if (nil? v) "" v))))
(let
((new-val (if (equal? cur "") (join " " items) (str cur " " (join " " items)))))
(assoc (tcl-var-set interp name new-val) :result new-val))))))
(define
tcl-cmd-eval
(fn (interp args) (tcl-eval-string interp (join " " args))))
(define
tcl-while-loop
(fn
(interp cond-str body)
(let
((er (tcl-expr-eval interp cond-str)))
(if
(tcl-false? (get er :result))
(get er :interp)
(let
((body-result (tcl-eval-string (get er :interp) body)))
(let
((code (get body-result :code)))
(cond
((= code 3) (assoc body-result :code 0))
((= code 2) body-result)
((= code 1) body-result)
(else
(tcl-while-loop
(assoc body-result :code 0)
cond-str
body)))))))))
(define
tcl-cmd-while
(fn
(interp args)
(tcl-while-loop interp (first args) (nth args 1))))
(define
tcl-cmd-if
(fn
(interp args)
(let
((er (tcl-expr-eval interp (first args))))
(let
((cond-true (tcl-true? (get er :result)))
(new-interp (get er :interp))
(rest-args (rest args)))
(let
((adj (if (and (> (len rest-args) 0) (equal? (first rest-args) "then")) (rest rest-args) rest-args)))
(let
((then-body (first adj)) (rest2 (rest adj)))
(if
cond-true
(tcl-eval-string new-interp then-body)
(cond
((= 0 (len rest2)) new-interp)
((equal? (first rest2) "else")
(if
(> (len rest2) 1)
(tcl-eval-string new-interp (nth rest2 1))
new-interp))
((equal? (first rest2) "elseif")
(tcl-cmd-if new-interp (rest rest2)))
(else new-interp)))))))))
(define
tcl-for-loop
(fn
(interp cond-str step body)
(let
((er (tcl-expr-eval interp cond-str)))
(if
(tcl-false? (get er :result))
(get er :interp)
(let
((body-result (tcl-eval-string (get er :interp) body)))
(let
((code (get body-result :code)))
(cond
((= code 3) (assoc body-result :code 0))
((= code 2) body-result)
((= code 1) body-result)
(else
(let
((step-result (tcl-eval-string (assoc body-result :code 0) step)))
(tcl-for-loop
(assoc step-result :code 0)
cond-str
step
body))))))))))
(define
tcl-cmd-for
(fn
(interp args)
(let
((init-body (first args))
(cond-str (nth args 1))
(step (nth args 2))
(body (nth args 3)))
(let
((init-result (tcl-eval-string interp init-body)))
(tcl-for-loop init-result cond-str step body)))))
(define
tcl-foreach-loop
(fn
(interp var-name items body)
(if
(= 0 (len items))
interp
(let
((body-result (tcl-eval-string (tcl-var-set interp var-name (first items)) body)))
(let
((code (get body-result :code)))
(cond
((= code 3) (assoc body-result :code 0))
((= code 2) body-result)
((= code 1) body-result)
(else
(tcl-foreach-loop
(assoc body-result :code 0)
var-name
(rest items)
body))))))))
(define
tcl-cmd-foreach
(fn
(interp args)
(let
((var-name (first args))
(list-str (nth args 1))
(body (nth args 2)))
(tcl-foreach-loop interp var-name (tcl-list-split list-str) body))))
(define
tcl-cmd-switch
(fn
(interp args)
(let
((str-val (first args)) (body (nth args 1)))
(let
((pairs (tcl-list-split body)))
(define
try-pairs
(fn
(ps)
(if
(= 0 (len ps))
interp
(let
((pat (first ps)) (bdy (nth ps 1)))
(if
(or (equal? pat str-val) (equal? pat "default"))
(if
(equal? bdy "-")
(try-pairs (rest (rest ps)))
(tcl-eval-string interp bdy))
(try-pairs (rest (rest ps))))))))
(try-pairs pairs)))))
(define
tcl-cmd-expr
(fn
(interp args)
(let
((s (join " " args)))
(let
((er (tcl-expr-eval interp s)))
(assoc (get er :interp) :result (get er :result))))))
(define tcl-cmd-gets (fn (interp args) (assoc interp :result "")))
(define
tcl-cmd-subst
(fn (interp args) (assoc interp :result (last args))))
(define
tcl-cmd-format
(fn (interp args) (assoc interp :result (join "" args))))
(define tcl-cmd-scan (fn (interp args) (assoc interp :result "0")))
(define
make-default-tcl-interp
(fn
()
(let
((i (make-tcl-interp)))
(let
((i (tcl-register i "set" tcl-cmd-set)))
(let
((i (tcl-register i "puts" tcl-cmd-puts)))
(let
((i (tcl-register i "incr" tcl-cmd-incr)))
(let
((i (tcl-register i "append" tcl-cmd-append)))
(let
((i (tcl-register i "unset" tcl-cmd-unset)))
(let
((i (tcl-register i "lappend" tcl-cmd-lappend)))
(let
((i (tcl-register i "eval" tcl-cmd-eval)))
(let
((i (tcl-register i "if" tcl-cmd-if)))
(let
((i (tcl-register i "while" tcl-cmd-while)))
(let
((i (tcl-register i "for" tcl-cmd-for)))
(let
((i (tcl-register i "foreach" tcl-cmd-foreach)))
(let
((i (tcl-register i "switch" tcl-cmd-switch)))
(let
((i (tcl-register i "break" tcl-cmd-break)))
(let
((i (tcl-register i "continue" tcl-cmd-continue)))
(let
((i (tcl-register i "return" tcl-cmd-return)))
(let
((i (tcl-register i "error" tcl-cmd-error)))
(let
((i (tcl-register i "expr" tcl-cmd-expr)))
(let
((i (tcl-register i "gets" tcl-cmd-gets)))
(let
((i (tcl-register i "subst" tcl-cmd-subst)))
(let
((i (tcl-register i "format" tcl-cmd-format)))
(tcl-register
i
"scan"
tcl-cmd-scan))))))))))))))))))))))))

81
lib/tcl/test.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Tcl-on-SX test runner — epoch protocol to sx_server.exe
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:-}"
TMPFILE=$(mktemp)
HELPER=$(mktemp --suffix=.sx)
trap "rm -f $TMPFILE $HELPER" EXIT
# Helper file: run both test suites and format a parseable summary string
cat > "$HELPER" << 'HELPER_EOF'
(define __pr (tcl-run-parse-tests))
(define __er (tcl-run-eval-tests))
(define tcl-test-summary
(str "PARSE:" (get __pr "passed") ":" (get __pr "failed")
" EVAL:" (get __er "passed") ":" (get __er "failed")))
HELPER_EOF
cat > "$TMPFILE" << EPOCHS
(epoch 1)
(load "lib/tcl/tokenizer.sx")
(epoch 2)
(load "lib/tcl/parser.sx")
(epoch 3)
(load "lib/tcl/tests/parse.sx")
(epoch 4)
(load "lib/tcl/runtime.sx")
(epoch 5)
(load "lib/tcl/tests/eval.sx")
(epoch 6)
(load "$HELPER")
(epoch 7)
(eval "tcl-test-summary")
EPOCHS
OUTPUT=$(timeout 30 "$SX_SERVER" < "$TMPFILE" 2>&1)
[ "$VERBOSE" = "-v" ] && echo "$OUTPUT"
# Extract summary line from epoch 7 output
SUMMARY=$(echo "$OUTPUT" | grep -A1 "^(ok-len 7 " | tail -1 | tr -d '"')
if [ -z "$SUMMARY" ]; then
echo "ERROR: no summary from test run"
echo "$OUTPUT" | tail -20
exit 1
fi
# Parse PARSE:N:M EVAL:N:M
PARSE_PART=$(echo "$SUMMARY" | grep -o 'PARSE:[0-9]*:[0-9]*')
EVAL_PART=$(echo "$SUMMARY" | grep -o 'EVAL:[0-9]*:[0-9]*')
PARSE_PASSED=$(echo "$PARSE_PART" | cut -d: -f2)
PARSE_FAILED=$(echo "$PARSE_PART" | cut -d: -f3)
EVAL_PASSED=$(echo "$EVAL_PART" | cut -d: -f2)
EVAL_FAILED=$(echo "$EVAL_PART" | cut -d: -f3)
PARSE_PASSED=${PARSE_PASSED:-0}; PARSE_FAILED=${PARSE_FAILED:-1}
EVAL_PASSED=${EVAL_PASSED:-0}; EVAL_FAILED=${EVAL_FAILED:-1}
TOTAL_PASSED=$((PARSE_PASSED + EVAL_PASSED))
TOTAL_FAILED=$((PARSE_FAILED + EVAL_FAILED))
TOTAL=$((TOTAL_PASSED + TOTAL_FAILED))
if [ "$TOTAL_FAILED" = "0" ]; then
echo "ok $TOTAL_PASSED/$TOTAL tcl tests passed (parse: $PARSE_PASSED, eval: $EVAL_PASSED)"
exit 0
else
echo "FAIL $TOTAL_PASSED/$TOTAL passed, $TOTAL_FAILED failed (parse: $PARSE_PASSED/$((PARSE_PASSED+PARSE_FAILED)), eval: $EVAL_PASSED/$((EVAL_PASSED+EVAL_FAILED)))"
if [ -z "$VERBOSE" ]; then
echo "--- output ---"
echo "$OUTPUT" | tail -20
fi
exit 1
fi

194
lib/tcl/tests/eval.sx Normal file
View File

@@ -0,0 +1,194 @@
; Tcl-on-SX eval tests
(define tcl-eval-pass 0)
(define tcl-eval-fail 0)
(define tcl-eval-failures (list))
(define
tcl-eval-assert
(fn
(label expected actual)
(if
(equal? expected actual)
(set! tcl-eval-pass (+ tcl-eval-pass 1))
(begin
(set! tcl-eval-fail (+ tcl-eval-fail 1))
(append!
tcl-eval-failures
(str label ": expected=" (str expected) " got=" (str actual)))))))
(define
tcl-run-eval-tests
(fn
()
(set! tcl-eval-pass 0)
(set! tcl-eval-fail 0)
(set! tcl-eval-failures (list))
(define interp (fn () (make-default-tcl-interp)))
(define run (fn (src) (tcl-eval-string (interp) src)))
(define
ok
(fn (label actual expected) (tcl-eval-assert label expected actual)))
(define
ok?
(fn (label condition) (tcl-eval-assert label true condition)))
(tcl-eval-assert "set-result" "hello" (get (run "set x hello") :result))
(tcl-eval-assert
"set-stored"
"hello"
(tcl-var-get (run "set x hello") "x"))
(tcl-eval-assert
"var-sub"
"hello"
(tcl-var-get (run "set x hello\nset y $x") "y"))
(tcl-eval-assert
"puts"
"world\n"
(get (run "set x world\nputs $x") :output))
(tcl-eval-assert
"puts-nonewline"
"hi"
(get (run "puts -nonewline hi") :output))
(tcl-eval-assert "incr" "6" (tcl-var-get (run "set x 5\nincr x") "x"))
(tcl-eval-assert
"incr-delta"
"8"
(tcl-var-get (run "set x 5\nincr x 3") "x"))
(tcl-eval-assert
"incr-neg"
"7"
(tcl-var-get (run "set x 10\nincr x -3") "x"))
(tcl-eval-assert
"append"
"foobar"
(tcl-var-get (run "set x foo\nappend x bar") "x"))
(tcl-eval-assert
"append-new"
"hello"
(tcl-var-get (run "append x hello") "x"))
(tcl-eval-assert
"cmdsub-result"
"6"
(get (run "set x 5\nset y [incr x]") :result))
(tcl-eval-assert
"cmdsub-y"
"6"
(tcl-var-get (run "set x 5\nset y [incr x]") "y"))
(tcl-eval-assert
"cmdsub-x"
"6"
(tcl-var-get (run "set x 5\nset y [incr x]") "x"))
(tcl-eval-assert
"multi-cmd"
"second"
(get (run "set x first\nset x second") :result))
(tcl-eval-assert "semi-x" "1" (tcl-var-get (run "set x 1; set y 2") "x"))
(tcl-eval-assert "semi-y" "2" (tcl-var-get (run "set x 1; set y 2") "y"))
(tcl-eval-assert
"braced-nosub"
"$x"
(tcl-var-get (run "set x 42\nset y {$x}") "y"))
(tcl-eval-assert
"concat-word"
"foobar"
(tcl-var-get (run "set x foo\nset y ${x}bar") "y"))
(tcl-eval-assert
"set-get"
"world"
(get (run "set x world\nset x") :result))
(tcl-eval-assert
"puts-channel"
"hello\n"
(get (run "puts stdout hello") :output))
(ok "if-true" (get (run "set x 0\nif {1} {set x 1}") :result) "1")
(ok "if-false" (get (run "set x 0\nif {0} {set x 1}") :result) "0")
(ok
"if-else-t"
(tcl-var-get (run "if {1} {set x yes} else {set x no}") "x")
"yes")
(ok
"if-else-f"
(tcl-var-get (run "if {0} {set x yes} else {set x no}") "x")
"no")
(ok
"if-cmp"
(tcl-var-get
(run "set x 5\nif {$x > 3} {set r big} else {set r small}")
"r")
"big")
(ok
"while"
(tcl-var-get
(run "set i 0\nset s 0\nwhile {$i < 5} {incr i\nincr s $i}")
"s")
"15")
(ok
"while-break"
(tcl-var-get
(run "set i 0\nwhile {1} {incr i\nif {$i == 3} {break}}")
"i")
"3")
(ok
"for"
(tcl-var-get
(run "set s 0\nfor {set i 1} {$i <= 5} {incr i} {incr s $i}")
"s")
"15")
(ok
"foreach"
(tcl-var-get (run "set s 0\nforeach x {1 2 3 4 5} {incr s $x}") "s")
"15")
(ok
"foreach-list"
(get (run "set acc \"\"\nforeach w {hello world} {append acc $w}") :result)
"helloworld")
(ok
"lappend"
(tcl-var-get (run "lappend lst a\nlappend lst b\nlappend lst c") "lst")
"a b c")
(ok?
"unset-gone"
(let
((i (run "set x 42\nunset x")))
(let
((frame (get i :frame)))
(nil? (get (get frame :locals) "x")))))
(ok "eval" (tcl-var-get (run "eval {set x hello}") "x") "hello")
(ok "expr-add" (get (run "expr {3 + 4}") :result) "7")
(ok "expr-cmp" (get (run "expr {5 > 3}") :result) "1")
(ok
"break-stops"
(tcl-var-get (run "set x 0\nwhile {1} {set x 1\nbreak\nset x 99}") "x")
"1")
(ok
"continue"
(tcl-var-get
(run
"set s 0\nfor {set i 1} {$i <= 5} {incr i} {if {$i == 3} {continue}\nincr s $i}")
"s")
"12")
(ok
"switch"
(tcl-var-get
(run "set x foo\nswitch $x {{foo} {set r yes} {bar} {set r no}}")
"r")
"yes")
(ok
"switch-default"
(tcl-var-get
(run "set x baz\nswitch $x {{foo} {set r yes} default {set r other}}")
"r")
"other")
(ok
"nested-if"
(tcl-var-get
(run
"set x 5\nif {$x > 10} {set r big} elseif {$x > 3} {set r mid} else {set r small}")
"r")
"mid")
(dict
"passed"
tcl-eval-pass
"failed"
tcl-eval-fail
"failures"
tcl-eval-failures)))

186
lib/tcl/tests/parse.sx Normal file
View File

@@ -0,0 +1,186 @@
(define tcl-parse-pass 0)
(define tcl-parse-fail 0)
(define tcl-parse-failures (list))
(define tcl-assert
(fn (label expected actual)
(if (= expected actual)
(set! tcl-parse-pass (+ tcl-parse-pass 1))
(begin
(set! tcl-parse-fail (+ tcl-parse-fail 1))
(append! tcl-parse-failures
(str label ": expected=" (str expected) " got=" (str actual)))))))
(define tcl-first-cmd
(fn (src) (nth (tcl-tokenize src) 0)))
(define tcl-cmd-words
(fn (src) (get (tcl-first-cmd src) :words)))
(define tcl-word
(fn (src wi) (nth (tcl-cmd-words src) wi)))
(define tcl-parts
(fn (src wi) (get (tcl-word src wi) :parts)))
(define tcl-part
(fn (src wi pi) (nth (tcl-parts src wi) pi)))
(define tcl-run-parse-tests
(fn ()
(set! tcl-parse-pass 0)
(set! tcl-parse-fail 0)
(set! tcl-parse-failures (list))
; empty / whitespace-only
(tcl-assert "empty" 0 (len (tcl-tokenize "")))
(tcl-assert "ws-only" 0 (len (tcl-tokenize " ")))
(tcl-assert "nl-only" 0 (len (tcl-tokenize "\n\n")))
; single command word count
(tcl-assert "1word" 1 (len (tcl-cmd-words "set")))
(tcl-assert "3words" 3 (len (tcl-cmd-words "set x 1")))
(tcl-assert "4words" 4 (len (tcl-cmd-words "set a b c")))
; word type — bare word is compound
(tcl-assert "bare-type" "compound" (get (tcl-word "set x 1" 0) :type))
(tcl-assert "bare-quoted" false (get (tcl-word "set x 1" 0) :quoted))
(tcl-assert "bare-part-type" "text" (get (tcl-part "set x 1" 0 0) :type))
(tcl-assert "bare-part-val" "set" (get (tcl-part "set x 1" 0 0) :value))
(tcl-assert "bare-part2-val" "x" (get (tcl-part "set x 1" 1 0) :value))
(tcl-assert "bare-part3-val" "1" (get (tcl-part "set x 1" 2 0) :value))
; multiple commands
(tcl-assert "semi-sep" 2 (len (tcl-tokenize "set x 1; set y 2")))
(tcl-assert "nl-sep" 2 (len (tcl-tokenize "set x 1\nset y 2")))
(tcl-assert "multi-nl" 3 (len (tcl-tokenize "a\nb\nc")))
; comments
(tcl-assert "comment-only" 0 (len (tcl-tokenize "# comment")))
(tcl-assert "comment-nl" 0 (len (tcl-tokenize "# comment\n")))
(tcl-assert "comment-then-cmd" 1 (len (tcl-tokenize "# comment\nset x 1")))
(tcl-assert "semi-then-comment" 1 (len (tcl-tokenize "set x 1; # comment")))
; brace-quoted words
(tcl-assert "brace-type" "braced" (get (tcl-word "{hello}" 0) :type))
(tcl-assert "brace-value" "hello" (get (tcl-word "{hello}" 0) :value))
(tcl-assert "brace-spaces" "hello world" (get (tcl-word "{hello world}" 0) :value))
(tcl-assert "brace-nested" "a {b} c" (get (tcl-word "{a {b} c}" 0) :value))
(tcl-assert "brace-no-var-sub" "hello $x" (get (tcl-word "{hello $x}" 0) :value))
(tcl-assert "brace-no-cmd-sub" "[expr 1]" (get (tcl-word "{[expr 1]}" 0) :value))
; double-quoted words
(tcl-assert "dq-type" "compound" (get (tcl-word "\"hello\"" 0) :type))
(tcl-assert "dq-quoted" true (get (tcl-word "\"hello\"" 0) :quoted))
(tcl-assert "dq-literal" "hello" (get (tcl-part "\"hello\"" 0 0) :value))
; variable substitution in bare word
(tcl-assert "var-type" "var" (get (tcl-part "$x" 0 0) :type))
(tcl-assert "var-name" "x" (get (tcl-part "$x" 0 0) :name))
(tcl-assert "var-long" "long_name" (get (tcl-part "$long_name" 0 0) :name))
; ${name} form
(tcl-assert "var-brace-type" "var" (get (tcl-part "${x}" 0 0) :type))
(tcl-assert "var-brace-name" "x" (get (tcl-part "${x}" 0 0) :name))
; array variable substitution
(tcl-assert "arr-type" "var-arr" (get (tcl-part "$arr(key)" 0 0) :type))
(tcl-assert "arr-name" "arr" (get (tcl-part "$arr(key)" 0 0) :name))
(tcl-assert "arr-key-len" 1 (len (get (tcl-part "$arr(key)" 0 0) :key)))
(tcl-assert "arr-key-text" "key"
(get (nth (get (tcl-part "$arr(key)" 0 0) :key) 0) :value))
; command substitution
(tcl-assert "cmd-type" "cmd" (get (tcl-part "[expr 1+1]" 0 0) :type))
(tcl-assert "cmd-src" "expr 1+1" (get (tcl-part "[expr 1+1]" 0 0) :src))
; nested command substitution
(tcl-assert "cmd-nested-src" "expr [string length x]"
(get (tcl-part "[expr [string length x]]" 0 0) :src))
; backslash substitution in double-quoted word
(let ((ps (tcl-parts "\"a\\nb\"" 0)))
(begin
(tcl-assert "bs-n-part0" "a" (get (nth ps 0) :value))
(tcl-assert "bs-n-part1" "\n" (get (nth ps 1) :value))
(tcl-assert "bs-n-part2" "b" (get (nth ps 2) :value))))
(let ((ps (tcl-parts "\"a\\tb\"" 0)))
(tcl-assert "bs-t-part1" "\t" (get (nth ps 1) :value)))
(let ((ps (tcl-parts "\"a\\\\b\"" 0)))
(tcl-assert "bs-bs-part1" "\\" (get (nth ps 1) :value)))
; mixed word: text + var + text in double-quoted
(let ((ps (tcl-parts "\"hello $name!\"" 0)))
(begin
(tcl-assert "mixed-text0" "hello " (get (nth ps 0) :value))
(tcl-assert "mixed-var1-type" "var" (get (nth ps 1) :type))
(tcl-assert "mixed-var1-name" "name" (get (nth ps 1) :name))
(tcl-assert "mixed-text2" "!" (get (nth ps 2) :value))))
; {*} expansion
(tcl-assert "expand-type" "expand" (get (tcl-word "{*}$list" 0) :type))
; line continuation between words
(tcl-assert "cont-words" 3 (len (tcl-cmd-words "set x \\\n 1")))
; continuation — third command word is correct
(tcl-assert "cont-word2-val" "1"
(get (tcl-part "set x \\\n 1" 2 0) :value))
; --- parser helpers ---
; tcl-parse is an alias for tcl-tokenize
(tcl-assert "parse-cmd-count" 1 (len (tcl-parse "set x 1")))
(tcl-assert "parse-2cmds" 2 (len (tcl-parse "set x 1; set y 2")))
; tcl-cmd-len
(tcl-assert "cmd-len-3" 3 (tcl-cmd-len (nth (tcl-parse "set x 1") 0)))
(tcl-assert "cmd-len-1" 1 (tcl-cmd-len (nth (tcl-parse "puts") 0)))
; tcl-word-simple? on braced word
(tcl-assert "simple-braced" true
(tcl-word-simple? (nth (get (nth (tcl-parse "{hello}") 0) :words) 0)))
; tcl-word-simple? on bare word with no subs
(tcl-assert "simple-bare" true
(tcl-word-simple? (nth (get (nth (tcl-parse "hello") 0) :words) 0)))
; tcl-word-simple? on word containing a var sub — false
(tcl-assert "simple-var-false" false
(tcl-word-simple? (nth (get (nth (tcl-parse "$x") 0) :words) 0)))
; tcl-word-simple? on word containing a cmd sub — false
(tcl-assert "simple-cmd-false" false
(tcl-word-simple? (nth (get (nth (tcl-parse "[expr 1]") 0) :words) 0)))
; tcl-word-literal on braced word
(tcl-assert "lit-braced" "hello world"
(tcl-word-literal (nth (get (nth (tcl-parse "{hello world}") 0) :words) 0)))
; tcl-word-literal on bare word
(tcl-assert "lit-bare" "hello"
(tcl-word-literal (nth (get (nth (tcl-parse "hello") 0) :words) 0)))
; tcl-word-literal on word with var sub returns nil
(tcl-assert "lit-var-nil" nil
(tcl-word-literal (nth (get (nth (tcl-parse "$x") 0) :words) 0)))
; tcl-nth-literal
(tcl-assert "nth-lit-0" "set"
(tcl-nth-literal (nth (tcl-parse "set x 1") 0) 0))
(tcl-assert "nth-lit-1" "x"
(tcl-nth-literal (nth (tcl-parse "set x 1") 0) 1))
(tcl-assert "nth-lit-2" "1"
(tcl-nth-literal (nth (tcl-parse "set x 1") 0) 2))
; tcl-nth-literal returns nil when word has subs
(tcl-assert "nth-lit-nil" nil
(tcl-nth-literal (nth (tcl-parse "set x $y") 0) 2))
(dict
"passed" tcl-parse-pass
"failed" tcl-parse-fail
"failures" tcl-parse-failures)))

308
lib/tcl/tokenizer.sx Normal file
View File

@@ -0,0 +1,308 @@
(define tcl-ws? (fn (c) (or (= c " ") (= c "\t") (= c "\r"))))
(define tcl-alpha?
(fn (c)
(and
(not (= c nil))
(or (and (>= c "a") (<= c "z")) (and (>= c "A") (<= c "Z"))))))
(define tcl-digit?
(fn (c) (and (not (= c nil)) (>= c "0") (<= c "9"))))
(define tcl-ident-start?
(fn (c) (or (tcl-alpha? c) (= c "_"))))
(define tcl-ident-char?
(fn (c) (or (tcl-ident-start? c) (tcl-digit? c))))
(define tcl-tokenize
(fn (src)
(let ((pos 0) (src-len (len src)) (commands (list)))
(define char-at
(fn (off)
(if (< (+ pos off) src-len) (nth src (+ pos off)) nil)))
(define cur (fn () (char-at 0)))
(define advance! (fn (n) (set! pos (+ pos n))))
(define skip-ws!
(fn ()
(when (tcl-ws? (cur))
(begin (advance! 1) (skip-ws!)))))
(define skip-to-eol!
(fn ()
(when (and (< pos src-len) (not (= (cur) "\n")))
(begin (advance! 1) (skip-to-eol!)))))
(define skip-brace-content!
(fn (d)
(when (and (< pos src-len) (> d 0))
(cond
((= (cur) "{") (begin (advance! 1) (skip-brace-content! (+ d 1))))
((= (cur) "}") (begin (advance! 1) (skip-brace-content! (- d 1))))
(else (begin (advance! 1) (skip-brace-content! d)))))))
(define skip-dquote-content!
(fn ()
(when (and (< pos src-len) (not (= (cur) "\"")))
(begin
(when (= (cur) "\\") (advance! 1))
(when (< pos src-len) (advance! 1))
(skip-dquote-content!)))))
(define parse-bs
(fn ()
(advance! 1)
(let ((c (cur)))
(cond
((= c nil) "\\")
((= c "n") (begin (advance! 1) "\n"))
((= c "t") (begin (advance! 1) "\t"))
((= c "r") (begin (advance! 1) "\r"))
((= c "\\") (begin (advance! 1) "\\"))
((= c "[") (begin (advance! 1) "["))
((= c "]") (begin (advance! 1) "]"))
((= c "{") (begin (advance! 1) "{"))
((= c "}") (begin (advance! 1) "}"))
((= c "$") (begin (advance! 1) "$"))
((= c ";") (begin (advance! 1) ";"))
((= c "\"") (begin (advance! 1) "\""))
((= c "'") (begin (advance! 1) "'"))
((= c " ") (begin (advance! 1) " "))
((= c "\n")
(begin
(advance! 1)
(skip-ws!)
" "))
(else (begin (advance! 1) (str "\\" c)))))))
(define parse-cmd-sub
(fn ()
(advance! 1)
(let ((start pos) (depth 1))
(define scan!
(fn ()
(when (and (< pos src-len) (> depth 0))
(cond
((= (cur) "[")
(begin (set! depth (+ depth 1)) (advance! 1) (scan!)))
((= (cur) "]")
(begin
(set! depth (- depth 1))
(when (> depth 0) (advance! 1))
(scan!)))
((= (cur) "{")
(begin (advance! 1) (skip-brace-content! 1) (scan!)))
((= (cur) "\"")
(begin
(advance! 1)
(skip-dquote-content!)
(when (= (cur) "\"") (advance! 1))
(scan!)))
((= (cur) "\\")
(begin (advance! 1) (when (< pos src-len) (advance! 1)) (scan!)))
(else (begin (advance! 1) (scan!)))))))
(scan!)
(let ((src-text (slice src start pos)))
(begin
(when (= (cur) "]") (advance! 1))
{:type "cmd" :src src-text})))))
(define scan-name!
(fn ()
(when (and (< pos src-len) (not (= (cur) "}")))
(begin (advance! 1) (scan-name!)))))
(define scan-ns-name!
(fn ()
(cond
((tcl-ident-char? (cur))
(begin (advance! 1) (scan-ns-name!)))
((and (= (cur) ":") (= (char-at 1) ":"))
(begin (advance! 2) (scan-ns-name!)))
(else nil))))
(define scan-klit!
(fn ()
(when (and (< pos src-len)
(not (= (cur) ")"))
(not (= (cur) "$"))
(not (= (cur) "["))
(not (= (cur) "\\")))
(begin (advance! 1) (scan-klit!)))))
(define scan-key!
(fn (kp)
(when (and (< pos src-len) (not (= (cur) ")")))
(cond
((= (cur) "$")
(begin (append! kp (parse-var-sub)) (scan-key! kp)))
((= (cur) "[")
(begin (append! kp (parse-cmd-sub)) (scan-key! kp)))
((= (cur) "\\")
(begin
(append! kp {:type "text" :value (parse-bs)})
(scan-key! kp)))
(else
(let ((kstart pos))
(begin
(scan-klit!)
(append! kp {:type "text" :value (slice src kstart pos)})
(scan-key! kp))))))))
(define parse-var-sub
(fn ()
(advance! 1)
(cond
((= (cur) "{")
(begin
(advance! 1)
(let ((start pos))
(begin
(scan-name!)
(let ((name (slice src start pos)))
(begin
(when (= (cur) "}") (advance! 1))
{:type "var" :name name}))))))
((tcl-ident-start? (cur))
(let ((start pos))
(begin
(scan-ns-name!)
(let ((name (slice src start pos)))
(if (= (cur) "(")
(begin
(advance! 1)
(let ((key-parts (list)))
(begin
(scan-key! key-parts)
(when (= (cur) ")") (advance! 1))
{:type "var-arr" :name name :key key-parts})))
{:type "var" :name name})))))
(else {:type "text" :value "$"}))))
(define scan-lit!
(fn (stop?)
(when (and (< pos src-len)
(not (stop? (cur)))
(not (= (cur) "$"))
(not (= (cur) "["))
(not (= (cur) "\\")))
(begin (advance! 1) (scan-lit! stop?)))))
(define parse-word-parts!
(fn (parts stop?)
(when (and (< pos src-len) (not (stop? (cur))))
(cond
((= (cur) "$")
(begin (append! parts (parse-var-sub)) (parse-word-parts! parts stop?)))
((= (cur) "[")
(begin (append! parts (parse-cmd-sub)) (parse-word-parts! parts stop?)))
((= (cur) "\\")
(begin
(append! parts {:type "text" :value (parse-bs)})
(parse-word-parts! parts stop?)))
(else
(let ((start pos))
(begin
(scan-lit! stop?)
(when (> pos start)
(append! parts {:type "text" :value (slice src start pos)}))
(parse-word-parts! parts stop?))))))))
(define parse-brace-word
(fn ()
(advance! 1)
(let ((depth 1) (start pos))
(define scan!
(fn ()
(when (and (< pos src-len) (> depth 0))
(cond
((= (cur) "{")
(begin (set! depth (+ depth 1)) (advance! 1) (scan!)))
((= (cur) "}")
(begin (set! depth (- depth 1)) (when (> depth 0) (advance! 1)) (scan!)))
(else (begin (advance! 1) (scan!)))))))
(scan!)
(let ((value (slice src start pos)))
(begin
(when (= (cur) "}") (advance! 1))
{:type "braced" :value value})))))
(define parse-dquote-word
(fn ()
(advance! 1)
(let ((parts (list)))
(begin
(parse-word-parts! parts (fn (c) (or (= c "\"") (= c nil))))
(when (= (cur) "\"") (advance! 1))
{:type "compound" :parts parts :quoted true}))))
(define parse-bare-word
(fn ()
(let ((parts (list)))
(begin
(parse-word-parts!
parts
(fn (c) (or (tcl-ws? c) (= c "\n") (= c ";") (= c nil))))
{:type "compound" :parts parts :quoted false}))))
(define parse-word-no-expand
(fn ()
(cond
((= (cur) "{") (parse-brace-word))
((= (cur) "\"") (parse-dquote-word))
(else (parse-bare-word)))))
(define parse-word
(fn ()
(cond
((and (= (cur) "{") (= (char-at 1) "*") (= (char-at 2) "}"))
(begin
(advance! 3)
{:type "expand" :word (parse-word-no-expand)}))
((= (cur) "{") (parse-brace-word))
((= (cur) "\"") (parse-dquote-word))
(else (parse-bare-word)))))
(define parse-words!
(fn (words)
(skip-ws!)
(cond
((or (= (cur) nil) (= (cur) "\n") (= (cur) ";")) nil)
((and (= (cur) "\\") (= (char-at 1) "\n"))
(begin (advance! 2) (skip-ws!) (parse-words! words)))
(else
(begin
(append! words (parse-word))
(parse-words! words))))))
(define skip-seps!
(fn ()
(when (< pos src-len)
(cond
((or (tcl-ws? (cur)) (= (cur) "\n") (= (cur) ";"))
(begin (advance! 1) (skip-seps!)))
((and (= (cur) "\\") (= (char-at 1) "\n"))
(begin (advance! 2) (skip-seps!)))
(else nil)))))
(define parse-all!
(fn ()
(skip-seps!)
(when (< pos src-len)
(cond
((= (cur) "#")
(begin (skip-to-eol!) (parse-all!)))
(else
(let ((words (list)))
(begin
(parse-words! words)
(when (> (len words) 0)
(append! commands {:type "command" :words words}))
(parse-all!))))))))
(parse-all!)
commands)))

View File

@@ -50,7 +50,7 @@ Core mapping:
## Roadmap ## Roadmap
### Phase 1 — tokenizer + parser (the Dodekalogue) ### Phase 1 — tokenizer + parser (the Dodekalogue)
- [ ] Tokenizer applying the 12 rules: - [x] Tokenizer applying the 12 rules:
1. Commands separated by `;` or newlines 1. Commands separated by `;` or newlines
2. Words separated by whitespace within a command 2. Words separated by whitespace within a command
3. Double-quoted words: `\` escapes + `[…]` + `${…}` + `$var` substitution 3. Double-quoted words: `\` escapes + `[…]` + `${…}` + `$var` substitution
@@ -63,12 +63,12 @@ Core mapping:
10. Order of substitution is left-to-right, single-pass 10. Order of substitution is left-to-right, single-pass
11. Substitutions don't recurse — substituted text is not re-parsed 11. Substitutions don't recurse — substituted text is not re-parsed
12. The result of any substitution is the value, not a new script 12. The result of any substitution is the value, not a new script
- [ ] Parser: script = list of commands; command = list of words; word = literal string + list of substitutions - [x] Parser: script = list of commands; command = list of words; word = literal string + list of substitutions
- [ ] Unit tests in `lib/tcl/tests/parse.sx` - [x] Unit tests in `lib/tcl/tests/parse.sx`
### Phase 2 — sequential eval + core commands ### Phase 2 — sequential eval + core commands
- [ ] `tcl-eval-script`: walk command list, dispatch each first-word into command table - [x] `tcl-eval-script`: walk command list, dispatch each first-word into command table
- [ ] Core commands: `set`, `unset`, `incr`, `append`, `lappend`, `puts`, `gets`, `expr`, `if`, `while`, `for`, `foreach`, `switch`, `break`, `continue`, `return`, `error`, `eval`, `subst`, `format`, `scan` - [x] Core commands: `set`, `unset`, `incr`, `append`, `lappend`, `puts`, `gets`, `expr`, `if`, `while`, `for`, `foreach`, `switch`, `break`, `continue`, `return`, `error`, `eval`, `subst`, `format`, `scan`
- [ ] `expr` is its own mini-language — operator precedence, function calls (`sin`, `sqrt`, `pow`, `abs`, `int`, `double`), variable substitution, command substitution - [ ] `expr` is its own mini-language — operator precedence, function calls (`sin`, `sqrt`, `pow`, `abs`, `int`, `double`), variable substitution, command substitution
- [ ] String commands: `string length`, `string index`, `string range`, `string compare`, `string match`, `string toupper`, `string tolower`, `string trim`, `string map`, `string repeat`, `string first`, `string last`, `string is`, `string cat` - [ ] String commands: `string length`, `string index`, `string range`, `string compare`, `string match`, `string toupper`, `string tolower`, `string trim`, `string map`, `string repeat`, `string first`, `string last`, `string is`, `string cat`
- [ ] List commands: `list`, `lindex`, `lrange`, `llength`, `lreverse`, `lsearch`, `lsort`, `lsort -integer/-real/-dictionary`, `lreplace`, `linsert`, `concat`, `split`, `join` - [ ] List commands: `list`, `lindex`, `lrange`, `llength`, `lreverse`, `lsearch`, `lsort`, `lsort -integer/-real/-dictionary`, `lreplace`, `linsert`, `concat`, `split`, `join`
@@ -120,7 +120,10 @@ Core mapping:
_Newest first._ _Newest first._
- _(none yet)_ - 2026-04-26: Phase 2 core commands — if/while/for/foreach/switch/break/continue/return/error/unset/lappend/eval/expr + :code control flow, 107 tests green (67 parse + 40 eval)
- 2026-04-26: Phase 2 eval engine — `lib/tcl/runtime.sx`, tcl-eval-script + set/puts/incr/append, 87 tests green (67 parse + 20 eval)
- 2026-04-25: Phase 1 parser — `lib/tcl/parser.sx`, word-simple?/word-literal helpers, 67 tests green, commit 6ee05259
- 2026-04-25: Phase 1 tokenizer (Dodekalogue) — `lib/tcl/tokenizer.sx`, 52 tests green, commit 666e29d5
## Blockers ## Blockers