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
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
use crate::grain::bytecode::code::{self, tag};
use crate::grain::bytecode::{Chain, Chunk, Op, Receiver, Root, Step, Switch, Tail};
use crate::grain::format::Caps;
use crate::grain::program::Function;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;

/// What the pools hold, so an instruction's indices can be checked against
/// something.
///
/// Chains and switches come through whole rather than as a count, because both
/// hold things that have to be checked rather than counted: how much operand
/// stack a chain consumes, and where a switch can send control.
#[derive(Debug, Clone)]
pub struct Pools<'a> {
    /// How many constants there are.
    pub consts: usize,
    /// How many interned names there are.
    pub names: usize,
    /// How many operator tokens there are.
    pub tokens: usize,
    /// How many op-assignments there are.
    pub assign_ops: usize,
    /// How many residual AST fragments there are.
    pub residuals: usize,
    /// The chain pool.
    pub chains: &'a [Chain],
    /// The switch pool.
    pub switches: &'a [Switch],
}

/// Why a chunk was rejected.
///
/// Every variant names something a correct compiler cannot produce, so a
/// failure here is a bug in the compiler or a corrupted artifact — never
/// anything a script can express.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyError {
    /// An instruction requires capabilities which is unavailable.
    MissingCaps {
        /// Artifact declared capabilities
        artifact: String,
        /// Missing capabilities
        missing: String,
        /// Byte offset of the offending tag
        at: usize,
    },
    /// A tag with no instruction behind it, or one whose operands run past the
    /// end of the chunk.
    Undecodable {
        /// Byte offset of the offending tag
        at: usize,
    },
    /// The last instruction stops short of the end, so the trailing bytes are
    /// not instructions.
    TrailingBytes {
        /// Where the trailing bytes start
        at: usize,
        /// How long the code is
        len: usize,
    },
    /// A chunk names a span the code does not have.
    ChunkOutOfRange {
        /// The chunk's first byte offset
        entry: u32,
        /// One past its last
        end: u32,
        /// How long the code is
        len: usize,
    },
    /// A jump leaves the chunk it is in — including into another chunk, which
    /// would run that function's instructions against this frame's locals.
    JumpOutOfRange {
        /// Byte offset of the jump
        at: usize,
        /// The byte offset it names
        target: u32,
    },
    /// A jump lands inside an instruction rather than on one. Decoding from
    /// there would read an operand's bytes as a tag.
    JumpIntoAnInstruction {
        /// Byte offset of the jump
        at: usize,
        /// The byte offset it names
        target: u32,
    },
    /// Two paths reach the same instruction with different stack depths, so
    /// the depth at that point is not a static property.
    DepthConflict {
        /// Byte offset of the instruction
        at: usize,
        /// The depth already recorded for it
        expected: usize,
        /// The depth the other path arrives with
        found: usize,
    },
    /// An instruction pops more than is on the stack.
    Underflow {
        /// Byte offset of the instruction
        at: usize,
        /// How many values it pops
        need: usize,
        /// How many are on the stack
        have: usize,
    },
    /// Execution can run past the last instruction.
    FallsOffTheEnd,
    /// The chunk claims less stack than it uses.
    StackExceedsDeclared {
        /// The depth actually reached
        needed: usize,
        /// The depth the chunk declares
        declared: u16,
    },
    /// An iterator is dropped where none was made. The compiler pairs these
    /// up lexically; an artifact off a wire has to be asked.
    IteratorUnderflow {
        /// Byte offset of the instruction
        at: usize,
    },
    /// A handler is disarmed where none was armed. A stale handler is worse
    /// than a missing one: the next unrelated error would be caught into an
    /// already-exited `catch` block.
    HandlerUnderflow {
        /// Byte offset of the instruction
        at: usize,
    },
    /// An index into a pool with nothing behind it.
    BadIndex {
        /// Byte offset of the instruction
        at: usize,
        /// Which pool was indexed
        what: &'static str,
        /// The index it used
        index: u32,
    },
}

