Author: Eric Taylor <[email protected]>
State: Draft
Type: Project
Vote: Pending
Created: 01-Sep-2026
Post-History:
Tcl-Version: 9.1
Keywords: assemble, bytecode, TAL, DSL, unsupported
Abstract
This TIP proposes promoting ::tcl::unsupported::assemble to a fully
supported, public command — ::tcl::assemble — with a documented
syntax, a documented stability contract, and a man page. No change to
the underlying assembler implementation or the TAL (Tcl Assembly
Language) syntax is proposed. The ::tcl::unsupported::assemble name
is retained as a deprecated alias for backward compatibility.
Rationale
::tcl::unsupported::assemble lets a script assemble a body of Tcl
Assembly Language text directly into a ByteCode object, using
mnemonic opcodes (load, store, add, invokeStk, and so on)
while the assembler itself resolves operand encoding and Local
Variable Table (LVT) slot allocation.
When used optimally, by placing assemble commands in a proc or method
body, the bytecode is the same as any compileable command in a code
body, such as expr. However, assemble also can do one-off assemble
and execute steps. For toplevel code used once, this can be a
satisfactory trade-off.
This puts genuine expr-level performance within reach of pure-Tcl
DSL authors, without requiring a C extension or any change to Tcl's
C-level bytecode-compiler API — assemble is, in effect, the same
bytecode-emission step expr already uses internally, made available
as a public command that accepts a different, and extensible, surface
syntax.
However, this does not require the caller to be pure Tcl. Nothing
about assemble prevents a C extension from doing its own lexing and
parsing of some other DSL syntax, entirely in C, and emitting TAL
text as the result.
A C extension can go further and construct the text of a complete
proc name args body command, with assemble calls embedded in the
body, and hand that whole text to Tcl_Eval/Tcl_EvalObjEx.
This means a public assemble benefits C-extension DSL authors as
much as pure-Tcl ones: they gain a stable, public path to
bytecode-level performance for their own DSL syntax without needing
to touch Tcl's internal compiler structures (ByteCode, CompileEnv,
and so on) directly.
Motivation
Assemble already works, and has for some time. The :/Calc
extension in the Reference Implementation has kept Assemble working
across Tcl 8.6 and 9.x. Two opcodes the extension relied on were
removed in 9.x; replacing them with branching TAL required no core
changes. This TIP does not add a new capability to Assemble — it
only formalizes one already relied upon.
Assemble is the cleanest available foundation for fast, pure-Tcl
DSLs. Several core-level expression-syntax proposals have surfaced
over time — an = command, a $(...) shorthand, TIP 759's parser and
expr modifications — all requiring a TIP. An = command TIP has
already been voted down, suggesting core-level expression syntax
faces a genuinely difficult path to adoption.
A public, stable Assemble sidesteps that difficulty, giving DSL authors, present and future, a path to bytecode-level performance with no core parser change and no dedicated TIP per syntax. One TIP buys an open platform rather than enshrining one surface syntax.
Assemble being unsupported is currently a barrier to responsible
use. Because the command lives under the ::tcl::unsupported
namespace, authors have no basis for treating Assemble as safe to
depend on in production, even though Assemble's behavior has been
stable. Formalizing Assemble removes that barrier with no
implementation work required.
Specification
The command currently registered as
::tcl::unsupported::assembleis additionally registered as::tcl::assemble. The implementation, accepted TAL syntax, and opcode set are unchanged by this TIP.::tcl::unsupported::assemblecontinues to work, as a deprecated alias for::tcl::assemble, for at least one major release cycle, to avoid breaking existing users of the unsupported name.A man page is added documenting:
- The command's syntax and general semantics (a body of TAL text in, a compiled result usable as a command body out).
- An initial supported opcode set, starting with the opcodes that
proved useful in a real-world example — for instance, the
:/Calcreference implementation described below — along with any differences between Tcl 8.6 and 9.x codebases affecting them. - Opcodes not yet included in this set remain accepted by
assemble, but are not covered by the stability commitment in item 4 until documented; documented coverage can be extended over time as further opcodes see real use, without requiring a new TIP unless it changes the semantics of an opcode already documented. - The intended primary usage pattern: invocation from within a
procor method body, whereassembleresolves LVT-addressed opcodes (load,store, and so on) against the caller's own local variables.
A stability commitment is made: the TAL syntax and the semantics of documented opcodes will not change or be removed without a subsequent TIP and a deprecation cycle. New opcodes, and documentation for existing but not-yet-documented opcodes, may be added as Tcl's bytecode instruction set grows and as further opcodes see real use.
No changes are proposed to
Tcl_Obj,ByteCode, or any other internal C structures, and no new C-level public API is introduced by this TIP. This is a namespace/documentation/ stability-contract change only, which keeps its implementation cost and review surface minimal.
Non-Goals
- This TIP does not change the TAL syntax, the opcode set, or any
other aspect of how
assemblecurrently works. It promotes the existing command to supported status, as-is. - This TIP does not propose or endorse any particular expression
shorthand syntax (
=,$(...),[(...)], or otherwise). Whether and how any of those proceed is left to their own TIPs. - This TIP does not require every opcode
assemblecurrently accepts to be documented up front. See item 3 of the Specification.
Backward Compatibility
Existing code using ::tcl::unsupported::assemble continues to work
unchanged, since that name remains available as an alias. Code
written against ::tcl::assemble gains no new capability beyond
naming and a documented support contract, so no compatibility risk is
introduced for existing scripts either way.
Reference Implementation
None required beyond the namespace-alias registration and
documentation described above; the assembler itself is unchanged.
A worked example of the kind of pure-Tcl DSL this TIP is intended to
support — a : expression-evaluator extension (referred to elsewhere
as Calc) using ::tcl::unsupported::assemble today, portable
across 8.6 and 9.x — is available at
https://github.com/rocketship88/colin-parser.
Example
The following, using that extension (and recoding the list example from TIP 759), illustrates the kind of
design freedom a pure-Tcl DSL built on assemble has, compared to a
core-level syntax change - an assignment operator, multiple
;-separated statements in a single call, bare variable names not using $, and built-in functions
list and llength, that map directly onto the list n and listLength bytecode
instructions respectively.
The nested list-of-lists construction can be seen in the
result comment:
: {n = 10;
mylist = list (
list(
list(2*n + 1, 2*n + 3) ,
list(2*n - 1, 2*n + 1)
) ,
list(
list(2*n + 1, 2*n - 1) ,
list(2*n + 3, 2*n + 1)
)
) ;
len = llength(mylist)
}
puts "n= $n mylist= { $mylist } len= $len "
# n= 10 mylist= { {{21 23} {19 21}} {{21 19} {23 21}} } len= 2
Note the command name here is :, not =, even though the DSL
itself allows either. This was a choice because when
written as = n = 10, the leading = reads
as though something is missing to its left; : n = 10 does not carry
that ambiguity.
This is a personal naming choice. Each author is free to choose their own command names, operators, and terseness/readability trade-offs, without needing a TIP, parser change, or coordination with any other DSL author's choices.
Amortizing the assembly cost: a proc= preprocessor
A DSL author can avoid paying the TAL parsing cost on every call by
assembling once, ahead of time, rather than per invocation. One
approach: a proc= wrapper that, at definition time, transforms a
proc body's DSL expressions into TAL, assembles them, and installs
the result as the actual proc body:
proc= myProc {x y} {
set result [: x*sin(y)]
...
}
The same effect can be reached without any new command at all, using
info procs/info body to rewrite chosen procs (or every proc in a
namespace) after the fact, in place.
Neither approach requires any change to Tcl beyond assemble itself
being public — both are ordinary Tcl code, mentioned here only to
show the amortization options a pure-Tcl DSL already has available.
Copyright
This document has been placed in the public domain.
