tcl: proc + uplevel + upvar + global + variable + info (+19 tests, 225 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 48s

Phase 3 headline feature: everything falls out of SX's first-class env chain.
- make-tcl-interp extended with :frame-stack and :procs fields
- proc: user-defined commands with param binding, rest args, isolated scope
- uplevel: run script in ancestor frame with correct frame propagation
- upvar: alias local name to remote frame variable (get/set follow alias)
- global/variable: sugar for upvar #0
- info: level, vars, locals, globals, commands, procs, args, body
- tcl-call-proc propagates updated frames back to caller after proc returns
- test.sh timeout bumped to 90s for larger runtime

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 09:30:28 +00:00
parent a49b1a9f79
commit eb5babaf99
3 changed files with 422 additions and 6 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 :frame (make-frame 0 nil) :commands {}}))
(define make-tcl-interp (fn () {:result "" :output "" :code 0 :frame (make-frame 0 nil) :frame-stack (list) :procs {} :commands {}}))
(define
tcl-register
@@ -28,6 +28,50 @@
(interp name f)
(assoc interp :commands (assoc (get interp :commands) name f))))
; --- upvar alias helpers ---
(define upvar-alias? (fn (v) (and (dict? v) (not (nil? (get v :upvar-level))))))
; take first n elements of a list
(define
take-n
(fn
(lst n)
(if
(or (<= n 0) (= 0 (len lst)))
(list)
(append (list (first lst)) (take-n (rest lst) (- n 1))))))
; replace element at index i in list with val (0-based)
(define
replace-at
(fn
(lst i val)
(let
((go
(fn
(remaining j acc)
(if
(= 0 (len remaining))
acc
(go
(rest remaining)
(+ j 1)
(append acc (list (if (= j i) val (first remaining)))))))))
(go lst 0 (list)))))
; build full-stack = frame-stack + [current-frame]
(define
tcl-full-stack
(fn (interp)
(append (get interp :frame-stack) (list (get interp :frame)))))
; get target frame at absolute level from full-stack
(define
tcl-frame-nth
(fn (full-stack level)
(nth full-stack level)))
(define
tcl-var-get
(fn
@@ -37,13 +81,50 @@
(if
(nil? val)
(error (str "can't read \"" name "\": no such variable"))
val))))
(if
(upvar-alias? val)
; follow alias to target frame
(let
((target-level (get val :upvar-level))
(target-name (get val :upvar-name)))
(let
((full-stack (tcl-full-stack interp)))
(let
((target-frame (tcl-frame-nth full-stack target-level)))
(let
((target-val (frame-lookup target-frame target-name)))
(if
(nil? target-val)
(error (str "can't read \"" name "\": no such variable"))
target-val)))))
val)))))
(define
tcl-var-set
(fn
(interp name val)
(assoc interp :frame (frame-set-top (get interp :frame) name val))))
(let
((cur-val (get (get (get interp :frame) :locals) name)))
(if
(and (not (nil? cur-val)) (upvar-alias? cur-val))
; set in target frame
(let
((target-level (get cur-val :upvar-level))
(target-name (get cur-val :upvar-name)))
(let
((full-stack (tcl-full-stack interp)))
(let
((target-frame (tcl-frame-nth full-stack target-level)))
(let
((updated-target (frame-set-top target-frame target-name val)))
(let
((new-full-stack (replace-at full-stack target-level updated-target)))
(let
((new-frame-stack (take-n new-full-stack (- (len new-full-stack) 1)))
(new-current (nth new-full-stack (- (len new-full-stack) 1))))
(assoc interp :frame new-current :frame-stack new-frame-stack)))))))
; normal set in current frame top
(assoc interp :frame (frame-set-top (get interp :frame) name val))))))
(define
tcl-eval-parts
@@ -143,6 +224,77 @@
{:values (quote ()) :interp interp}
words)))
; --- proc call ---
; Bind proc parameters: returns updated frame
(define
tcl-bind-params
(fn
(frame params call-args)
(if
(= 0 (len params))
frame
(let
((pname (first params)) (rest-ps (rest params)))
(if
(equal? pname "args")
; rest param: collect remaining call-args as list string
(frame-set-top frame "args" (tcl-list-build call-args))
(if
(= 0 (len call-args))
(error (str "wrong # args: no value for parameter \"" pname "\""))
(tcl-bind-params
(frame-set-top frame pname (first call-args))
rest-ps
(rest call-args))))))))
(define
tcl-call-proc
(fn
(interp proc-name proc-def call-args)
(let
((param-spec (get proc-def :args))
(body (get proc-def :body)))
(let
((params (if (equal? param-spec "") (list) (tcl-list-split param-spec))))
(let
((caller-stack-len (len (get interp :frame-stack)))
(new-frame (make-frame (+ (len (get interp :frame-stack)) 1) nil)))
(let
((bound-frame (tcl-bind-params new-frame params call-args)))
(let
((proc-interp
(assoc interp
:frame bound-frame
:frame-stack (append (get interp :frame-stack) (list (get interp :frame)))
:output ""
:result ""
:code 0))
(caller-output (get interp :output)))
(let
((result-interp (tcl-eval-string proc-interp body)))
(let
((code (get result-interp :code))
(result-val (get result-interp :result))
(proc-output (get result-interp :output)))
(let
; result-stack = [updated-frame-0..updated-caller-frame]
; recover updated caller frame and below-caller frames
((result-stack (get result-interp :frame-stack)))
(let
((updated-below (take-n result-stack caller-stack-len))
(updated-caller
(if
(> (len result-stack) caller-stack-len)
(nth result-stack caller-stack-len)
(get interp :frame))))
(assoc interp
:frame updated-caller
:frame-stack updated-below
:result result-val
:output (str caller-output proc-output)
:code (if (= code 2) 0 code)))))))))))))
(define
tcl-eval-cmd
(fn
@@ -160,7 +312,12 @@
((cmd-fn (get (get cur-interp :commands) cmd-name)))
(if
(nil? cmd-fn)
(error (str "unknown command: \"" cmd-name "\""))
(let
((proc-def (get (get cur-interp :procs) cmd-name)))
(if
(nil? proc-def)
(error (str "unknown command: \"" cmd-name "\""))
(tcl-call-proc cur-interp cmd-name proc-def cmd-args)))
(cmd-fn cur-interp cmd-args)))))))))
(define
@@ -1875,6 +2032,229 @@
(assoc (tcl-var-set interp varname new-dict) :result new-dict)))))))
(else (error (str "dict: unknown subcommand \"" sub "\""))))))))
; --- proc command ---
(define
tcl-cmd-proc
(fn
(interp args)
(let
((name (first args))
(arg-spec (nth args 1))
(body (nth args 2)))
(assoc interp
:procs (assoc (get interp :procs) name {:args arg-spec :body body})
:result ""))))
; --- parse uplevel/upvar level argument ---
; Returns absolute level number.
; current-level = len(frame-stack)
(define
tcl-parse-level
(fn
(level-str current-level)
(if
(equal? (substring level-str 0 1) "#")
; absolute: #N
(parse-int (substring level-str 1 (string-length level-str)))
; relative: N levels up from current
(- current-level (parse-int level-str)))))
; --- uplevel command ---
(define
tcl-cmd-uplevel
(fn
(interp args)
(let
((current-level (len (get interp :frame-stack))))
(let
; check if first arg is a level specifier
((has-level
(and
(> (len args) 1)
(or
(equal? (substring (first args) 0 1) "#")
(let
((fst (first args)))
(and
(> (string-length fst) 0)
(tcl-expr-digit? (substring fst 0 1)))))))
(level-str (if (and (> (len args) 1) (or (equal? (substring (first args) 0 1) "#") (and (> (string-length (first args)) 0) (tcl-expr-digit? (substring (first args) 0 1))))) (first args) "1"))
(script (if (and (> (len args) 1) (or (equal? (substring (first args) 0 1) "#") (and (> (string-length (first args)) 0) (tcl-expr-digit? (substring (first args) 0 1))))) (nth args 1) (first args))))
(let
((target-level (tcl-parse-level level-str current-level)))
(let
((full-stack (tcl-full-stack interp)))
(let
((target-frame (tcl-frame-nth full-stack target-level)))
(let
((temp-interp
(assoc interp
:frame target-frame
:frame-stack (take-n (get interp :frame-stack) target-level)
:output ""))
(saved-output (get interp :output)))
(let
((result-interp (tcl-eval-string temp-interp script)))
(let
((updated-target (get result-interp :frame))
(new-output (get result-interp :output)))
(let
((new-full-stack (replace-at full-stack target-level updated-target)))
(let
((new-frame-stack (take-n new-full-stack (- (len new-full-stack) 1)))
(new-current (nth new-full-stack (- (len new-full-stack) 1))))
(assoc interp
:frame new-current
:frame-stack new-frame-stack
:result (get result-interp :result)
:output (str saved-output new-output)
:code (get result-interp :code))))))))))))))
; --- upvar command ---
(define
tcl-cmd-upvar
(fn
(interp args)
(let
((current-level (len (get interp :frame-stack))))
(let
; check if first arg is a level specifier
((has-level
(and
(> (len args) 2)
(or
(equal? (substring (first args) 0 1) "#")
(tcl-expr-digit? (substring (first args) 0 1)))))
(level-str (if (and (> (len args) 2) (or (equal? (substring (first args) 0 1) "#") (tcl-expr-digit? (substring (first args) 0 1)))) (first args) "1"))
(pair-args (if (and (> (len args) 2) (or (equal? (substring (first args) 0 1) "#") (tcl-expr-digit? (substring (first args) 0 1)))) (rest args) args)))
(let
((target-level (tcl-parse-level level-str current-level)))
(let
((bind-pairs
(fn
(i-interp remaining)
(if
(< (len remaining) 2)
i-interp
(let
((remote-name (first remaining))
(local-name (nth remaining 1)))
(let
((alias {:upvar-level target-level :upvar-name remote-name}))
(bind-pairs
(assoc i-interp :frame (frame-set-top (get i-interp :frame) local-name alias))
(rest (rest remaining)))))))))
(assoc (bind-pairs interp pair-args) :result "")))))))
; --- global command ---
(define
tcl-cmd-global
(fn
(interp args)
(reduce
(fn
(i name)
(tcl-cmd-upvar i (list "#0" name name)))
interp
args)))
; --- variable command ---
(define
tcl-cmd-variable
(fn
(interp args)
(let
((go
(fn
(i remaining)
(if
(= 0 (len remaining))
i
(let
((name (first remaining))
(rest-rem (rest remaining)))
(let
((linked (tcl-cmd-upvar i (list "#0" name name))))
(if
(and (> (len rest-rem) 0) (not (equal? (substring (first rest-rem) 0 1) "-")))
(let
((val (first rest-rem)))
(go (assoc (tcl-var-set linked name val) :result "") (rest rest-rem)))
(go linked rest-rem))))))))
(go interp args))))
; --- info command ---
(define
tcl-cmd-info
(fn
(interp args)
(if
(= 0 (len args))
(error "info: wrong # args")
(let
((sub (first args)) (rest-args (rest args)))
(cond
; info level
((equal? sub "level")
(assoc interp :result (str (len (get interp :frame-stack)))))
; info vars / info locals
((or (equal? sub "vars") (equal? sub "locals"))
(let
((frame-locals (get (get interp :frame) :locals)))
(assoc interp :result
(tcl-list-build
(filter
(fn (k) (not (upvar-alias? (get frame-locals k))))
(keys frame-locals))))))
; info globals
((equal? sub "globals")
(let
((global-frame
(if
(= 0 (len (get interp :frame-stack)))
(get interp :frame)
(first (get interp :frame-stack)))))
(let
((global-locals (get global-frame :locals)))
(assoc interp :result
(tcl-list-build
(filter
(fn (k) (not (upvar-alias? (get global-locals k))))
(keys global-locals)))))))
; info commands
((equal? sub "commands")
(assoc interp :result (tcl-list-build (keys (get interp :commands)))))
; info procs
((equal? sub "procs")
(assoc interp :result (tcl-list-build (keys (get interp :procs)))))
; info args procname
((equal? sub "args")
(let
((pname (first rest-args)))
(let
((proc-def (get (get interp :procs) pname)))
(if
(nil? proc-def)
(error (str "info args: \"" pname "\" isn't a procedure"))
(assoc interp :result (get proc-def :args))))))
; info body procname
((equal? sub "body")
(let
((pname (first rest-args)))
(let
((proc-def (get (get interp :procs) pname)))
(if
(nil? proc-def)
(error (str "info body: \"" pname "\" isn't a procedure"))
(assoc interp :result (get proc-def :body))))))
(else (error (str "info: unknown subcommand \"" sub "\""))))))))
(define
make-default-tcl-interp
(fn
@@ -1949,4 +2329,16 @@
((i (tcl-register i "split" tcl-cmd-split)))
(let
((i (tcl-register i "join" tcl-cmd-join)))
(tcl-register i "dict" tcl-cmd-dict))))))))))))))))))))))))))))))))))))))
(let
((i (tcl-register i "dict" tcl-cmd-dict)))
(let
((i (tcl-register i "proc" tcl-cmd-proc)))
(let
((i (tcl-register i "uplevel" tcl-cmd-uplevel)))
(let
((i (tcl-register i "upvar" tcl-cmd-upvar)))
(let
((i (tcl-register i "global" tcl-cmd-global)))
(let
((i (tcl-register i "variable" tcl-cmd-variable)))
(tcl-register i "info" tcl-cmd-info))))))))))))))))))))))))))))))))))))))))))))