/// Check that a chunk is internally consistent before running it.
///
/// Two passes. The first decodes straight through, recording where each
/// instruction starts; that is what makes it safe to say a jump target is or is
/// not an instruction, which a reachability walk alone cannot — a jump into the
/// middle of an operand would decode the operand's bytes as a tag and look
/// perfectly reasonable.
///
/// The second is an abstract interpretation over stack depth: walk every
/// reachable instruction, and require that all paths into one agree on how deep
/// the operand stack is. That single property catches the whole class of
/// compiler bugs where one branch of a conditional leaves a value and the other
/// does not — which is otherwise invisible until a program takes the unlucky
/// path.
///
/// Together they are what makes an artifact safe to execute in place: a chunk
/// that passes cannot underflow the operand stack, jump outside itself, or
/// decode an operand as an instruction. What it does *not* prove is
/// termination — a jump target inside the chunk is well-formed whether or not
/// it closes a loop — which is why [`Op::Tick`] sits on every back edge and why
/// a host running untrusted bytecode still needs `max_operations`.
///
/// Returns the measured stack high water, which is what the chunk should
/// declare.
pub fn verify(
    caps: Caps,
    code: &[u8],
    functions: &[Function],
    chunks: &[Chunk],
    pools: &Pools,
) -> Result<Vec<u16>, VerifyError> {
    // Pass one: where do instructions start?
    //
    // Over the whole buffer at once, because every chunk shares it and an
    // instruction boundary is a property of the bytes, not of who runs them.
    let mut starts = vec![false; code.len() + 1];
    let mut at = 0usize;
    while at < code.len() {
        starts[at] = true;
        let width = code::width(code, at).ok_or(VerifyError::Undecodable { at })?;
        check_indices(at, code, pools)?;
        at += width;
    }
    if at != code.len() {
        return Err(VerifyError::TrailingBytes {
            at,
            len: code.len(),
        });
    }

    if !functions.is_empty() && !caps.contains(Caps::FUNCTION) {
        return Err(VerifyError::MissingCaps {
            at: 0,
            artifact: caps.to_string(),
            missing: Caps::FUNCTION.to_string(),
        });
    }

    chunks
        .iter()
        .map(|chunk| verify_chunk(caps, code, chunk, &starts, pools))
        .collect()
}

/// What every path into an instruction has to agree on.
///
/// The operand stack is the obvious one. The iterator stack is here for the
/// same reason: a `for` loop's iterator lives on a stack of the VM's own, and
/// a chunk that leaves one behind — or drops one it never made — is a chunk
/// whose loops are not the shape the compiler thought. "The compiler balances
/// them" is exactly the sort of claim a verifier for untrusted bytecode exists
/// to check rather than take on trust.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct State {
    operands: usize,
    iters: usize,
    handlers: usize,
}

