12 KiB
G-007 Location-transparent punk::console across repl and code interps
Status: achieved 2026-07-05
Scope: src/modules/punk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm, src/modules/punk/repl/codethread-999999.0a1.0.tm
Goal: punk::console presents one API in every interp/thread of a punk session - code-interp callers see the same console facts and can perform the same queries/operations as the parent, routed to the console-owning context via a punk-side ownership registry, with no repl eval required.
Acceptance: from a running punk session's code interp, without repl eval: console_fact_get returns the same values the parent sees and a fact set in the parent is immediately visible; a terminal query (e.g. get_cursor_pos or dec_get_mode) against the default console succeeds and cooperates with the repl reader (no lost or garbled input); a console constructed and owned by code-interp code is operated on locally (no round-trip to the parent); the existing console test suites pass and single-interp (non-repl) usage is unchanged.
Context
A punk session splits work between the parent (repl) thread/interp and a code interp running in a
codethread. punk::console was written for application code, but it also carries the framework's
console infrastructure - and all of its interesting state lives in the parent: the per-console
facts store (console_facts + legacy variables), the dec_has_mode/ansi_has_mode caches, the
anchored default instance with its settled can_respond, the cooperative
input_chunks_waiting store, and raw-mode state. A code interp that does
package require punk::console gets a structurally identical but hollow twin: empty facts, no
anchored instances - and, worse than invisibility, a second would-be stdin reader that is not
part of the parent's cooperative reader protocol. Exploring or building against punk::console
from inside a session therefore requires repl eval, which exists as a development hack.
The distinction that matters is not application-vs-infrastructure audience but ownership and
location of state: exactly one context can own an input channel's reader and the process
console's raw mode. Any operation on a console you do not own must be brokered to the owner.
The brokering already exists in ad-hoc form: the repl installs a vt52 alias in the code interp
that forwards via thread::send to the repl thread (repl-999999.0a1.0.tm ~:3435), proving the
transport works while the codethread runs (the repl thread services events).
The punk::console 0.2.0-0.5.0 migration created the small set of choke points that make a
systematic bridge tractable: all queries flow through internal::get_ansi_response_payload,
all spec handling through console_spec_resolve / internal::opt_console_* /
internal::hybrid_console_spec, all terminal-property facts through
console_fact_get/console_fact_set, and raw mode through enableRaw/disableRaw. Bridging
those bridges everything above them. There is also precedent for cross-thread state: is_raw
already lives in tsv because it must be visible to all threads.
Approach
- Ownership registry (punk-side, tsv-backed). punk::console records an owner context per
registered/anchored console, captured at registration time (the context performing
opunk::console::create/anchoring is the owner - this covers G-001-style backends such as a tk-widget console owned by the thread running the Tk mainloop, with no change to the opunk class). The default console's owner is the repl thread. Consulted at the routing choke points at use time, so it is always current. Stale-entry handling: hook the opunk lifecycle (ensure_object_integration precedent) soforgetclears the entry, and/or validate owner liveness (thread::exists) at consult time - this recovers the lifecycle cohesion that an opunk-side registry would have had natively. - Fact and cache visibility via tsv.
console_fact_get/console_fact_set(and the mode caches) move to - or are mirrored in - tsv so any thread reads the same values with no RPC. Nuance: non-default channel names are thread-local, so tsv keys need an owner qualifier (or sharing may initially be limited to the default console) to avoid cross-thread aliasing. - Choke-point brokering. Codethread setup installs shims generalising the existing
vt52alias:thread::sendacross the thread boundary,interp aliasfor nested interps within the codethread. Local fast path when the calling context is the owner; bridge otherwise. Channel values cannot cross threads - the anchored instance name spec form is the handle that travels (resolved in the owning context), which the existing spec grammar already provides. - Per-console routing, not per-context. A console constructed and owned by code-interp code (e.g. an ssh-channel console opened by user code) operates locally; only operations on consoles owned elsewhere route. The brokering must compose with the settled-capability gating that already refuses queries on consoles settled as unable to respond.
Alternatives considered
- Split punk::console into an application namespace and an infrastructure namespace - rejected: both audiences need the same operations; an audience split duplicates the API surface while fixing neither state visibility nor reader contention. The split that matters is client-vs-owner transport, behind one API shape.
- Owner field on
::opunk::Console- rejected: voo object values are immutable snapshots, so an embedded owner is a cached claim that goes stale exactly when it matters (console handoff between subshells is G-002's purpose; owning threads can exit). Ownership is process topology, not a terminal fact, and the class's design deliberately avoids depending on the integrating layer; G-001's acceptance also requires the base class unchanged. The construction-time knowledge an owner field would capture is instead recorded at registration time into the punk-side registry. If a subclass ever needs to hint ownership, that is a small additive follow-up. - Ownership registry in the opunk::console package namespace (beside
instances::*) - rejected: the instance anchors are per-interp/per-thread namespace variables, but the entire point of the ownership registry is that other threads consult it - it must live in shared storage (tsv), and opunk::console deliberately carries no Thread dependency, whereas punk::console already requires Thread and has the tsv precedent (is_raw). punk::console's choke points also need ownership answers for plain channel-pair consoles in sessions where opunk::console is never loaded, so a punk-side path would exist regardless - an opunk-side registry would duplicate it. Its one real virtue, lifecycle cohesion withcreate/forget, is recovered punk-side via the integration-hook approach (see Approach) rather than by relocating the registry. - Status quo (independent module load per interp +
repl eval) - rejected: hollow twin state, competing stdin readers, andrepl evalas a permanent requirement for basic interactive exploration. - tsv-share everything, no brokering - insufficient alone: facts and caches can be shared, but channels cannot; queries, emission and raw-mode cycling still need a single owner. tsv handles the visibility half; brokering handles the operation half.
Notes
-
Acceptance verified interactively 2026-07-05 on punksys (tcl 8.6) and punk902z (tcl 9), built kits (punk::console 0.7.0, opunk::console 0.4.0; punk902z also rerun against dev modules via
srclaunch): fact parity and immediate cross-context visibility in both directions; brokered terminal queries (get_cursor_pos, dec_get_mode) from the code interp succeed and cooperate with the repl reader in raw mode with typed-ahead input preserved intact; a chan-pipe console anchored from the code interp registers its anchoring codethread as owner, routes nowhere (local operation), and clears its registry entry on forget. Test suites had already passed non-interactively (ownerrouting.test et al, baseline exec-14.3 only). -
Not blocked by G-002 - deliberately sequenced before it. A v1 with a single fixed owner (the repl thread) is definable now and is the mechanism G-002's "target a named console" criterion generalises to multiple owners. Deferred to G-002: multi-owner topology, codethread-owned consoles as first-class subshell targets, and full retirement of
repl eval. -
Raw-mode arbitration brokering and retiring the hand-written
vt52alias in favour of the general mechanism are natural extensions but are intentionally not in this goal's acceptance (a "broker complete" variant was considered and deferred). -
Deadlock consideration for
thread::sendsync shims: proven safe in the existingvt52alias pattern because the repl thread services events while the codethread executes; preserve that property (or use async+vwait) when generalising. -
The choke points listed in Context are the intended shim set; anything routed above them (the ~60 public procs) inherits brokering for free.
-
Line-mode reader cooperation - characterized interactively (punk902z tcl9 + punksys tcl8.6, 2026-07-05), no routing regression. Brokered (code-interp) and owner-side (
repl eval) queries behave identically in line mode on both runtimes, satisfying the location-transparency intent; the limitations below are properties of line mode's interaction with the query raw-window cycling (the#review-flagged line-modeinput_chunks_waitingpath in punk::repl), not artifacts of G-007 brokering, and are out of scope here:- Complete typed-ahead lines survive: consumed during the raw window, stashed to
input_chunks_waiting, recovered and executed after the loop via the line-mode waiting-chunks path (accompanied by the red "chan blocked is true" diagnostic and echo interleaving - cosmetic only, no loss). - Incomplete typed-ahead (characters pending without Enter) breaks the query cycle: the
in-flight cooked-mode read starves the raw-window response read (per-query timeouts,
~560ms), and the responses later echo outside any read window and are submitted to the
line parser as phantom input (e.g.
invalid command name 60). - On tcl 8.6 (punksys), every line-mode query initially failed the same way - owner-side included, no routing involved. Root-caused (2026-07-05, empirically against clean tclkits 8.6.13/8.6.17): 8.6 console channels expose no -inputmode configure key, so three punk::console guards misclassified a real 8.6 console as a pipe; fatally, input_at_eof's pipe-branch probe read ran on the drained console channel immediately before each query's raw cycle, making the 8.6 channel driver park a blocking cooked-mode ReadConsole (driver reads sample the console mode at issue time - a later raw flip cannot rescue them) that swallowed the query response until Enter. Fixed in punk::console 0.7.1 by treating a twapi console handle for stdin as definitive console detection at all three sites (see console-buildversion.txt). Queries issued while the repl reader is armed and idle at a line-mode prompt (e.g. from background after-scripts) remain subject to the parked-read limitation on 8.6 - raw mode, or the raw-default roadmap, covers that residue.
- Raw mode (the repl's own editor) cooperates cleanly in both directions - queries succeed and typed-ahead is preserved intact. Line mode is today's default (raw is the intended future default); fixing line-mode query cooperation would be its own goal. Related: both diagnostics observed (the query-timeout stderr message, the "chan blocked" notice) are the kind of raw diagnostic traffic G-011 gives a defined per-console home.
- Complete typed-ahead lines survive: consumed during the raw window, stashed to