Files
rose-ash/plans/agent-briefings/tcl-loop.md
giles fb72c4ab9c sx-loops: add common-lisp, apl, ruby, tcl (12 slots)
Plans + briefings for four new language loops, each with a delcc/JIT
showcase that the runtime already supports natively:

- common-lisp — conditions + restarts on delimited continuations
- apl — rank-polymorphic primitives + 6 operators on the JIT
- ruby — fibers as delcc, blocks/yield as escape continuations
- tcl — uplevel/upvar via first-class env chain, the Dodekalogue

Launcher scripts now spawn 12 windows (was 8).
2026-04-25 09:25:30 +00:00

6.7 KiB

tcl-on-sx loop agent (single agent, queue-driven)

Role: iterates plans/tcl-on-sx.md forever. uplevel/upvar is the headline showcase — Tcl's superpower for defining your own control structures, requiring deep VM cooperation in any normal host but falling out of SX's first-class env-chain. Plus the Dodekalogue (12 rules), command-substitution everywhere, and "everything is a string" homoiconicity.

description: tcl-on-sx queue loop
subagent_type: general-purpose
run_in_background: true
isolation: worktree

Prompt

You are the sole background agent working /root/rose-ash/plans/tcl-on-sx.md. Isolated worktree, forever, one commit per feature. Never push.

Restart baseline — check before iterating

  1. Read plans/tcl-on-sx.md — roadmap + Progress log.
  2. ls lib/tcl/ — pick up from the most advanced file.
  3. If lib/tcl/tests/*.sx exist, run them. Green before new work.
  4. If lib/tcl/scoreboard.md exists, that's your baseline.

The queue

Phase order per plans/tcl-on-sx.md:

  • Phase 1 — tokenizer + parser. The Dodekalogue (12 rules): word-splitting, command sub […], var sub $name/${name}/$arr(idx), double-quote vs brace word, backslash, ;, # comments only at command start, single-pass left-to-right substitution
  • Phase 2 — sequential eval + core commands. set/unset/incr/append/lappend, puts/gets, expr (own mini-language), if/while/for/foreach/switch, string commands, list commands, dict commands
  • Phase 3THE SHOWCASE: proc + uplevel + upvar. Frame stack with proc-call push/pop; uplevel #N script evaluates in caller's frame; upvar aliases names across frames. Classic programs (for-each-line, assert macro, with-temp-var) green
  • Phase 4return -code N, catch, try/trap/finally, throw. Control flow as integer codes
  • Phase 5 — namespaces + ensembles. namespace eval, qualified names ::ns::cmd, ensembles, namespace path
  • Phase 6 — coroutines (built on fibers, same delcc as Ruby fibers) + system commands + drive corpus to 150+

Within a phase, pick the checkbox that unlocks the most tests per effort.

Every iteration: implement → test → commit → tick [ ] → Progress log → next.

Ground rules (hard)

  • Scope: only lib/tcl/** and plans/tcl-on-sx.md. Do not edit spec/, hosts/, shared/, other lib/<lang>/ dirs, lib/stdlib.sx, or lib/ root. Tcl primitives go in lib/tcl/runtime.sx.
  • NEVER call sx_build. 600s watchdog. If sx_server binary broken → Blockers entry, stop.
  • Shared-file issues → plan's Blockers with minimal repro.
  • Delimited continuations are in lib/callcc.sx + spec/evaluator.sx Step 5. sx_summarise spec/evaluator.sx first — 2300+ lines.
  • SX files: sx-tree MCP tools ONLY. sx_validate after edits.
  • Worktree: commit locally. Never push. Never touch main.
  • Commit granularity: one feature per commit.
  • Plan file: update Progress log + tick boxes every commit.

Tcl-specific gotchas

  • Everything is a string. Internally cache shimmer reps (list, dict, int, double) for performance, but every value must be re-stringifiable. Mutating one rep dirties the cached string and vice versa.
  • The Dodekalogue is strict. Substitution is one-pass, left-to-right. The result of a substitution is a value, not a script — it does NOT get re-parsed for further substitutions. This is what makes Tcl safe-by-default. Don't accidentally re-parse.
  • Brace word {…} is the only way to defer evaluation. No substitution inside, just balanced braces. Used for if {expr} body, proc body, expr arguments.
  • Double-quote word "…" is identical to a bare word for substitution purposes — it just allows whitespace in a single word. \ escapes still apply.
  • Comments are only at command position. # this is a comment after a ; or newline; not inside a command. set x 1 # not a comment is a 4-arg set.
  • expr has its own grammar — operator precedence, function calls — and does its own substitution. Brace expr {$x + 1} to avoid double-substitution and to enable bytecode caching.
  • if and while re-parse the condition only if not braced. Always use if {…}/while {…} form. The unbraced form re-substitutes per iteration.
  • return from a proc uses control code 2. break is 3, continue is 4. error is 1. catch traps any non-zero code; user can return non-zero with return -code error -errorcode FOO message.
  • uplevel #0 script is global frame. uplevel 1 script (or just uplevel script) is caller's frame. uplevel #N is absolute level N (0=global, 1=top-level proc, 2=proc-called-from-top, …). Negative levels are errors.
  • upvar #N otherVar localVar binds localVar in the current frame as an alias — both names refer to the same storage. Reads and writes go through the alias.
  • info level with no arg returns current level number. info level N (positive) returns the command list that invoked level N. info level -N returns the command list of the level N relative-up.
  • Variable names with (…) are array elements: set arr(foo) 1. Arrays are not first-class values — you can't set x $arr. array get arr gives a flat list {key1 val1 key2 val2 …}.
  • List vs string. set l "a b c" and set l [list a b c] look the same when printed but the second has a cached list rep. lindex works on both via shimmering. Most user code can't tell the difference.
  • incr x errors if x doesn't exist; pre-set with set x 0 or use incr x 0 first if you mean "create-or-increment". Or use dict incr for dicts.
  • Coroutines are fibers. coroutine name body starts a coroutine; calling name resumes it; yield value from inside suspends and returns value to the resumer. Same primitive as Ruby fibers — share the implementation under the hood.
  • switch matches first clause whose pattern matches. Default is default. Variant matches: glob (default), -exact, -glob, -regexp. Body - means "fall through to next clause's body".
  • Test corpus: custom + slice of Tcl's own tests. Place programs in lib/tcl/tests/programs/ with .tcl extension.

General gotchas (all loops)

  • SX do = R7RS iteration. Use begin for multi-expr sequences.
  • cond/when/let clauses evaluate only the last expr.
  • type-of on user fn returns "lambda".
  • Shell heredoc || gets eaten — escape or use case.

Style

  • No comments in .sx unless non-obvious.
  • No new planning docs — update plans/tcl-on-sx.md inline.
  • Short, factual commit messages (tcl: uplevel + upvar (+11)).
  • One feature per iteration. Commit. Log. Next.

Go. Read the plan; find first [ ]; implement.