/// Walk one chunk's reachable instructions, checking that every path into an
/// instruction agrees on the stack depth.
fn verify_chunk(
    caps: Caps,
    code: &[u8],
    chunk: &Chunk,
    starts: &[bool],
    pools: &Pools,
) -> Result<u16, VerifyError> {
    let (entry, end) = (chunk.entry() as usize, chunk.end() as usize);
    if end > code.len() || entry > end {
        return Err(VerifyError::ChunkOutOfRange {
            entry: chunk.entry(),
            end: chunk.end(),
            len: code.len(),
        });
    }

    let mut depth_at: Vec<Option<State>> = vec![None; code.len()];
    let mut work_list = vec![(entry, State::default())];
    let mut high_water = 0usize;

    while let Some((at, state)) = work_list.pop() {
        if at >= end {
            return Err(VerifyError::FallsOffTheEnd);
        }

        // Merge point: either this is the first visit, or every earlier path
        // has to have arrived in the same state.
        match depth_at[at] {
            Some(seen) if seen == state => continue,
            Some(seen) => {
                return Err(VerifyError::DepthConflict {
                    at,
                    expected: seen.operands,
                    found: state.operands,
                })
            }
            None => depth_at[at] = Some(state),
        }

        let depth = state.operands;
        high_water = high_water.max(depth);

        let op = code::decode(code, at).ok_or(VerifyError::Undecodable { at })?;

        let required_caps = required_caps(&op, pools);

        if !caps.contains(required_caps) {
            return Err(VerifyError::MissingCaps {
                at,
                artifact: caps.to_string(),
                missing: (required_caps - caps).to_string(),
            });
        }

        let (requires, pops, pushes) = effect(&op, pools);

        // Number of slots to pop from the stack must necessarily
        // be <= the number of slots required to be on the stack.
        // Otherwise there is an error in the `effect` mapping table.
        if pops > requires {
            return Err(VerifyError::Undecodable { at });
        }
        // Check if there are enough stack slots to inspect/pop
        if depth < requires {
            return Err(VerifyError::Underflow {
                at,
                need: requires,
                have: depth,
            });
        }

        let next_state = State {
            operands: depth - pops + pushes,
            iters: match op {
                Op::IterInit => state.iters + 1,
                Op::IterDrop => state
                    .iters
                    .checked_sub(1)
                    .ok_or(VerifyError::IteratorUnderflow { at })?,
                _ => state.iters,
            },
            handlers: match op {
                Op::PushHandler { .. } => state.handlers + 1,
                Op::PopHandler => state
                    .handlers
                    .checked_sub(1)
                    .ok_or(VerifyError::HandlerUnderflow { at })?,
                _ => state.handlers,
            },
        };
        let next_depth = next_state.operands;
        high_water = high_water.max(next_depth);

        let width = code::width(code, at).expect("decoded, so it has a width");
        let next = at + width;

        let mut go = |target: u32, state: State| -> Result<(), VerifyError> {
            let target = target as usize;
            // Within this chunk: a jump into another function's body would run
            // its instructions against this frame's locals.
            if target < entry || target >= end {
                return Err(VerifyError::JumpOutOfRange {
                    at,
                    target: target as u32,
                });
            }
            if !starts[target] {
                return Err(VerifyError::JumpIntoAnInstruction {
                    at,
                    target: target as u32,
                });
            }
            work_list.push((target, state));
            Ok(())
        };

        match op {
            // Terminal: nothing follows. A `throw` always fails, so control
            // leaves the chunk here as surely as it does at a `Return`.
            //
            // Neither has to balance the iterator stack: leaving a frame
            // truncates it to what the frame started with.
            Op::Return | Op::Throw => {}

            Op::Jump(target) => go(target, next_state)?,

            // The catch block is entered on the exception path, so it starts
            // where the `try` did: same operand depth, same iterators, and
            // inside the handler it will disarm itself.
            Op::PushHandler { target, .. } => {
                go(
                    target,
                    State {
                        operands: depth,
                        iters: state.iters,
                        handlers: next_state.handlers,
                    },
                )?;
                work_list.push((next, next_state));
            }

            Op::JumpIfFalse { target }
            | Op::JumpIfTrue { target }
            | Op::SkipIfNotUnit { target } => {
                go(target, next_state)?;
                work_list.push((next, next_state));
            }

            // The one instruction whose edges differ in more than where they
            // go: falling through carries the item it pushed and still holds
            // the iterator, while the exit edge has neither.
            Op::IterNext { exit, indexed } => {
                go(
                    exit,
                    State {
                        operands: depth,
                        iters: state
                            .iters
                            .checked_sub(1)
                            .ok_or(VerifyError::IteratorUnderflow { at })?,
                        handlers: state.handlers,
                    },
                )?;
                work_list.push((
                    next,
                    State {
                        // The item, and the count under it when there is one.
                        operands: depth + 1 + usize::from(indexed),
                        iters: state.iters,
                        handlers: state.handlers,
                    },
                ));
            }

            // Terminal like `Jump`, with one successor per arm. A table with
            // no entry behind it is caught by `check_indices`, which has
            // already run over every instruction.
            Op::Switch(index) => {
                if let Some(table) = pools.switches.get(index as usize) {
                    for target in table
                        .cases
                        .iter()
                        .map(|case| case.target)
                        .chain(table.ranges.iter().map(|range| range.target))
                        .chain(core::iter::once(table.default))
                    {
                        go(target, next_state)?;
                    }
                }
            }

            _ => {
                if next >= end {
                    return Err(VerifyError::FallsOffTheEnd);
                }
                work_list.push((next, next_state));
            }
        }
    }

    let Ok(high_water) = u16::try_from(high_water) else {
        return Err(VerifyError::StackExceedsDeclared {
            needed: high_water,
            declared: chunk.max_stack(),
        });
    };
    if high_water > chunk.max_stack() {
        return Err(VerifyError::StackExceedsDeclared {
            needed: high_water as usize,
            declared: chunk.max_stack(),
        });
    }

    Ok(high_water)
}

/// Capabilities an instruction requires.
fn required_caps(op: &Op, pools: &Pools) -> Caps {
    match op {
        Op::Chain(index) => match pools.chains.get(*index as usize) {
            Some(chain) => {
                let mut caps = Caps::empty();

                match chain.root {
                    Root::Local { .. } | Root::Named { .. } | Root::Temporary => {}
                    Root::This { .. } => caps.insert(Caps::THIS),
                }
                chain.steps.iter().for_each(|step| match step {
                    Step::Index { .. } => caps.insert(Caps::INDEXING),
                    Step::Property { .. } => caps.insert(Caps::PROPERTY),
                    Step::Method { .. } => caps.insert(Caps::METHOD),
                });

                caps
            }
            None => Caps::empty(),
        },

        Op::Const(..)
        | Op::Unit
        | Op::Bool(..)
        | Op::LoadLocal(..)
        | Op::LoadNamed(..)
        | Op::StoreLocal { .. }
        | Op::DeclareLocal { .. }
        | Op::Pop
        | Op::AssignLocal { .. }
        | Op::AssignNamed { .. }
        | Op::JumpIfFalse { .. }
        | Op::JumpIfTrue { .. }
        | Op::Switch(..)
        | Op::Jump(..)
        | Op::UnwindTo(..)
        | Op::Tick
        | Op::Checkpoint
        | Op::PushHandler { .. }
        | Op::PopHandler
        | Op::SkipIfNotUnit { .. }
        | Op::Call { .. }
        | Op::Rotate(..)
        | Op::CheckSize { .. }
        | Op::InterpolateStart
        | Op::InterpolateAppend
        | Op::InterpolateEnd
        | Op::Throw
        | Op::IterInit
        | Op::IterNext { .. }
        | Op::IterDrop
        | Op::Return
        | Op::LoadShared(..)
        | Op::LoadSharedNamed(..)
        | Op::StoreShared(..)
        | Op::Statement { .. } => Caps::empty(),

        Op::MakeFnPtr | Op::MakeClosure(..) | Op::CallFnPtr { .. } => Caps::FN_PTR,

        Op::Curry(..) => Caps::FN_PTR | Caps::CURRYING,

        // `EvalAst` is a host-only instruction, so it is never in an artifact.
        Op::EvalAst { .. } => Caps::empty(),

        Op::Share(..) | Op::ShareNamed(..) => Caps::SHARING,

        Op::RequireThis | Op::LoadThis | Op::LoadThisShared | Op::AssignThis { .. } => Caps::THIS,

        Op::CallRef { receiver, .. } => match receiver {
            Receiver::Local(..) | Receiver::Named(..) => Caps::empty(),
            Receiver::This => Caps::THIS,
        },

        Op::MakeArray(..) => Caps::ARRAY,
        Op::MakeMap(..) => Caps::MAP,
        Op::IsShared => Caps::SHARING,
    }
}

