rhai 1.26.0

Embedded scripting for Rust
Documentation
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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
use crate::tokenizer::Token;

/// What `x op= y` needs to reproduce Rhai's resolution order.
///
/// Both the op-assignment and the plain operator are carried, because Rhai
/// tries the first and falls back to expanding into the second when no
/// op-assignment implementation exists (`eval/stmt.rs:217-236`).
///
/// Lives in the program's op-assignment pool rather than in the instruction:
/// four fields including two `Token`s do not fit an operand, and the same
/// `+=` used in ten places is one entry.
// No `Eq`: `Token` carries float literals, so it is only `PartialEq`.
#[derive(Debug, Clone, PartialEq)]
pub struct AssignOp {
    /// The `+=` token, for the built-in lookup.
    pub op_assign: Token,
    /// `"+="`, for dispatch and for error messages.
    pub op_assign_name: u32,
    /// The `+` token, for the expansion.
    pub op: Token,
    /// `"+"`.
    pub op_name: u32,
}

/// Where [`Op::CallRef`] finds the variable it calls through.
///
/// The two differ in how the variable is reached, not in what happens to it:
/// both take a reference where Rhai would and fall back to a value where it
/// would not, by the same rule.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Receiver {
    /// A local, addressed by slot. Nothing was pushed for it — the call reads
    /// the scope entry itself, and a slot always names one.
    Local(u16),

    /// A variable no slot addresses: the caller's, a module's, or nothing.
    ///
    /// [`Op::LoadNamed`] has already resolved the name and left its value as
    /// argument zero, which is what raises `ErrorVariableNotFound` against the
    /// variable rather than against the call — two positions the table cannot
    /// give one instruction. The call re-reaches the scope entry for the
    /// reference and falls back to that value when there is no entry to reach:
    /// a resolver's answer, a module's constant, a `const`.
    ///
    /// So the by-reference path pays for a clone it discards. Worth removing
    /// only if a profile of a host-heavy script says so; a local, which is the
    /// common receiver by far, never makes one.
    Named(u32),

    /// The frame's receiver, for `f(this, ..)`.
    ///
    /// Rhai applies the same rewrite to `this` as to a variable, but only when
    /// the receiver is neither shared nor curried (`func/call.rs:1409-1433`).
    /// Shared-ness is a run-time property, so the value arrives on the stack as
    /// argument zero and the call reaches for the register instead when it turns
    /// out to be usable by reference — the deferral [`Receiver::Local`] already
    /// makes for a read-only entry.
    ///
    /// Unlike either of the others, [`Op::LoadThis`] pushes it *before* the
    /// remaining arguments. Rhai's two arms disagree about when `this` is read:
    /// the by-reference one takes it after them (`func/call.rs:1417`), but the
    /// fallback that a shared or unbound receiver lands in reads and flattens it
    /// first (`:1462`). Reading first is what makes `f(this, { this = 9; 1 })`
    /// pass the pre-mutation value, and an unbound `f(this, no_such)` report
    /// `ErrorUnboundThis` rather than `ErrorVariableNotFound`.
    This,
}

