1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
//! The evaluation slots: Tk hands over a script, tclrs compiles and runs it.
//!
//! This is the boundary phase 1 measured its way to. Everything Tk asked for
//! before it could be answered with a data structure; `Tcl_EvalEx(interp, "file
//! tildeexpand ~/.Xdefaults", TCL_INDEX_NONE, TCL_EVAL_GLOBAL)`
//! (`tk9.0.4/generic/tkOption.c:1592`) cannot be, and it is where the honest
//! run stopped.
//!
//! # How a script re-enters tclrs
//!
//! There is no second evaluator here and no interpretation of Tcl text by this
//! module. A script arrives as bytes, and goes through the same three stages a
//! script from the command line goes through:
//!
//! 1. [`crate::parser::parse`], through the interpreter's chunk cache, so a
//! script Tk evaluates twice is compiled once;
//! 2. [`crate::compiler`], which lowers it to a `fusevm::Chunk`;
//! 3. `crate::runtime::run_source`, which runs that chunk on a fusevm VM with
//! the same numeric hook, extension handler and JIT arming every other
//! evaluation gets.
//!
//! The value of the last command becomes the interpreter result, readable by Tk
//! through `Tcl_GetObjResult` (slot 166); a failure becomes `TCL_ERROR` with
//! the message as the result, which is Tcl's contract
//! (`generic/tclBasic.c`: the result holds the error message after a failed
//! evaluation).
//!
//! # `TCL_EVAL_GLOBAL`
//!
//! `TCL_EVAL_GLOBAL` is `0x020000` (`generic/tcl.h:985`) and means "evaluate in
//! the global namespace rather than the caller's frame". Every evaluation
//! started here *is* global: `crate::runtime::run_source` runs a chunk whose
//! variables are the interpreter's globals, and a procedure's locals live in
//! fusevm frame slots that a chunk addresses by index, so they are not
//! reachable by name from outside the chunk that declared them. The flag is
//! therefore honoured by construction and the bit is recorded rather than
//! branched on — and a script Tk evaluates without the flag from inside a tclrs
//! procedure would see the globals too, which is the one place this differs
//! from Tcl and is stated here rather than hidden.
//!
//! # The other three variadic slots
//!
//! Four of the seven are marshalled by `trampoline.c` because their variadic
//! arguments carry the payload: `Tcl_AppendStringsToObj` (15), `Tcl_Panic` (2),
//! `Tcl_ObjPrintf` (578) and `Tcl_AppendPrintfToObj` (579). The rest are not,
//! and the reason is the same for each: Tk calls them only to build text that a
//! *script* would read, and no script reads it during initialisation.
//!
//! * `Tcl_SetErrorCode` (slot 228) — sets `::errorCode`, read by `catch`
//! handlers. Tk sets one where it is about to fail
//! (`tk9.0.4/generic/tkWindow.c:2795`).
//! * `Tcl_AppendResult` (slot 70) — appends to the interpreter result, and Tk
//! uses it for error text only.
//! * `Tcl_VarEval` (slot 260) — Tk never calls it. `grep -r Tcl_VarEval` over
//! `tk9.0.4/{generic,macosx,unix,ttk}` finds no call site.
//!
//! Ignoring the variadic arguments of a function one *defines* is well formed
//! on both calling conventions this targets, because the caller lays them out
//! and tears them down; under AAPCS64 in particular they go on the stack while
//! the fixed arguments stay in registers, so reading the fixed ones is
//! unaffected. What is lost is only the text — never the control flow, and
//! never the validity of a returned object.
//!
//! The two printf slots were once on that list, on the argument that Tk's 443
//! and 93 call sites are all error wording. They are not: `wm geometry .`
//! answers with `Tcl_ObjPrintf("%dx%d+%d+%d", …)`, and `bind Button` rebuilds
//! every pattern it reports through `Tcl_AppendPrintfToObj`
//! (`tk9.0.4/generic/tkBind.c:5190,5212`). Both are ordinary results a script
//! reads, so both are marshalled.
use ;
use ;
use host;
use interp;
/// `TCL_EVAL_GLOBAL` (`generic/tcl.h:985`).
pub const TCL_EVAL_GLOBAL: c_int = 0x0002_0000;
/// Compile and run `src` in the interpreter behind `interp`, leaving the value
/// or the error message as the interpreter result.
///
/// # Safety
/// `interp` is a `Tcl_Interp *` this crate handed to Tk.
pub unsafe
/// Slot 291. `int Tcl_EvalEx(Tcl_Interp *, const char *script, Tcl_Size
/// numBytes, int flags)` — `generic/tclDecls.h:778-779`.
///
/// `numBytes` is `TCL_INDEX_NONE` (`(Tcl_Size)-1`, `generic/tcl.h:2292`) for
/// "measure it", which is what both of Tk's call sites pass
/// (`tk9.0.4/generic/tkOption.c:1592`, `tk9.0.4/generic/tkWindow.c:3508`).
///
/// `Tcl_Eval` and `Tcl_GlobalEval` are macros over this slot, with `0` and
/// `TCL_EVAL_GLOBAL` respectively (`generic/tclDecls.h:3966-3969`), so it also
/// serves both of those.
/// # Safety
/// `interp_ptr` is a `Tcl_Interp *` this crate handed to Tk, and `script` is
/// `num_bytes` readable bytes — or a NUL-terminated string when `num_bytes` is
/// negative, which is `TCL_INDEX_NONE`.
pub unsafe extern "C"
/// Slot 293. `int Tcl_EvalObjEx(Tcl_Interp *, Tcl_Obj *objPtr, int flags)` —
/// `generic/tclDecls.h:784-785`. `Tcl_EvalObj` and `Tcl_GlobalEvalObj` are
/// macros over it (`generic/tclDecls.h:4172-4174`).
///
/// Tk builds the script as a *list* — `Tcl_NewStringObj("wm geometry .")` then
/// `Tcl_ListObjAppendElement` (`tk9.0.4/generic/tkWindow.c:3446-3449`) — so the
/// value's string rep is the script, which is what this evaluates.
/// # Safety
/// `interp_ptr` is a `Tcl_Interp *` this crate handed to Tk, and `obj` is null
/// or a live `Tcl_Obj`.
pub unsafe extern "C"
/// Slot 292. `int Tcl_EvalObjv(Tcl_Interp *, Tcl_Size objc, Tcl_Obj *const
/// objv[], int flags)` — `generic/tclDecls.h:781-782`.
///
/// Unlike the other two this one carries an *already parsed* command: the words
/// are given, and no substitution is to be performed on them
/// (`generic/tclBasic.c`'s `Tcl_EvalObjv` dispatches on `objv[0]` directly).
/// So it takes the two routes a word list can take, in the order Tcl takes
/// them:
///
/// 1. `objv[0]` names a command Tk registered — call it, with these exact
/// words. No re-parse, so a word containing a space or a `$` stays one
/// word;
/// 2. otherwise, the words are joined as a Tcl list and evaluated as a script.
/// Joining quotes each word, so the split the parser performs gives back the
/// words that went in — which is why this is a faithful reproduction of the
/// invocation and not a second, looser parse.
/// # Safety
/// `interp_ptr` is a `Tcl_Interp *` this crate handed to Tk, and `objv` holds
/// `objc` live `Tcl_Obj` pointers.
pub unsafe extern "C"
// ---------------------------------------------------------------------------
// The C trampoline's two callbacks
// ---------------------------------------------------------------------------
/// What `tclrs_tk_append_strings_to_obj` in `trampoline.c` hands back: the
/// object and the NULL-terminated argument list it walked, as a counted array.
///
/// # Safety
/// Called only from that function, with `count` valid entries.
pub unsafe extern "C"
/// What `tclrs_tk_panic_trampoline` hands back: the formatted message.
///
/// `Tcl_Panic` is declared `TCL_NORETURN` (`generic/tclDecls.h:62`) and Tcl's
/// own body aborts (`generic/tclPanic.c`), so returning from here would be a
/// contract violation on top of whatever made Tk panic.
///
/// # Safety
/// Called only from that function, with a NUL-terminated buffer.
pub unsafe extern "C" !
/// What `tclrs_tk_obj_printf` hands back: the finished text, to be wrapped in
/// a value the way `Tcl_ObjPrintf` does (`generic/tclStringObj.c:2937-2943`).
///
/// The value is returned unreferenced, which is `TclNewObj`'s contract
/// (`generic/tclObj.c:1149-1151`) and what every caller of `Tcl_ObjPrintf`
/// expects: Tk hands the result straight to `Tcl_SetObjResult`, which takes the
/// first reference.
///
/// # Safety
/// Called only from that function, with `length` readable bytes at `text`.
pub unsafe extern "C"
/// What `tclrs_tk_append_printf_to_obj` hands back: the finished text, to be
/// appended the way `Tcl_AppendPrintfToObj` appends
/// (`generic/tclStringObj.c:2904-2915`, which is `Tcl_ObjPrintf`'s body over a
/// caller's value rather than a fresh one).
///
/// A NULL value is Tk's own precondition rather than this side's: every one of
/// its 93 call sites passes an object it has just built, and Tcl's body
/// dereferences without a check. It is refused here anyway, because the
/// alternative is a wild write.
///
/// # Safety
/// Called only from that function, with `length` readable bytes at `text`.
pub unsafe extern "C"
extern "C"