/// How many operands an instruction requires, consumes and produces.
fn effect(op: &Op, pools: &Pools) -> (usize, usize, usize) {
    match op {
        // A chain eats the indices and arguments its steps named, plus a root
        // that is not a slot, plus the value being assigned, and leaves one
        // behind. An index with no chain behind it reads as consuming nothing;
        // `check_indices` is what rejects it.
        Op::Chain(index) => match pools.chains.get(*index as usize) {
            Some(chain) => {
                let consumes = chain.consumes();
                (consumes, consumes, 1)
            }
            None => (0, 0, 1),
        },

        Op::Const(..)
        | Op::Unit
        | Op::Bool(..)
        | Op::LoadLocal(..)
        | Op::LoadNamed(..)
        | Op::LoadShared(..)
        | Op::LoadSharedNamed(..)
        | Op::MakeClosure(..)
        | Op::LoadThis
        | Op::LoadThisShared
        | Op::EvalAst { .. } => (0, 0, 1),

        // A binding check, which either raises or does nothing.
        Op::RequireThis => (0, 0, 0),

        // Sharing is a change to the scope, not to the operand stack.
        Op::Share(..) | Op::ShareNamed(..) => (0, 0, 0),

        Op::StoreLocal { .. } | Op::DeclareLocal { .. } | Op::Pop => (1, 1, 0),

        // Pops the value, leaves nothing: the statement's unit value is a
        // separate `Op::Unit`.
        Op::AssignLocal { .. } | Op::AssignNamed { .. } | Op::AssignThis { .. } => (1, 1, 0),

        Op::JumpIfFalse { .. } | Op::JumpIfTrue { .. } | Op::Switch(..) => (1, 1, 0),

        Op::Jump(..)
        | Op::UnwindTo(..)
        | Op::Tick
        | Op::Checkpoint
        | Op::Statement { .. }
        | Op::PushHandler { .. }
        | Op::PopHandler => (0, 0, 0),

        Op::SkipIfNotUnit { .. } => (1, 0, 0),

        // Arguments in, result out.
        Op::Call { argc, .. } => (*argc as usize, *argc as usize, 1),

        // A named receiver's value is argument zero like any other, and so is
        // `this` — which is pushed first rather than last, but the depth is the
        // same either way. A local's is not on the stack at all. An `argc` of
        // zero names no receiver and is rejected when it runs.
        Op::CallRef { argc, receiver, .. } => match receiver {
            Receiver::Local(..) => {
                let len = (*argc as usize).saturating_sub(1);
                (len, len, 1)
            }
            Receiver::Named(..) | Receiver::This => (*argc as usize, *argc as usize, 1),
        },

        // Reorders what is already there, so the depth is unchanged — but it
        // has to reach every one of them, and saying so is what stops a
        // hand-made artifact reaching under the frame.
        Op::Rotate(under) => (
            *under as usize + 1,
            *under as usize + 1,
            *under as usize + 1,
        ),

        Op::MakeArray(len) => (*len as usize, *len as usize, 1),

        // A key and a value per entry, and the template underneath them.
        Op::MakeMap(len) => {
            let len = 2 * *len as usize + 1;
            (len, len, 1)
        }

        // Measures the element it is standing on without taking it: the
        // literal is still being built and every element it has so far is
        // still on the stack.
        Op::CheckSize { .. } => (1, 1, 1),

        // The buffer is an ordinary operand: started, appended to, then
        // replaced by the string it built.
        // A name in, a pointer out.
        Op::MakeFnPtr | Op::IsShared => (1, 1, 1),
        // The arguments and the pointer itself, leaving one of each.
        Op::Curry(argc) => (*argc as usize + 1, *argc as usize + 1, 1),
        Op::CallFnPtr { argc, .. } => (*argc as usize + 1, *argc as usize + 1, 1),

        Op::InterpolateStart => (0, 0, 1),
        Op::InterpolateAppend => (1, 1, 0),
        Op::InterpolateEnd => (1, 1, 1),

        // Pops the thrown value; nothing follows, so what it leaves is moot.
        Op::Throw | Op::StoreShared(..) => (1, 1, 0),

        // The iterable goes onto the iterator stack, not back onto this one.
        Op::IterInit => (1, 1, 0),
        // Its two edges disagree, so the successor match does the work.
        Op::IterNext { .. } | Op::IterDrop => (0, 0, 0),

        // Consumes whatever is left, so depth afterwards is not meaningful.
        Op::Return => (0, 0, 0),
    }
}

