TIP 760: Promote assemble to a Supported Command

Login
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

  1. The command currently registered as ::tcl::unsupported::assemble is additionally registered as ::tcl::assemble. The implementation, accepted TAL syntax, and opcode set are unchanged by this TIP.

  2. ::tcl::unsupported::assemble continues 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.

  3. 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 :/Calc reference 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 proc or method body, where assemble resolves LVT-addressed opcodes (load, store, and so on) against the caller's own local variables.
  4. 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.

  5. 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

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.