/// One VM instruction, as the compiler emits it and a disassembly shows it.
///
/// **Not the executed form.** A program's code is a byte slice, assembled from
/// these by [`assemble`](crate::grain::bytecode::assemble) and dispatched on
/// directly, so a loaded program can borrow its instructions from the artifact
/// rather than building sixteen bytes of enum per instruction. See
/// [`code`](crate::grain::bytecode::code) for the encoding.
///
/// A stack machine: operands are pushed and consumed on an operand stack, and
/// locals live in slots addressed directly. `EvalAst` is the escape hatch that
/// hands a fragment back to Rhai's tree walker, so anything the compiler cannot
/// yet lower still runs, and the whole language stays covered. Lowering more of
/// it converts residuals into instructions rather than adding coverage.
///
/// Instructions carry no source position. Several of them can fail against a
/// place in the source, and the position for that comes from the program's
/// [`Positions`](crate::grain::bytecode::Positions) table, keyed on the instruction's
/// own address. Keeping it out means the diagnostics can be stripped from an
/// artifact without touching the code.
///
/// Anything too wide for an operand is a `u32` index into one of the program's
/// pools, which is also what keeps a repeated operator or name from being
/// stored twice.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Op {
    /// Push constant `.0` from the pool.
    Const(u32),
    /// Push unit.
    Unit,
    /// Push a boolean.
    Bool(bool),

    /// Push the value in local slot `.0`.
    LoadLocal(u16),
    /// Pop and write into local slot `.0`, which must already exist.
    StoreLocal {
        /// The slot index
        slot: u16,
        /// Whether the value should be stored as a constant.
        is_const: bool,
    },

    /// Push the value of the variable named `.0`, found by name.
    ///
    /// For the variables no slot can address: the ones the caller already had
    /// in its `Scope` when the program started, which sit below the base every
    /// slot is measured from. Without this, a script that reads anything its
    /// host supplied is a fragment, and so cannot be written to an artifact at
    /// all.
    ///
    /// Three places are searched, in Rhai's order (`eval/expr.rs:107-155`):
    /// the resolver a host may have registered with `Engine::on_var`, then the
    /// scope, then the modules loaded into the global namespace. Missing from
    /// all three is `ErrorVariableNotFound`.
    ///
    /// A reverse scan of the scope per read, where a slot is an index — which
    /// is why the compiler only emits this for a name it could not resolve.
    LoadNamed(u32),

    /// Pop a value and assign it to the variable named `name`, optionally
    /// through an operator.
    ///
    /// [`Op::LoadNamed`]'s counterpart, and resolved the same way. Assigning
    /// to anything that is not a scope entry it can take a reference to — a
    /// value the resolver produced, a module's constant, a `const` — is
    /// `ErrorAssignmentToConstant`, as it is in the walker.
    AssignNamed {
        /// The name of the variable
        name: u32,
        /// Index into the op-assignment pool; absent for a plain `=`.
        op: Option<u32>,
    },

    /// Pop a value and assign it to local slot `slot`, optionally through an
    /// operator.
    ///
    /// Separate from `StoreLocal` because `x += y` is not `x = x + y`: Rhai
    /// looks for an op-assignment implementation that mutates in place, and
    /// only expands to the binary form if there is none.
    AssignLocal {
        /// The slot index
        slot: u16,
        /// Names the variable in `ErrorAssignmentToConstant`.
        var_name: u32,
        /// Index into the op-assignment pool; absent for a plain `=`.
        op: Option<u32>,
    },
    /// Pop and declare it as a new local, extending the scope by one.
    ///
    /// Slots are assigned in declaration order, so the new local always lands
    /// at the top of the scope. Carries the name because locals live in the
    /// caller's `Scope`, where entries are named, and carries const-ness
    /// because Rhai enforces it through the value's own access mode.
    DeclareLocal {
        /// The name of the variable
        name: u32,
        /// Whether the variable is declared `const`.
        is_const: bool,
    },

    /// Discard the top of the operand stack.
    Pop,

    /// Jump to `.0`.
    ///
    /// An instruction index as the compiler emits it, a byte offset once
    /// assembled — instructions vary in length, so there is nothing else it
    /// could be.
    Jump(u32),
    /// Pop a condition and jump to `.0` if it is true. Mirrors
    /// [`Op::JumpIfFalse`]; both exist so short-circuit `&&` and `||` lower
    /// without an extra negation.
    JumpIfTrue {
        /// Where to jump to
        target: u32,
    },
    /// Pop a condition and jump to `.0` if it is false.
    ///
    /// Its position-table entry is the condition's own position, because Rhai
    /// rejects a non-boolean guard against the guard expression rather than the
    /// statement — and the differential harness compares error positions.
    JumpIfFalse {
        /// Where to jump to
        target: u32,
    },
    /// Inspects a condition and jump to `.0` if it is not `()`.
    /// The condition is not popped, so the caller can read it afterwards.
    /// Mirrors [`Op::JumpIfFalse`]; exists so short-circuit `??` lower.
    SkipIfNotUnit {
        /// Where to skip to
        target: u32,
    },

    /// Pop `argc` arguments and call the function named by `name`, pushing the
    /// result.
    ///
    /// Dispatch goes through Rhai, so every registered function, operator and
    /// script function resolves exactly as it would in the walker. Only calls
    /// Rhai handles syntactically before dispatch — `Fn`, `call`, `curry`,
    /// `eval`, `is_def_var` — are excluded.
    ///
    /// The position table's entry for this instruction is the call site. Rhai's
    /// dispatch path takes one and reports failures against it; `call_fn_raw`
    /// does not, so an error that comes back without a position gets this one.
    ///
    /// `op` indexes the operator pool when the call is an operator, and names
    /// the token the built-in lookup keys on. The walker short-circuits these
    /// to a function pointer rather than dispatching, and a VM that did not
    /// would be slower than the tree it replaced.
    Call {
        /// The name of the function
        name: u32,
        /// How many arguments to pop
        argc: u8,
        /// Index into the operator pool; absent unless the call is an operator.
        op: Option<u32>,
        /// This call captures the parent's scope.
        capture_parent_scope: bool,
    },

    /// Call `name` with a variable as its first argument, taken by reference.
    ///
    /// Rhai rewrites `f(x, ..)` into `x.f(..)` whenever the first argument is a
    /// plain variable, so that a `&mut` first parameter mutates the variable
    /// rather than a copy (`func/call.rs:1434-1460`). `push(a, 2)` and
    /// `a.push(2)` are the same call; only the second reached the mutation
    /// through [`Op::Chain`].
    ///
    /// Two things follow from the rewrite, and together they are why this is an
    /// instruction rather than an argument order:
    ///
    /// * the variable is read *after* the other arguments, so an argument that
    ///   writes to it is seen;
    /// * a shared or read-only variable is passed by value instead — Rhai hands
    ///   out a reference to neither (`func/call.rs:1449-1454`).
    ///
    /// Operators never reach here: under `fast_operators` a binary one
    /// short-circuits before the rewrite (`func/call.rs:1775`), so `a + b`
    /// reads `a` first and needs no reference.
    CallRef {
        /// The name of the function
        name: u32,
        /// How many arguments to pop, not counting the receiver.
        argc: u8,
        /// Where the first argument is found.
        receiver: Receiver,
        /// This call captures the parent's scope.
        capture_parent_scope: bool,
    },

    /// Move the top of the operand stack down past `.0` values.
    ///
    /// A [`Receiver::Named`] receiver is resolved by [`Op::LoadNamed`] after
    /// the other arguments, and this puts it back in argument order.
    Rotate(u8),

    /// Pop a subject and jump to wherever switch table `.0` sends it.
    ///
    /// Always jumps — the table's default is where a subject that matches
    /// nothing goes, and an absent `_` arm compiles to a jump past the
    /// statement. Arms with guards are not table entries: the table sends a
    /// subject to the head of a chain that tries each guard in source order
    /// and falls through to the default, which is what keeps dispatch a
    /// lookup.
    ///
    /// The table is in the program's switch pool rather than in the
    /// instruction because it is unbounded, and because two arms of one
    /// `switch` share it.
    Switch(u32),

    /// Turn local slot `.0` into a shared cell, so a closure can capture it.
    ///
    /// Rhai's parser emits one of these per captured variable ahead of the
    /// `curry` call that binds them (`parser.rs:3707`). Sharing is what makes
    /// the closure and the enclosing scope see the same value afterwards; the
    /// write-through in `place` is the other half.
    ///
    /// The variable resolver gets first refusal, as it does in
    /// `eval/stmt.rs:998`: if a host's `on_var` answers the name, the variable
    /// is *not* shared.
    Share(u16),

    /// The same for a variable no slot names — one the caller supplied.
    ShareNamed(u32),

    /// Push local slot `.0` without flattening it.
    ///
    /// A read normally hands back what a shared cell contains, which is right
    /// for a value and wrong for a capture: currying a closure has to bind the
    /// *cell*, or the closure gets a copy and stops being a closure.
    LoadShared(u16),

    /// The same for a variable no slot names — one the caller supplied.
    ///
    /// [`Op::LoadNamed`] flattens, and a closure capturing a caller's variable
    /// through that read binds a copy: the aliasing is dead, and a write to the
    /// variable afterwards is invisible to the closure. The slot case has had
    /// [`Op::LoadShared`] since closures were lowered at all; this is the half
    /// that was missing.
    LoadSharedNamed(u32),

    /// Push the receiver bound to the running frame.
    ///
    /// `this` is not a scope entry and no slot addresses it: Rhai threads it
    /// through evaluation as a parameter (`func/script.rs:29`) and keeps it out
    /// of the `Scope` altogether. So it gets a register of its own, and these
    /// four instructions are the only things that reach it.
    ///
    /// Flattens, as [`Op::LoadLocal`] does. Rhai reads `this` *unflattened*
    /// (`eval/expr.rs:272`) but flattens at almost every consumer — a `let`
    /// (`eval/stmt.rs:436`), an assignment's right-hand side (`:321`), a call's
    /// arguments (`func/call.rs:1428`) — so the flattening read is the common
    /// one and [`Op::LoadThisShared`] is the exception, exactly as it is for a
    /// local.
    ///
    /// `ErrorUnboundThis` when the frame has no receiver.
    LoadThis,

    /// The same without flattening.
    ///
    /// [`Op::LoadShared`]'s counterpart, for the three readers that have to see
    /// the cell rather than what it holds: a `switch` subject, `is_shared`, and
    /// a curried capture.
    LoadThisShared,

    /// Raise `ErrorUnboundThis` if the frame has no receiver. Pushes nothing.
    ///
    /// `this = v` checks *before* it evaluates `v` (`eval/stmt.rs:299-302`),
    /// unlike the variable arm, which evaluates the value first (`:319-323`).
    /// Without a check of its own, `this = no_such` in an unbound frame would
    /// report `ErrorVariableNotFound` where Rhai reports `ErrorUnboundThis`.
    RequireThis,

    /// Pop a value and assign it to the frame's receiver, optionally through an
    /// operator.
    ///
    /// [`Op::AssignLocal`] without the slot or the name, because `this` has
    /// neither — and neither does Rhai's own failure here: assigning to a
    /// read-only receiver is `ErrorAssignmentToConstant("")`
    /// (`eval/stmt.rs:118-122`), named for an expression that has no name.
    AssignThis {
        /// Index into the op-assignment pool; absent for a plain `=`.
        op: Option<u32>,
    },

    /// Pop a value and push whether it is a shared cell.
    ///
    /// Rhai answers `is_shared` before dispatch and registers no function for
    /// it (`func/call.rs:1240`), so there is nothing to call — and the value
    /// has to arrive unflattened, or the answer is always false.
    IsShared,

    /// Push a function pointer to the compiled function named `.0`.
    ///
    /// A closure's, whose name the parser makes up (`anon$…`) and which
    /// [`Op::MakeFnPtr`] would refuse — Rhai only builds pointers to names a
    /// script could have written. The name is known here, so unlike
    /// `MakeFnPtr` it needs no operand on the stack.
    MakeClosure(u32),

    /// Pop a name and push a function pointer to it.
    ///
    /// Deliberately the *late-bound* kind, carrying a name and nothing else.
    /// Rhai's other kind embeds a `ScriptFuncDef` — an AST body — which is
    /// both unreachable from outside the crate and exactly the allocation this
    /// project exists to remove. Building our own means a pointer resolves
    /// through the compiled function table like any other call, and a program
    /// holding one can still be written to an artifact.
    ///
    /// The cost is that a `Normal` pointer is late-bound where Rhai's is
    /// early-bound: redefining the function after taking a pointer to it is
    /// visible here and not in the walker.
    MakeFnPtr,

    /// Pop `.0` arguments and a function pointer, and push the pointer with
    /// those arguments bound to the front of it.
    Curry(u8),

    /// Pop `argc` arguments and a target, and call a function pointer.
    ///
    /// A compiled function of that name and arity is called directly, with the
    /// curried arguments spliced in front. Anything else — a native function,
    /// a name that resolves elsewhere — goes to Rhai's own `call_raw`.
    ///
    /// `method` distinguishes `f.call(x)` from `call(f, x)`, which are not the
    /// same call. In method position a target that is *not* a pointer is not
    /// an error: Rhai takes the first argument as the pointer and binds the
    /// target as `this` (`func/call.rs:816-919`), which is how a closure is
    /// called against a receiver.
    CallFnPtr {
        /// How many arguments to pop
        argc: u8,
        /// Whether the call is in method position (`f.call(x)`).
        method: bool,
        /// Where the receiver came from, when there is anywhere to put it back.
        ///
        /// `obj.call(f)` binds `obj` as the closure's `this` **by reference**
        /// (`func/call.rs:862`), so a closure that writes to `this` writes to
        /// `obj`. The receiver's *value* is on the operand stack either way —
        /// this only says where it came from, so the write can be carried back
        /// there.
        ///
        /// `None` in call position, and for a receiver with nowhere to write
        /// back to: `[1, 2].call(f)` mutates a temporary, as it does in Rhai.
        /// Only meaningful when `method` is set.
        receiver: Option<Receiver>,
    },

    /// Push an empty buffer for an interpolated string to be built in.
    ///
    /// Interpolation is three instructions rather than one because Rhai checks
    /// the size limit after **every** segment and blames the segment that went
    /// over. One instruction has one position-table entry, so it could not say
    /// which; a pool of per-segment positions would say it but would not be
    /// strippable, and diagnostics staying separable is the point of the
    /// table. An instruction per segment puts each position exactly where the
    /// rest of them live.
    ///
    /// The buffer is an ordinary operand, so a nested interpolation needs
    /// nothing special.
    InterpolateStart,

    /// Pop a segment and append it to the buffer beneath it.
    ///
    /// Not `+`, which is what it looks like: `+` is overridable and
    /// interpolation is not, and the string-plus-anything operator skips this
    /// size check. A string segment is written straight out and never reaches
    /// dispatch; anything else goes through Rhai's `to_string` rendering,
    /// which consults native functions only.
    InterpolateAppend,

    /// Replace the buffer with the interned string it built.
    InterpolateEnd,

    /// Pop `.0` values and push them as an array.
    ///
    /// Only for a literal whose elements are not all constant — one that is
    /// gets folded into the pool by Rhai's own optimizer before this sees it.
    MakeArray(u16),

    /// Build a map from a template and `n` key/value pairs above it.
    ///
    /// The stack holds `[template, k0, v0, .., k(n-1), v(n-1)]`. The template
    /// is a constant map that already carries every key the literal mentions,
    /// with the computed ones holding a placeholder — that is Rhai's own shape
    /// (`ast/expr.rs:283`), and it is why an entirely constant map never
    /// reaches here: the optimizer has already folded it into the template
    /// alone.
    ///
    /// Keys ride on the operand stack as string constants rather than in a
    /// pool of their own. They are constants either way, and the constant pool
    /// already deduplicates them across the program.
    MakeMap(u16),

    /// Measure the value on top of the stack into the array literal being
    /// built, and raise `ErrorDataTooLarge` if the running total is over.
    ///
    /// The operand is the element's index within its literal: zero starts a
    /// fresh total, and [`Op::MakeArray`] discards it. That is what keeps
    /// `[a, [b, c], d]` straight — the inner literal's total is pushed and
    /// popped inside the outer one's.
    ///
    /// A separate instruction rather than work inside `MakeArray` because Rhai
    /// blames the *element* that tipped the total over
    /// (`eval/expr.rs:328`), and one instruction has one position-table entry.
    /// Putting it here rather than in a pool beside the element count is what
    /// keeps those positions strippable, which matters more for a literal than
    /// for a chain: an array can have any number of elements.
    CheckSize {
        /// The element's index within its literal
        index: u16,
        /// Whether the element counts towards the map limit rather than the
        /// array one. Rhai adds one to a different member of the triple for
        /// each (`eval/expr.rs:323` against `:354`), so the same running total
        /// cannot serve both.
        map: bool,
    },

    /// Walk `a.b[i].c`, indexing the chain pool.
    ///
    /// One instruction for the whole chain rather than one per step, because
    /// the walk holds a `&mut` into the container at every level and a borrow
    /// cannot survive a trip round the dispatch loop. Index values and method
    /// arguments were pushed before it, in step order.
    ///
    /// Pushes the value for a read, or unit for an assignment.
    Chain(u32),

    /// Truncate the scope back to `.0` locals, dropping everything a block
    /// declared. The compile-time slot model unwinds in step.
    UnwindTo(u16),

    /// Count one operation against `max_operations`, and give `on_progress` a
    /// chance to terminate.
    ///
    /// Emitted on loop back-edges. Rhai ticks per AST node, so counts differ;
    /// what this preserves is that a limit is enforced and an interrupt is
    /// honoured, which is what allows `loop {}` to be killed.
    ///
    /// Its table entry is read on every iteration rather than only on failure,
    /// which is why the in-memory position table is dense.
    Tick,

    /// Record the current scope length as the depth an error escaping this
    /// chunk unwinds to.
    ///
    /// Rhai rewinds a nested block whether it is left normally or by a throw,
    /// and never rewinds the top level of a chunk (`eval/stmt.rs`, and
    /// `eval_global_statements` passing `rewind_scope = false`). The normal
    /// path is [`Op::UnwindTo`], which an escaping error jumps straight past —
    /// so the frame needs a floor to fall back to, and the last top-level
    /// statement boundary is exactly it.
    ///
    /// Emitted once before each top-level statement of a chunk that runs in the
    /// caller's scope, so it costs nothing per iteration and nothing at all to
    /// a function body, whose scope is discarded whole.
    Checkpoint,

    /// A statement begins here, at nesting `depth` within its chunk.
    ///
    /// Where the debugger stops. Rhai runs its callback per AST node
    /// (`eval/stmt.rs:269`) and a chunk has no nodes, so the compiler records
    /// where the statements were and the VM stops there instead. Without this
    /// there is nothing to stop at, and break-points and stepping are inert.
    ///
    /// `depth` is how many statements enclose this one. It is what stepping
    /// re-arms against: Rhai restores the stepping state when the statement it
    /// was asked at *ends* (`eval/stmt.rs:271`), and the next marker at the
    /// same depth or shallower is where that has happened. Without it, a
    /// `next` over an `if` would step into its body.
    ///
    /// Emitted only under `debugging`. An instruction per statement is not
    /// worth carrying to a device with no callback to call, so a shipping build
    /// has none. Decoding is unconditional, so an artifact written by a
    /// debugging build still runs anywhere — it simply cannot be stopped.
    Statement {
        /// How many statements enclose this one.
        depth: u16,
    },

    /// Evaluate residual AST fragment `residual` through Rhai's walker,
    /// pushing its value.
    ///
    /// `rewind_scope` reaches `eval_stmt_block` when the fragment is a block,
    /// and decides whether locals it declares survive. Statement fragments
    /// rewind, so they cannot disturb the scope shape slots were resolved
    /// against. A whole-program fragment does not, because Rhai does not
    /// rewind top-level statements and callers can see what they declared.
    EvalAst {
        /// Index into the residual pool
        residual: u32,
        /// Whether locals the fragment declares are discarded afterwards.
        rewind_scope: bool,
    },

    /// Arm a handler covering the instructions up to the matching
    /// [`Op::PopHandler`], catching to `target`.
    ///
    /// Records where the operand stack, the scope and the iterator stack were
    /// when it was armed, because an error can be raised at any depth of all
    /// three and the catch block has to start where the `try` did.
    ///
    /// Only errors Rhai considers catchable are caught: `return`, `break`,
    /// `continue` and `exit` unwind as errors too and must pass straight
    /// through (`eval/stmt.rs:806`).
    ///
    /// `catch_var` names the variable the error is bound to. Its table entry
    /// is that variable's position, which is what Rhai reports
    /// `ErrorTooManyVariables` against.
    PushHandler {
        /// Where to jump to when an error is caught.
        target: u32,
        /// The name the error is bound to; absent for a bare `catch`.
        catch_var: Option<u32>,
    },

    /// Disarm the innermost handler.
    ///
    /// Emitted twice per `try`: once where the body ends normally, and once
    /// where the catch block does — the second ends the region in which a
    /// bare `throw;` means "re-raise the original".
    PopHandler,

    /// Pop an iterable and start iterating it.
    ///
    /// The iterator goes on a stack of the VM's own rather than the operand
    /// stack, because it is not a `Dynamic`. Rhai's iterator functions take
    /// the iterable **by value** and hand back something that cannot be
    /// re-created, so it is made once here and lives until the loop ends.
    ///
    /// Its table entry is the iterable's *start* position, which is what
    /// `ErrorFor` is reported against (`eval/stmt.rs:703`) — a different
    /// position from the one [`Op::IterNext`] uses.
    IterInit,

    /// Advance the current iterator: push the next item and fall through, or
    /// drop the iterator and jump to `exit`.
    ///
    /// The only instruction whose two edges leave different amounts on the
    /// operand stack, which is why the verifier gives it explicit successors.
    ///
    /// Its table entry is the iterable's position — `position`, not
    /// `start_position` — because that is what a fallible iterator's error is
    /// filled in with (`eval/stmt.rs:749`).
    IterNext {
        /// Where to jump to once the iterator is exhausted.
        exit: u32,
        /// `for (x, i) in seq`: the count is pushed under the item, so the two
        /// `StoreShared`s that follow pop them in declaration order.
        indexed: bool,
    },

    /// Discard the current iterator.
    ///
    /// Emitted where a `break` leaves a loop, since the jump skips the
    /// [`Op::IterNext`] that would have dropped it on exhaustion. Leaving a
    /// frame drops whatever it left behind without this.
    IterDrop,

    /// Pop a value and write it into local slot `.0`, through a shared cell
    /// rather than over it.
    ///
    /// Distinct from [`Op::StoreLocal`] only in intent: the `for` loop
    /// variable is written once per iteration and a closure in the body may
    /// have shared it, in which case Rhai writes into the cell and every
    /// closure made in the loop sees the last value (`eval/stmt.rs:752`).
    StoreShared(u16),

    /// Pop a value and raise it as a `throw`.
    ///
    /// Always fails, with `ErrorRuntime` carrying the value — Rhai wraps
    /// nothing and converts nothing, so any type can be thrown. Its table
    /// entry is the `throw` keyword's own position, not the expression's
    /// (`eval/stmt.rs:877`).
    Throw,

    /// End the chunk, yielding the top of the operand stack, or unit if empty.
    Return,
}