/// Check that every pool reference resolves.
///
/// The VM treats these as assertions, and an artifact is the one place they can
/// be wrong without a compiler bug. Reads the operands off the bytes rather
/// than off a decoded `Op`, so it runs in the same pass that measures widths.
fn check_indices(at: usize, code: &[u8], pools: &Pools) -> Result<(), VerifyError> {
    let index = |offset: usize| code::u16_at(code, at + offset).map_or(0, u32::from);
    let bounded = |index: u32, what: &'static str, len: usize| {
        if index as usize >= len {
            Err(VerifyError::BadIndex { at, what, index })
        } else {
            Ok(())
        }
    };

    match code[at] {
        tag::CONST => bounded(index(1), "constant", pools.consts),
        tag::DECLARE_LOCAL | tag::DECLARE_CONST => bounded(index(1), "name", pools.names),
        tag::CALL
        | tag::CALL_CAPTURE
        | tag::CALL_LOCAL_REF
        | tag::CALL_LOCAL_REF_CAPTURE
        | tag::CALL_THIS_REF
        | tag::CALL_THIS_REF_CAPTURE => bounded(index(1), "name", pools.names),
        // The function's, then the receiver variable's. The slot a local
        // receiver names is not a pool index and is checked against the scope
        // when it runs, as every other slot is.
        tag::CALL_NAMED_REF | tag::CALL_NAMED_REF_CAPTURE => {
            bounded(index(1), "name", pools.names)?;
            bounded(index(4), "name", pools.names)
        }
        tag::CALL_OP => {
            bounded(index(1), "name", pools.names)?;
            bounded(index(4), "operator", pools.tokens)
        }
        tag::ASSIGN_LOCAL => bounded(index(3), "name", pools.names),
        tag::LOAD_NAMED
        | tag::LOAD_SHARED_NAMED
        | tag::ASSIGN_NAMED
        | tag::SHARE_NAMED
        | tag::MAKE_CLOSURE => bounded(index(1), "name", pools.names),
        tag::ASSIGN_NAMED_OP => {
            bounded(index(1), "name", pools.names)?;
            bounded(index(3), "op-assignment", pools.assign_ops)
        }
        tag::ASSIGN_LOCAL_OP => {
            bounded(index(3), "name", pools.names)?;
            bounded(index(5), "op-assignment", pools.assign_ops)
        }
        // `this` needs no name, so the operator is the whole of it — and
        // omitting this would hand `program.assign_op` an unchecked index out
        // of a corrupt artifact.
        tag::ASSIGN_THIS_OP => bounded(index(1), "op-assignment", pools.assign_ops),
        // The receiver's name, which the write-back resolves the scope entry
        // by. A local's slot is not a pool index and is checked against the
        // scope when it runs, as every other slot is.
        tag::CALL_FN_PTR_ON_NAMED => bounded(index(2), "name", pools.names),
        tag::EVAL_AST | tag::EVAL_AST_KEEP => bounded(index(1), "fragment", pools.residuals),
        tag::CHAIN => {
            bounded(index(1), "chain", pools.chains.len())?;
            check_chain_indices(at, &pools.chains[index(1) as usize], pools)
        }
        tag::SWITCH => bounded(index(1), "switch", pools.switches.len()),
        _ => Ok(()),
    }
}

