tcl: Phase 6 coroutines + clock/file stubs + idiom corpus (+40 tests, 329 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled

- Coroutines (generator-style): coroutine/yield/yieldto commands; eager yield
  collection during body execution, pop-on-call dispatch via registered command
  closures; coro-yields + coroutines threaded through tcl-call-proc
- info exists varname (plus hostname/script/tclversion stubs)
- clock seconds/milliseconds/format/scan stubs
- File I/O stubs: open/close/read/eof/seek/tell/flush + file subcommands
- format command: full %-specifier parsing with flags, width, zero-pad, left-align
- Fixed dict set/unset/incr/append/update to use tcl-var-get (upvar alias aware)
- Fixed lappend and append to use tcl-var-get for reading (upvar alias aware)
- 20 coroutine tests (coro.sx) + 20 idiom corpus tests (idioms.sx)
- event-loop.tcl program: cooperative scheduler demo using coroutines
- Note: coroutines eagerly collect yields (generator-style, not true suspension)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 11:05:28 +00:00
parent ea064346e1
commit 2c61be39de
5 changed files with 805 additions and 23 deletions

View File

@@ -20,7 +20,7 @@
(frame name val)
(assoc frame :locals (assoc (get frame :locals) name val))))
(define make-tcl-interp (fn () {:result "" :output "" :code 0 :errorinfo "" :errorcode "" :frame (make-frame 0 nil) :frame-stack (list) :procs {} :commands {} :current-ns "::"}))
(define make-tcl-interp (fn () {:result "" :output "" :code 0 :errorinfo "" :errorcode "" :frame (make-frame 0 nil) :frame-stack (list) :procs {} :commands {} :current-ns "::" :coroutines {} :in-coro false :coro-yields (list)}))
(define
tcl-register
@@ -296,7 +296,10 @@
:frame-stack updated-below
:result result-val
:output (str caller-output proc-output)
:code (if (= code 2) 0 code))))))))))))))
:code (if (= code 2) 0 code)
:coro-yields (get result-interp :coro-yields)
:coroutines (get result-interp :coroutines)
:commands (get result-interp :commands))))))))))))))
(define
tcl-eval-cmd
@@ -383,7 +386,7 @@
(let
((name (first args)) (suffix (join "" (rest args))))
(let
((cur (let ((v (frame-lookup (get interp :frame) name))) (if (nil? v) "" v))))
((cur (let ((v (frame-lookup (get interp :frame) name))) (if (nil? v) "" (tcl-var-get interp name)))))
(let
((new-val (str cur suffix)))
(assoc (tcl-var-set interp name new-val) :result new-val))))))
@@ -1102,10 +1105,12 @@
(let
((name (first args)) (items (rest args)))
(let
((cur (let ((v (frame-lookup (get interp :frame) name))) (if (nil? v) "" v))))
((cur (let ((v (frame-lookup (get interp :frame) name))) (if (nil? v) "" (tcl-var-get interp name)))))
(let
((new-val (if (equal? cur "") (join " " items) (str cur " " (join " " items)))))
(assoc (tcl-var-set interp name new-val) :result new-val))))))
((quoted-items (map tcl-list-quote-elem items)))
(let
((new-val (if (equal? cur "") (join " " quoted-items) (str cur " " (join " " quoted-items)))))
(assoc (tcl-var-set interp name new-val) :result new-val)))))))
(define
tcl-cmd-eval
@@ -1281,9 +1286,139 @@
tcl-cmd-subst
(fn (interp args) (assoc interp :result (last args))))
; Format helper: repeat char ch n times, building pad string
(define
tcl-fmt-make-pad
(fn
(ch cnt acc)
(if (<= cnt 0) acc (tcl-fmt-make-pad ch (- cnt 1) (str ch acc)))))
; Format helper: pad string s to width w
(define
tcl-fmt-pad
(fn
(s width zero-pad? left-align?)
(let
((w (if (equal? width "") 0 (parse-int width))))
(let
((pad-len (- w (string-length s))))
(if
(<= pad-len 0)
s
(let
((pad (tcl-fmt-make-pad (if zero-pad? "0" " ") pad-len "")))
(if left-align? (str s pad) (str pad s))))))))
; Format helper: scan flag characters
(define
tcl-fmt-scan-flags
(fn
(chars j flags)
(if
(>= j (len chars))
{:j j :flags flags}
(let
((ch (nth chars j)))
(if
(contains? (list "-" "0" "+" " " "#") ch)
(tcl-fmt-scan-flags chars (+ j 1) (str flags ch))
{:j j :flags flags})))))
; Format helper: scan digits for width/precision
(define
tcl-fmt-scan-num
(fn
(chars j acc-n)
(if
(>= j (len chars))
{:j j :num acc-n}
(let
((ch (nth chars j)))
(if
(tcl-expr-digit? ch)
(tcl-fmt-scan-num chars (+ j 1) (str acc-n ch))
{:j j :num acc-n})))))
; Main format apply: process chars, produce output string
(define
tcl-fmt-apply
(fn
(chars n-len fmt-args i arg-idx acc)
(if
(>= i n-len)
acc
(let
((c (nth chars i)))
(if
(not (equal? c "%"))
(tcl-fmt-apply chars n-len fmt-args (+ i 1) arg-idx (str acc c))
; parse specifier
(let
((i2 (+ i 1)))
(if
(>= i2 n-len)
(str acc "%")
(let
((c2 (nth chars i2)))
(if
(equal? c2 "%")
(tcl-fmt-apply chars n-len fmt-args (+ i2 1) arg-idx (str acc "%"))
; scan flags
(let
((fr (tcl-fmt-scan-flags chars i2 "")))
(let
((flags (get fr :flags)) (j (get fr :j)))
(let
((wr (tcl-fmt-scan-num chars j "")))
(let
((width (get wr :num)) (j2 (get wr :j)))
; skip precision .N
(let
((j3
(if
(and (< j2 n-len) (equal? (nth chars j2) "."))
(let ((pr (tcl-fmt-scan-num chars (+ j2 1) ""))) (get pr :j))
j2)))
(if
(>= j3 n-len)
(str acc "?")
(let
((type-char (nth chars j3))
(cur-arg (if (< arg-idx (len fmt-args)) (nth fmt-args arg-idx) "")))
(let
((zero-pad? (contains? (split flags "") "0"))
(left-align? (contains? (split flags "") "-")))
(let
((formatted
(cond
((or (equal? type-char "d") (equal? type-char "i"))
(tcl-fmt-pad (str (parse-int cur-arg)) width zero-pad? left-align?))
((equal? type-char "s")
(tcl-fmt-pad cur-arg width false left-align?))
((or (equal? type-char "f") (equal? type-char "g") (equal? type-char "e"))
cur-arg)
((equal? type-char "x")
(str (parse-int cur-arg)))
((equal? type-char "o")
(str (parse-int cur-arg)))
((equal? type-char "c")
cur-arg)
(else (str "%" type-char)))))
(tcl-fmt-apply chars n-len fmt-args (+ j3 1) (+ arg-idx 1) (str acc formatted))))))))))))))))))))
(define
tcl-cmd-format
(fn (interp args) (assoc interp :result (join "" args))))
(fn
(interp args)
(if
(= 0 (len args))
(error "format: wrong # args")
(let
((fmt-str (first args)) (fmt-args (rest args)))
(let
((chars (split fmt-str ""))
(n-len (string-length fmt-str)))
(assoc interp :result (tcl-fmt-apply chars n-len fmt-args 0 0 "")))))))
(define tcl-cmd-scan (fn (interp args) (assoc interp :result "0")))
@@ -2092,7 +2227,7 @@
(key (nth rest-args 1))
(val (nth rest-args 2)))
(let
((cur (let ((v (frame-lookup (get interp :frame) varname))) (if (nil? v) "" v))))
((cur (let ((v (if (nil? (frame-lookup (get interp :frame) varname)) nil (tcl-var-get interp varname)))) (if (nil? v) "" v))))
(let
((new-dict (tcl-dict-set-pair cur key val)))
(assoc (tcl-var-set interp varname new-dict) :result new-dict)))))
@@ -2101,7 +2236,7 @@
(let
((varname (first rest-args)) (key (nth rest-args 1)))
(let
((cur (let ((v (frame-lookup (get interp :frame) varname))) (if (nil? v) "" v))))
((cur (let ((v (if (nil? (frame-lookup (get interp :frame) varname)) nil (tcl-var-get interp varname)))) (if (nil? v) "" v))))
(let
((new-dict (tcl-dict-unset-key cur key)))
(assoc (tcl-var-set interp varname new-dict) :result new-dict)))))
@@ -2177,7 +2312,7 @@
((body (nth rest-args (- n 1)))
(kv-args (slice rest-args 1 (- n 1))))
(let
((cur (let ((v (frame-lookup (get interp :frame) varname))) (if (nil? v) "" v))))
((cur (let ((v (if (nil? (frame-lookup (get interp :frame) varname)) nil (tcl-var-get interp varname)))) (if (nil? v) "" v))))
(let
((bound-interp
(let
@@ -2234,7 +2369,7 @@
(key (nth rest-args 1))
(delta (if (> (len rest-args) 2) (parse-int (nth rest-args 2)) 1)))
(let
((cur (let ((v (frame-lookup (get interp :frame) varname))) (if (nil? v) "" v))))
((cur (let ((v (if (nil? (frame-lookup (get interp :frame) varname)) nil (tcl-var-get interp varname)))) (if (nil? v) "" v))))
(let
((old-val (let ((v (tcl-dict-get cur key))) (if (nil? v) "0" v))))
(let
@@ -2249,7 +2384,7 @@
(key (nth rest-args 1))
(suffix (join "" (slice rest-args 2 (len rest-args)))))
(let
((cur (let ((v (frame-lookup (get interp :frame) varname))) (if (nil? v) "" v))))
((cur (let ((v (if (nil? (frame-lookup (get interp :frame) varname)) nil (tcl-var-get interp varname)))) (if (nil? v) "" v))))
(let
((old-val (let ((v (tcl-dict-get cur key))) (if (nil? v) "" v))))
(let
@@ -2776,8 +2911,264 @@
(nil? entry)
(error (str "info body: \"" pname "\" isn't a procedure"))
(assoc interp :result (get (get entry :def) :body))))))
; info exists varname — 1 if variable exists in current frame, 0 otherwise
((equal? sub "exists")
(let
((varname (first rest-args)))
(let
((val (frame-lookup (get interp :frame) varname)))
(assoc interp :result (if (nil? val) "0" "1")))))
; info hostname — stub
((equal? sub "hostname")
(assoc interp :result "localhost"))
; info script — stub
((equal? sub "script")
(assoc interp :result ""))
; info tclversion — stub
((equal? sub "tclversion")
(assoc interp :result "8.6"))
(else (error (str "info: unknown subcommand \"" sub "\""))))))))
; --- coroutine support ---
; yield: inside a coroutine body, record a yielded value
(define
tcl-cmd-yield
(fn
(interp args)
(let
((val (if (> (len args) 0) (first args) "")))
(if
(get interp :in-coro)
(assoc
(assoc interp :coro-yields (append (get interp :coro-yields) (list val)))
:result "")
(error "yield called outside coroutine")))))
; yieldto: stub — yield empty string
(define
tcl-cmd-yieldto
(fn
(interp args)
(if
(get interp :in-coro)
(assoc
(assoc interp :coro-yields (append (get interp :coro-yields) (list "")))
:result "")
(error "yieldto called outside coroutine"))))
; make-coro-cmd: returns a command function that pops values from the coroutine's yields list
(define
make-coro-cmd
(fn
(coro-name)
(fn
(interp args)
(let
((coros (get interp :coroutines)))
(let
((coro (get coros coro-name)))
(if
(nil? coro)
(error (str "coroutine \"" coro-name "\" not found"))
(let
((yields (get coro :yields))
(pos (get coro :pos)))
(if
(>= pos (len yields))
(assoc interp :result "")
(let
((val (nth yields pos)))
(let
((new-coro (assoc coro :pos (+ pos 1))))
(assoc
(assoc interp :coroutines (assoc coros coro-name new-coro))
:result val)))))))))))
; coroutine: execute proc eagerly in a coroutine context, collecting all yields
(define
tcl-cmd-coroutine
(fn
(interp args)
(if
(< (len args) 2)
(error "coroutine: wrong # args")
(let
((coro-name (first args))
(cmd-name (nth args 1))
(call-args (rest (rest args))))
; set up coroutine context
(let
((coro-interp
(assoc interp
:in-coro true
:coro-yields (list)
:result ""
:code 0)))
; find the command or proc and execute it
(let
((cmd-fn (get (get coro-interp :commands) cmd-name)))
(let
((exec-result
(if
(nil? cmd-fn)
(let
((proc-entry (tcl-proc-lookup coro-interp cmd-name)))
(if
(nil? proc-entry)
(error (str "coroutine: unknown command \"" cmd-name "\""))
(tcl-call-proc coro-interp (get proc-entry :name) (get proc-entry :def) call-args)))
(cmd-fn coro-interp call-args))))
(let
((yields (get exec-result :coro-yields)))
; build the coroutine state
(let
((new-coros (assoc (get exec-result :coroutines) coro-name {:yields yields :pos 0})))
; register the coroutine command in the commands dict
(let
((new-commands (assoc (get exec-result :commands) coro-name (make-coro-cmd coro-name))))
(assoc exec-result
:coroutines new-coros
:commands new-commands
:in-coro false
:coro-yields (list)
:result "")))))))))))
; --- clock command (stubs) ---
(define
tcl-cmd-clock
(fn
(interp args)
(if
(= 0 (len args))
(error "clock: wrong # args")
(let
((sub (first args)) (rest-args (rest args)))
(cond
((equal? sub "seconds") (assoc interp :result "0"))
((equal? sub "milliseconds") (assoc interp :result "0"))
((equal? sub "format") (assoc interp :result "Thu Jan 1 00:00:00 UTC 1970"))
((equal? sub "scan") (assoc interp :result "0"))
(else (error (str "clock: unknown subcommand \"" sub "\""))))))))
; --- file I/O stubs ---
(define
tcl-cmd-open
(fn
(interp args)
(assoc interp :result "file0")))
(define
tcl-cmd-close
(fn
(interp args)
(assoc interp :result "")))
(define
tcl-cmd-read
(fn
(interp args)
(assoc interp :result "")))
; gets channel ?varname?
(define
tcl-cmd-gets-chan
(fn
(interp args)
(if
(> (len args) 1)
; gets channel varname: store "" and return -1 (EOF)
(assoc (tcl-var-set interp (nth args 1) "") :result "-1")
; gets channel: return "" (EOF)
(assoc interp :result ""))))
(define
tcl-cmd-eof
(fn
(interp args)
(assoc interp :result "1")))
(define
tcl-cmd-seek
(fn
(interp args)
(assoc interp :result "")))
(define
tcl-cmd-tell
(fn
(interp args)
(assoc interp :result "0")))
(define
tcl-cmd-flush
(fn
(interp args)
(assoc interp :result "")))
; file command dispatcher
(define
tcl-cmd-file
(fn
(interp args)
(if
(= 0 (len args))
(error "file: wrong # args")
(let
((sub (first args)) (rest-args (rest args)))
(cond
((equal? sub "exists")
(assoc interp :result "0"))
((equal? sub "join")
(assoc interp :result (join "/" rest-args)))
((equal? sub "split")
(assoc interp :result (tcl-list-build (filter (fn (s) (not (equal? s ""))) (split (first rest-args) "/")))))
((equal? sub "tail")
(let
((parts (filter (fn (s) (not (equal? s ""))) (split (first rest-args) "/"))))
(assoc interp :result (if (= 0 (len parts)) "" (last parts)))))
((equal? sub "dirname")
(let
((parts (filter (fn (s) (not (equal? s ""))) (split (first rest-args) "/"))))
(assoc interp :result
(if
(<= (len parts) 1)
"."
(str "/" (join "/" (take-n parts (- (len parts) 1))))))))
((equal? sub "extension")
(let
((nm (first rest-args)))
(let
((dot-idx (tcl-string-last "." nm (- (string-length nm) 1))))
(assoc interp :result
(if
(equal? dot-idx "-1")
""
(substring nm (parse-int dot-idx) (string-length nm)))))))
((equal? sub "rootname")
(let
((nm (first rest-args)))
(let
((dot-idx (tcl-string-last "." nm (- (string-length nm) 1))))
(assoc interp :result
(if
(equal? dot-idx "-1")
nm
(substring nm 0 (parse-int dot-idx)))))))
((equal? sub "isfile") (assoc interp :result "0"))
((equal? sub "isdir") (assoc interp :result "0"))
((equal? sub "isdirectory") (assoc interp :result "0"))
((equal? sub "readable") (assoc interp :result "0"))
((equal? sub "writable") (assoc interp :result "0"))
((equal? sub "size") (assoc interp :result "0"))
((equal? sub "mkdir") (assoc interp :result ""))
((equal? sub "copy") (assoc interp :result ""))
((equal? sub "rename") (assoc interp :result ""))
((equal? sub "delete") (assoc interp :result ""))
(else (error (str "file: unknown subcommand \"" sub "\""))))))))
(define
make-default-tcl-interp
(fn
@@ -2872,4 +3263,28 @@
((i (tcl-register i "throw" tcl-cmd-throw)))
(let
((i (tcl-register i "try" tcl-cmd-try)))
(tcl-register i "namespace" tcl-cmd-namespace))))))))))))))))))))))))))))))))))))))))))))))))
(let
((i (tcl-register i "namespace" tcl-cmd-namespace)))
(let
((i (tcl-register i "coroutine" tcl-cmd-coroutine)))
(let
((i (tcl-register i "yield" tcl-cmd-yield)))
(let
((i (tcl-register i "yieldto" tcl-cmd-yieldto)))
(let
((i (tcl-register i "clock" tcl-cmd-clock)))
(let
((i (tcl-register i "open" tcl-cmd-open)))
(let
((i (tcl-register i "close" tcl-cmd-close)))
(let
((i (tcl-register i "read" tcl-cmd-read)))
(let
((i (tcl-register i "eof" tcl-cmd-eof)))
(let
((i (tcl-register i "seek" tcl-cmd-seek)))
(let
((i (tcl-register i "tell" tcl-cmd-tell)))
(let
((i (tcl-register i "flush" tcl-cmd-flush)))
(tcl-register i "file" tcl-cmd-file))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))