/// Check the pool references *inside* a chain record.
///
/// A chain is one instruction over an unbounded record, so nearly all of what
/// it names lives in the pool rather than in the code. Bounding only the
/// record's own index would leave most of the instruction unverified.
fn check_chain_indices(at: usize, chain: &Chain, pools: &Pools) -> Result<(), VerifyError> {
    let bounded = |index: u32, what: &'static str, len: usize| {
        if index as usize >= len {
            Err(VerifyError::BadIndex { at, what, index })
        } else {
            Ok(())
        }
    };

    match chain.root {
        Root::Local { name, .. } | Root::Named { name, .. } => {
            bounded(name, "name", pools.names)?;
        }
        // Neither names anything in a pool: a temporary has no name at all, and
        // `this` is a register rather than an entry.
        Root::This { .. } | Root::Temporary => {}
    }

    for step in &chain.steps {
        match step {
            // Its operands are stack offsets and its positions are its own.
            Step::Index { .. } => {}
            Step::Property {
                name,
                getter,
                setter,
                ..
            } => {
                bounded(*name, "name", pools.names)?;
                bounded(*getter, "name", pools.names)?;
                bounded(*setter, "name", pools.names)?;
            }
            Step::Method { name, .. } => bounded(*name, "name", pools.names)?,
        }
    }

    match chain.tail {
        Tail::Assign { op: Some(op) } => bounded(op, "op-assignment", pools.assign_ops),
        Tail::Assign { op: None } | Tail::Read => Ok(()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::grain::bytecode::assemble;
    use crate::grain::format::Abi;

    fn pools() -> Pools<'static> {
        Pools {
            consts: 0,
            names: 0,
            tokens: 0,
            assign_ops: 0,
            residuals: 0,
            chains: &[],
            switches: &[],
        }
    }

    /// Assemble one chunk spanning the whole buffer, and check it.
    fn check(ops: Vec<Op>) -> Result<Vec<u16>, VerifyError> {
        let (code, _) = assemble(&ops).expect("the test ops must assemble");
        let chunk = Chunk::new(0, code.len() as u32, 8);
        verify(Abi::host().caps, &code, &[], &[chunk], &pools())
    }

    /// The same, for bytes `assemble` would refuse to produce — which is what
    /// a corrupt artifact hands the loader.
    fn check_bytes(code: Vec<u8>, max_stack: u16) -> Result<Vec<u16>, VerifyError> {
        let chunk = Chunk::new(0, code.len() as u32, max_stack);
        verify(Abi::host().caps, &code, &[], &[chunk], &pools())
    }

    #[test]
    fn accepts_a_well_formed_chunk() {
        assert_eq!(check(vec![Op::Unit, Op::Return]), Ok(vec![1]));
    }

    /// `this` is a register, so reading it costs a push and nothing else, and
    /// assigning to it consumes one without leaving anything behind.
    #[test]
    #[cfg(not(feature = "no_function"))]
    fn the_this_register_is_reached_without_touching_the_scope() {
        assert_eq!(check(vec![Op::LoadThis, Op::Return]), Ok(vec![1]));
        assert_eq!(check(vec![Op::LoadThisShared, Op::Return]), Ok(vec![1]));
        assert_eq!(
            check(vec![Op::RequireThis, Op::Unit, Op::Return]),
            Ok(vec![1])
        );
        assert_eq!(
            check(vec![
                Op::RequireThis,
                Op::Unit,
                Op::AssignThis { op: None },
                Op::Unit,
                Op::Return
            ]),
            Ok(vec![1])
        );
    }

    /// The operator is the whole of `ASSIGN_THIS_OP`'s payload, so an artifact
    /// naming one the pool does not have has to be refused here — nothing
    /// downstream re-checks it.
    #[test]
    fn rejects_an_op_assignment_to_this_that_the_pool_does_not_have() {
        let ops = vec![
            Op::Unit,
            Op::AssignThis { op: Some(0) },
            Op::Unit,
            Op::Return,
        ];
        assert_eq!(
            check(ops),
            Err(VerifyError::BadIndex {
                at: 1,
                what: "op-assignment",
                index: 0,
            })
        );
    }

    /// A chain is one instruction over an unbounded record, so almost all of it
    /// is in the pool rather than in the code. Checking only the record's own
    /// index would leave the rest of the instruction unverified.
    #[test]
    fn rejects_a_chain_that_names_something_the_pools_do_not_have() {
        let chain = |root, steps, tail| Chain {
            root,
            steps,
            tail,
            operands: 0,
        };
        let property = |name| Step::Property {
            name,
            getter: 0,
            setter: 0,
            flags: Default::default(),
            pos: rhai::Position::NONE,
        };

        let past_the_end = [
            chain(
                Root::Named {
                    name: 3,
                    pos: rhai::Position::NONE,
                },
                vec![],
                Tail::Read,
            ),
            chain(Root::Local { slot: 0, name: 3 }, vec![], Tail::Read),
            chain(Root::Temporary, vec![property(3)], Tail::Read),
            chain(
                Root::Temporary,
                vec![Step::Method {
                    name: 3,
                    argc: 0,
                    operand: 0,
                    flags: Default::default(),
                    pos: rhai::Position::NONE,
                }],
                Tail::Read,
            ),
        ];

        for chain in past_the_end {
            let temporary = chain.roots_on_stack();
            let mut ops = vec![Op::Chain(0), Op::Return];
            if temporary {
                ops.insert(0, Op::Unit);
            }
            let (code, _) = assemble(&ops).expect("must assemble");
            let chunk = Chunk::new(0, code.len() as u32, 8);
            let pools = Pools {
                names: 1,
                chains: core::slice::from_ref(&chain),
                ..pools()
            };
            assert!(
                matches!(
                    verify(Abi::host().caps, &code, &[], &[chunk], &pools),
                    Err(VerifyError::BadIndex {
                        what: "name",
                        index: 3,
                        ..
                    }),
                ),
                "{chain:?} names name 3 of 1 and must be refused",
            );
        }

        // And the op-assignment a tail can carry.
        let assigning = chain(
            Root::Local { slot: 0, name: 0 },
            vec![],
            Tail::Assign { op: Some(2) },
        );
        let (code, _) = assemble(&[Op::Unit, Op::Chain(0), Op::Return]).expect("must assemble");
        let chunk = Chunk::new(0, code.len() as u32, 8);
        assert!(matches!(
            verify(
                Abi::host().caps,
                &code,
                &[],
                &[chunk],
                &Pools {
                    names: 1,
                    chains: core::slice::from_ref(&assigning),
                    ..pools()
                },
            ),
            Err(VerifyError::BadIndex {
                what: "op-assignment",
                index: 2,
                ..
            }),
        ));
    }

    /// Chunks share one buffer, so a jump from one into another would run the
    /// callee's instructions against the caller's locals.
    #[test]
    fn rejects_a_jump_from_one_chunk_into_another() {
        // Two chunks: `Unit; Return` twice. The first jumps into the second.
        let (mut code, _) = assemble(&[Op::Unit, Op::Return]).unwrap();
        let boundary = code.len() as u32;
        code.push(tag::JUMP);
        code.extend_from_slice(&0u32.to_le_bytes()); // back into chunk one
        code.push(tag::RETURN);

        let chunks = [
            Chunk::new(0, boundary, 8),
            Chunk::new(boundary, code.len() as u32, 8),
        ];
        assert!(matches!(
            verify(Abi::host().caps, &code, &[], &chunks, &pools()),
            Err(VerifyError::JumpOutOfRange { .. }),
        ));
    }

    /// The reason the high water is returned rather than merely checked: the
    /// compiler's estimate is one slot per instruction, and the VM reserves
    /// from it.
    #[test]
    fn the_high_water_is_what_the_chunk_uses_not_what_it_declares() {
        assert_eq!(
            check(vec![
                Op::Unit,
                Op::Unit,
                Op::Pop,
                Op::Pop,
                Op::Unit,
                Op::Return
            ]),
            Ok(vec![2]),
        );
    }

    /// The property the verifier exists for: one branch leaves a value, the
    /// other does not, and nothing notices until a program takes the wrong
    /// path at runtime.
    #[test]
    fn rejects_branches_that_disagree_on_depth() {
        let ops = vec![
            Op::Bool(true),
            Op::JumpIfFalse { target: 3 },
            Op::Unit, // the taken path pushes
            Op::Return,
        ];

        assert!(
            matches!(check(ops.clone()), Err(VerifyError::DepthConflict { .. })),
            "a branch imbalance must be rejected, got {:?}",
            check(ops),
        );
    }

    #[test]
    fn rejects_a_jump_off_the_end() {
        // Assembled by hand: `assemble` refuses an index it cannot resolve, so
        // an out-of-range *address* can only come from a corrupt artifact.
        let mut code = vec![tag::JUMP];
        code.extend_from_slice(&99u32.to_le_bytes());
        code.push(tag::RETURN);

        assert!(matches!(
            check_bytes(code, 8),
            Err(VerifyError::JumpOutOfRange { .. }),
        ));
    }

    /// A jump into an operand would read that operand's bytes as a tag, which
    /// is how a byte-addressed chunk goes wrong in a way an index-addressed one
    /// could not.
    #[test]
    fn rejects_a_jump_into_the_middle_of_an_instruction() {
        let mut code = vec![tag::JUMP];
        code.extend_from_slice(&3u32.to_le_bytes()); // lands inside itself
        code.push(tag::RETURN);

        assert_eq!(
            check_bytes(code, 8),
            Err(VerifyError::JumpIntoAnInstruction { at: 0, target: 3 }),
        );
    }

    /// A switch is a jump with many targets, and every one of them needs the
    /// proof an ordinary jump gets — otherwise the one arm nobody tested is
    /// the one that decodes an operand as an opcode.
    #[test]
    fn every_arm_of_a_switch_is_checked() {
        let ops = vec![
            Op::Unit,
            Op::Switch(0),
            Op::Unit, // index 2: the case arm
            Op::Return,
            Op::Unit, // index 4: the default
            Op::Return,
        ];
        let (code, offsets) = assemble(&ops).expect("must assemble");
        let chunk = Chunk::new(0, code.len() as u32, 8);

        let table = |case: u32, default: u32| Switch {
            cases: vec![crate::grain::bytecode::SwitchCase {
                hash: 7,
                target: case,
            }],
            ranges: Vec::new(),
            default,
        };

        let good = [table(offsets[2], offsets[4])];
        assert_eq!(
            verify(
                Abi::host().caps,
                &code,
                &[],
                &[chunk],
                &Pools {
                    switches: &good,
                    ..pools()
                }
            ),
            Ok(vec![1]),
        );

        // One byte into the `Switch` instruction's own operand.
        let mid = [table(offsets[1] + 1, offsets[4])];
        assert!(
            matches!(
                verify(
                    Abi::host().caps,
                    &code,
                    &[],
                    &[chunk],
                    &Pools {
                        switches: &mid,
                        ..pools()
                    }
                ),
                Err(VerifyError::JumpIntoAnInstruction { .. }),
            ),
            "a case arm landing mid-instruction must be refused",
        );

        // The default is a target like any other, and the easiest to forget.
        let outside = [table(offsets[2], 9999)];
        assert!(
            matches!(
                verify(
                    Abi::host().caps,
                    &code,
                    &[],
                    &[chunk],
                    &Pools {
                        switches: &outside,
                        ..pools()
                    }
                ),
                Err(VerifyError::JumpOutOfRange { .. }),
            ),
            "a default outside the chunk must be refused",
        );
    }

    #[test]
    fn rejects_popping_an_empty_stack() {
        assert!(matches!(
            check(vec![Op::Pop, Op::Return]),
            Err(VerifyError::Underflow { .. }),
        ));
    }

    /// A rotate reaches under the operands above it, and one frame's operands
    /// sit on the same stack as its caller's. Nothing at run time knows where
    /// the frame started, so the depth it needs is checked here or nowhere.
    #[test]
    fn rejects_a_rotate_that_reaches_below_the_frame() {
        assert!(matches!(
            check(vec![Op::Unit, Op::Unit, Op::Rotate(2), Op::Return]),
            Err(VerifyError::Underflow {
                need: 3,
                have: 2,
                ..
            }),
        ));
        assert_eq!(
            check(vec![
                Op::Unit,
                Op::Unit,
                Op::Unit,
                Op::Rotate(2),
                Op::Return
            ]),
            Ok(vec![3]),
            "with the third operand there it is in range, and nothing moves",
        );
    }

    #[test]
    fn rejects_running_past_the_last_instruction() {
        assert_eq!(check(vec![Op::Unit]), Err(VerifyError::FallsOffTheEnd));
    }

    #[test]
    fn rejects_an_index_with_nothing_behind_it() {
        let (code, _) = assemble(&[Op::Const(7), Op::Return]).unwrap();
        let chunk = Chunk::new(0, code.len() as u32, 8);
        assert!(matches!(
            verify(
                Abi::host().caps,
                &code,
                &[],
                &[chunk],
                &Pools {
                    consts: 1,
                    ..pools()
                }
            ),
            Err(VerifyError::BadIndex {
                what: "constant",
                ..
            }),
        ));
    }

    #[test]
    fn rejects_a_chunk_that_outgrows_its_declared_stack() {
        let (code, _) = assemble(&[Op::Unit, Op::Unit, Op::Unit, Op::Return]).unwrap();
        assert!(matches!(
            check_bytes(code, 2),
            Err(VerifyError::StackExceedsDeclared { .. }),
        ));
    }

    /// Bytes that are not instructions must be named as such rather than
    /// executed.
    #[test]
    fn rejects_a_tag_it_does_not_know() {
        assert_eq!(
            check_bytes(vec![0xff], 8),
            Err(VerifyError::Undecodable { at: 0 }),
        );
    }

    #[test]
    fn rejects_an_instruction_whose_operands_are_cut_off() {
        assert_eq!(
            check_bytes(vec![tag::CONST, 0], 8),
            Err(VerifyError::Undecodable { at: 0 }),
        );
    }

    /// A chunk naming a span the code does not have is a corrupt artifact, not
    /// a compiler bug — and must not index out of bounds.
    #[test]
    fn rejects_a_chunk_that_names_code_it_does_not_have() {
        let (code, _) = assemble(&[Op::Unit, Op::Return]).unwrap();
        assert!(matches!(
            verify(
                Abi::host().caps,
                &code,
                &[],
                &[Chunk::new(0, 9999, 8)],
                &pools()
            ),
            Err(VerifyError::ChunkOutOfRange { .. }),
        ));
    }
}