Skip to main content

harn_vm/
chunk.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::rc::Rc;
5
6use harn_parser::TypeExpr;
7use serde::{Deserialize, Serialize};
8
9/// Bytecode opcodes for the Harn VM.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[repr(u8)]
12pub enum Op {
13    /// Push a constant from the constant pool onto the stack.
14    Constant, // arg: u16 constant index
15    /// Push nil onto the stack.
16    Nil,
17    /// Push true onto the stack.
18    True,
19    /// Push false onto the stack.
20    False,
21
22    // --- Variable operations ---
23    /// Get a variable by name (from constant pool).
24    GetVar, // arg: u16 constant index (name)
25    /// Define a new immutable variable. Pops value from stack.
26    DefLet, // arg: u16 constant index (name)
27    /// Define a new mutable variable. Pops value from stack.
28    DefVar, // arg: u16 constant index (name)
29    /// Assign to an existing mutable variable. Pops value from stack.
30    SetVar, // arg: u16 constant index (name)
31    /// Push a new lexical scope onto the environment stack.
32    PushScope,
33    /// Pop the current lexical scope from the environment stack.
34    PopScope,
35
36    // --- Arithmetic ---
37    Add,
38    Sub,
39    Mul,
40    Div,
41    Mod,
42    Pow,
43    Negate,
44
45    // --- Comparison ---
46    Equal,
47    NotEqual,
48    Less,
49    Greater,
50    LessEqual,
51    GreaterEqual,
52
53    // --- Logical ---
54    Not,
55
56    // --- Control flow ---
57    /// Jump unconditionally. arg: u16 offset.
58    Jump,
59    /// Jump if top of stack is falsy. Does not pop. arg: u16 offset.
60    JumpIfFalse,
61    /// Jump if top of stack is truthy. Does not pop. arg: u16 offset.
62    JumpIfTrue,
63    /// Pop top of stack (discard).
64    Pop,
65
66    // --- Functions ---
67    /// Call a function/builtin. arg: u8 = arg count. Name is on stack below args.
68    Call,
69    /// Tail call: like Call, but replaces the current frame instead of pushing
70    /// a new one. Used for `return f(x)` to enable tail call optimization.
71    /// For builtins, behaves like a regular Call (no frame to replace).
72    TailCall,
73    /// Return from current function. Pops return value.
74    Return,
75    /// Create a closure. arg: u16 = chunk index in function table.
76    Closure,
77
78    // --- Collections ---
79    /// Build a list. arg: u16 = element count. Elements are on stack.
80    BuildList,
81    /// Build a dict. arg: u16 = entry count. Key-value pairs on stack.
82    BuildDict,
83    /// Subscript access: stack has [object, index]. Pushes result.
84    Subscript,
85    /// Optional subscript (`obj?[index]`). Like `Subscript` but pushes nil
86    /// instead of indexing when the object is nil.
87    SubscriptOpt,
88    /// Slice access: stack has [object, start_or_nil, end_or_nil]. Pushes sublist/substring.
89    Slice,
90
91    // --- Object operations ---
92    /// Property access. arg: u16 = constant index (property name).
93    GetProperty,
94    /// Optional property access (?.). Like GetProperty but returns nil
95    /// instead of erroring when the object is nil. arg: u16 = constant index.
96    GetPropertyOpt,
97    /// Property assignment. arg: u16 = constant index (property name).
98    /// Stack: [value] → assigns to the named variable's property.
99    SetProperty,
100    /// Subscript assignment. arg: u16 = constant index (variable name).
101    /// Stack: [index, value] → assigns to variable[index] = value.
102    SetSubscript,
103    /// Method call. arg1: u16 = constant index (method name), arg2: u8 = arg count.
104    MethodCall,
105    /// Optional method call (?.). Like MethodCall but returns nil if the
106    /// receiver is nil instead of dispatching. arg1: u16, arg2: u8.
107    MethodCallOpt,
108
109    // --- String ---
110    /// String concatenation of N parts. arg: u16 = part count.
111    Concat,
112
113    // --- Iteration ---
114    /// Set up a for-in loop. Expects iterable on stack. Pushes iterator state.
115    IterInit,
116    /// Advance iterator. If exhausted, jumps. arg: u16 = jump offset.
117    /// Pushes next value and the variable name is set via DefVar before the loop.
118    IterNext,
119
120    // --- Pipe ---
121    /// Pipe: pops [value, callable], invokes callable(value).
122    Pipe,
123
124    // --- Error handling ---
125    /// Pop value, raise as error.
126    Throw,
127    /// Push exception handler. arg: u16 = offset to catch handler.
128    TryCatchSetup,
129    /// Remove top exception handler (end of try body).
130    PopHandler,
131
132    // --- Concurrency ---
133    /// Execute closure N times sequentially, push results as list.
134    /// Stack: count, closure → result_list
135    Parallel,
136    /// Execute closure for each item in list, push results as list.
137    /// Stack: list, closure → result_list
138    ParallelMap,
139    /// Execute closure for each item in list, push a stream that emits in completion order.
140    /// Stack: list, closure → stream
141    ParallelMapStream,
142    /// Like ParallelMap but wraps each result in Result.Ok/Err, never fails.
143    /// Stack: list, closure → {results: [Result], succeeded: int, failed: int}
144    ParallelSettle,
145    /// Store closure for deferred execution, push TaskHandle.
146    /// Stack: closure → TaskHandle
147    Spawn,
148    /// Acquire a process-local mutex for the current lexical scope.
149    /// arg: u16 constant index (key string).
150    SyncMutexEnter,
151
152    // --- Imports ---
153    /// Import a file. arg: u16 = constant index (path string).
154    Import,
155    /// Selective import. arg1: u16 = path string, arg2: u16 = names list constant.
156    SelectiveImport,
157
158    // --- Deadline ---
159    /// Pop duration value, push deadline onto internal deadline stack.
160    DeadlineSetup,
161    /// Pop deadline from internal deadline stack.
162    DeadlineEnd,
163
164    // --- Enum ---
165    /// Build an enum variant value.
166    /// arg1: u16 = constant index (enum name), arg2: u16 = constant index (variant name),
167    /// arg3: u16 = field count. Fields are on stack.
168    BuildEnum,
169
170    // --- Match ---
171    /// Match an enum pattern. Checks enum_name + variant on the top of stack (dup'd match value).
172    /// arg1: u16 = constant index (enum name), arg2: u16 = constant index (variant name).
173    /// If match succeeds, pushes true; else pushes false.
174    MatchEnum,
175
176    // --- Loop control ---
177    /// Pop the top iterator from the iterator stack (cleanup on break from for-in).
178    PopIterator,
179
180    // --- Defaults ---
181    /// Push the number of arguments passed to the current function call.
182    GetArgc,
183
184    // --- Type checking ---
185    /// Runtime type check on a variable.
186    /// arg1: u16 = constant index (variable name),
187    /// arg2: u16 = constant index (expected type name).
188    /// Throws a TypeError if the variable's type doesn't match.
189    CheckType,
190
191    // --- Result try operator ---
192    /// Try-unwrap: if top is Result.Ok(v), replace with v. If Result.Err(e), return it.
193    TryUnwrap,
194    /// Wrap top of stack in Result.Ok unless it is already a Result.
195    TryWrapOk,
196
197    // --- Spread call ---
198    /// Call with spread arguments. Stack: [callee, args_list] -> result.
199    CallSpread,
200    /// Direct builtin call. Followed by u64 builtin ID, u16 name constant, u8 arg count.
201    /// Runtime still checks closure shadowing before using the ID.
202    CallBuiltin,
203    /// Direct builtin spread call. Followed by u64 builtin ID and u16 name constant.
204    /// Stack: [args_list] -> result.
205    CallBuiltinSpread,
206    /// Method call with spread arguments. Stack: [object, args_list] -> result.
207    /// Followed by 2 bytes for method name constant index.
208    MethodCallSpread,
209
210    // --- Misc ---
211    /// Duplicate top of stack.
212    Dup,
213    /// Swap top two stack values.
214    Swap,
215    /// Membership test: stack has [item, collection]. Pushes bool.
216    /// Works for lists (item in list), dicts (key in dict), strings (substr in string), and sets.
217    Contains,
218
219    // --- Typed arithmetic/comparison fast paths ---
220    AddInt,
221    SubInt,
222    MulInt,
223    DivInt,
224    ModInt,
225    AddFloat,
226    SubFloat,
227    MulFloat,
228    DivFloat,
229    ModFloat,
230    EqualInt,
231    NotEqualInt,
232    LessInt,
233    GreaterInt,
234    LessEqualInt,
235    GreaterEqualInt,
236    EqualFloat,
237    NotEqualFloat,
238    LessFloat,
239    GreaterFloat,
240    LessEqualFloat,
241    GreaterEqualFloat,
242    EqualBool,
243    NotEqualBool,
244    EqualString,
245    NotEqualString,
246
247    /// Yield a value from a generator. Pops value, sends through channel, suspends.
248    Yield,
249
250    // --- Slot-indexed locals ---
251    /// Get a frame-local slot. arg: u16 slot index.
252    GetLocalSlot,
253    /// Define or initialize a frame-local slot. Pops value from stack.
254    DefLocalSlot,
255    /// Assign an existing frame-local slot. Pops value from stack.
256    SetLocalSlot,
257}
258
259impl Op {
260    pub(crate) const ALL: &'static [Self] = &[
261        Op::Constant,
262        Op::Nil,
263        Op::True,
264        Op::False,
265        Op::GetVar,
266        Op::DefLet,
267        Op::DefVar,
268        Op::SetVar,
269        Op::PushScope,
270        Op::PopScope,
271        Op::Add,
272        Op::Sub,
273        Op::Mul,
274        Op::Div,
275        Op::Mod,
276        Op::Pow,
277        Op::Negate,
278        Op::Equal,
279        Op::NotEqual,
280        Op::Less,
281        Op::Greater,
282        Op::LessEqual,
283        Op::GreaterEqual,
284        Op::Not,
285        Op::Jump,
286        Op::JumpIfFalse,
287        Op::JumpIfTrue,
288        Op::Pop,
289        Op::Call,
290        Op::TailCall,
291        Op::Return,
292        Op::Closure,
293        Op::BuildList,
294        Op::BuildDict,
295        Op::Subscript,
296        Op::SubscriptOpt,
297        Op::Slice,
298        Op::GetProperty,
299        Op::GetPropertyOpt,
300        Op::SetProperty,
301        Op::SetSubscript,
302        Op::MethodCall,
303        Op::MethodCallOpt,
304        Op::Concat,
305        Op::IterInit,
306        Op::IterNext,
307        Op::Pipe,
308        Op::Throw,
309        Op::TryCatchSetup,
310        Op::PopHandler,
311        Op::Parallel,
312        Op::ParallelMap,
313        Op::ParallelMapStream,
314        Op::ParallelSettle,
315        Op::Spawn,
316        Op::SyncMutexEnter,
317        Op::Import,
318        Op::SelectiveImport,
319        Op::DeadlineSetup,
320        Op::DeadlineEnd,
321        Op::BuildEnum,
322        Op::MatchEnum,
323        Op::PopIterator,
324        Op::GetArgc,
325        Op::CheckType,
326        Op::TryUnwrap,
327        Op::TryWrapOk,
328        Op::CallSpread,
329        Op::CallBuiltin,
330        Op::CallBuiltinSpread,
331        Op::MethodCallSpread,
332        Op::Dup,
333        Op::Swap,
334        Op::Contains,
335        Op::AddInt,
336        Op::SubInt,
337        Op::MulInt,
338        Op::DivInt,
339        Op::ModInt,
340        Op::AddFloat,
341        Op::SubFloat,
342        Op::MulFloat,
343        Op::DivFloat,
344        Op::ModFloat,
345        Op::EqualInt,
346        Op::NotEqualInt,
347        Op::LessInt,
348        Op::GreaterInt,
349        Op::LessEqualInt,
350        Op::GreaterEqualInt,
351        Op::EqualFloat,
352        Op::NotEqualFloat,
353        Op::LessFloat,
354        Op::GreaterFloat,
355        Op::LessEqualFloat,
356        Op::GreaterEqualFloat,
357        Op::EqualBool,
358        Op::NotEqualBool,
359        Op::EqualString,
360        Op::NotEqualString,
361        Op::Yield,
362        Op::GetLocalSlot,
363        Op::DefLocalSlot,
364        Op::SetLocalSlot,
365    ];
366
367    pub(crate) fn from_byte(byte: u8) -> Option<Self> {
368        Self::ALL.get(byte as usize).copied()
369    }
370}
371
372/// A constant value in the constant pool.
373#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
374pub enum Constant {
375    Int(i64),
376    Float(f64),
377    String(String),
378    Bool(bool),
379    Nil,
380    Duration(i64),
381}
382
383/// Monomorphic inline-cache state for bytecode instructions that repeatedly
384/// resolve the same property or builtin method. Cache guards are intentionally
385/// conservative: each entry is tied to the instruction's name constant index
386/// and a single receiver shape. Harn collection values are immutable or
387/// copy-on-write at the VM level, so receiver-kind caches do not need
388/// invalidation. Struct field caches guard on the field name at the cached
389/// slot before reading the indexed field.
390#[derive(Debug, Clone, PartialEq, Eq)]
391pub(crate) enum InlineCacheEntry {
392    Empty,
393    Property {
394        name_idx: u16,
395        target: PropertyCacheTarget,
396    },
397    Method {
398        name_idx: u16,
399        argc: usize,
400        target: MethodCacheTarget,
401    },
402}
403
404#[derive(Debug, Clone, PartialEq, Eq)]
405pub(crate) enum PropertyCacheTarget {
406    DictField(Rc<str>),
407    StructField { field_name: Rc<str>, index: usize },
408    ListCount,
409    ListEmpty,
410    ListFirst,
411    ListLast,
412    StringCount,
413    StringEmpty,
414    PairFirst,
415    PairSecond,
416    EnumVariant,
417    EnumFields,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
421pub(crate) enum MethodCacheTarget {
422    ListCount,
423    ListEmpty,
424    StringCount,
425    StringEmpty,
426    DictCount,
427    RangeCount,
428    RangeLen,
429    RangeEmpty,
430    RangeFirst,
431    RangeLast,
432    SetCount,
433    SetLen,
434    SetEmpty,
435}
436
437/// Debug metadata for a slot-indexed local in a compiled chunk.
438#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
439pub struct LocalSlotInfo {
440    pub name: String,
441    pub mutable: bool,
442    pub scope_depth: usize,
443}
444
445impl fmt::Display for Constant {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        match self {
448            Constant::Int(n) => write!(f, "{n}"),
449            Constant::Float(n) => write!(f, "{n}"),
450            Constant::String(s) => write!(f, "\"{s}\""),
451            Constant::Bool(b) => write!(f, "{b}"),
452            Constant::Nil => write!(f, "nil"),
453            Constant::Duration(ms) => write!(f, "{ms}ms"),
454        }
455    }
456}
457
458/// A compiled chunk of bytecode.
459#[derive(Debug, Clone)]
460pub struct Chunk {
461    /// The bytecode instructions.
462    pub code: Vec<u8>,
463    /// Constant pool.
464    pub constants: Vec<Constant>,
465    /// Source line numbers for each instruction (for error reporting).
466    pub lines: Vec<u32>,
467    /// Source column numbers for each instruction (for error reporting).
468    /// Parallel to `lines`; 0 means no column info available.
469    pub columns: Vec<u32>,
470    /// Source file that this chunk was compiled from, when known. Set for
471    /// chunks compiled from imported modules so runtime errors can report
472    /// the correct file path for each frame instead of always pointing at
473    /// the entry-point pipeline.
474    pub source_file: Option<String>,
475    /// Current column to use when emitting instructions (set by compiler).
476    current_col: u32,
477    /// Compiled function bodies (for closures).
478    pub functions: Vec<CompiledFunctionRef>,
479    /// Instruction offset to inline-cache slot. Slots are assigned at emit time
480    /// for cacheable instructions while bytecode bytes remain immutable.
481    inline_cache_slots: BTreeMap<usize, usize>,
482    /// Shared cache entries so cloned chunks in call frames warm the same side
483    /// table as the compiled chunk used by tests/debugging.
484    inline_caches: Rc<RefCell<Vec<InlineCacheEntry>>>,
485    /// Source-name metadata for slot-indexed locals in this chunk.
486    pub(crate) local_slots: Vec<LocalSlotInfo>,
487}
488
489pub type ChunkRef = Rc<Chunk>;
490pub type CompiledFunctionRef = Rc<CompiledFunction>;
491
492/// Serializable snapshot of a [`Chunk`] suitable for the on-disk bytecode
493/// cache and for in-memory stdlib artifact caches. Inline-cache state is
494/// dropped at freeze time because it warms at runtime per-process; the
495/// rest of the chunk round-trips byte-identically.
496#[derive(Debug, Clone, Serialize, Deserialize)]
497pub struct CachedChunk {
498    pub(crate) code: Vec<u8>,
499    pub(crate) constants: Vec<Constant>,
500    pub(crate) lines: Vec<u32>,
501    pub(crate) columns: Vec<u32>,
502    pub(crate) source_file: Option<String>,
503    pub(crate) current_col: u32,
504    pub(crate) functions: Vec<CachedCompiledFunction>,
505    pub(crate) inline_cache_slots: BTreeMap<usize, usize>,
506    pub(crate) local_slots: Vec<LocalSlotInfo>,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct CachedCompiledFunction {
511    pub(crate) name: String,
512    pub(crate) type_params: Vec<String>,
513    pub(crate) nominal_type_names: Vec<String>,
514    pub(crate) params: Vec<ParamSlot>,
515    pub(crate) default_start: Option<usize>,
516    pub(crate) chunk: CachedChunk,
517    pub(crate) is_generator: bool,
518    pub(crate) is_stream: bool,
519    pub(crate) has_rest_param: bool,
520}
521
522/// One parameter slot of a compiled user-defined function. Carries the
523/// declared name, the (optional) declared type expression, and a flag
524/// for whether a default value was provided. The runtime consults the
525/// type expression in `bind_param_slots` to enforce declared types
526/// against the values supplied at the call site.
527#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct ParamSlot {
529    pub name: String,
530    /// Declared parameter type. `None` for untyped parameters (gradual
531    /// typing); the runtime skips type assertion when absent.
532    pub type_expr: Option<TypeExpr>,
533    /// True when the parameter has a default-value clause. Diagnostic
534    /// only — the canonical authority for arity ranges is
535    /// [`CompiledFunction::default_start`].
536    pub has_default: bool,
537}
538
539impl ParamSlot {
540    /// Build a [`ParamSlot`] from a parser-side [`harn_parser::TypedParam`].
541    /// Centralizes the conversion so every compile path stays in lockstep.
542    pub fn from_typed_param(param: &harn_parser::TypedParam) -> Self {
543        Self {
544            name: param.name.clone(),
545            type_expr: param.type_expr.clone(),
546            has_default: param.default_value.is_some(),
547        }
548    }
549
550    /// Build a `Vec<ParamSlot>` from a slice of parser-side typed
551    /// parameters. Used pervasively at compile sites instead of
552    /// `TypedParam::names` (which discarded the type info we now need
553    /// at runtime).
554    pub fn vec_from_typed(params: &[harn_parser::TypedParam]) -> Vec<Self> {
555        params.iter().map(Self::from_typed_param).collect()
556    }
557}
558
559/// A compiled function (closure body).
560#[derive(Debug, Clone)]
561pub struct CompiledFunction {
562    pub name: String,
563    /// Generic type parameters declared by this function. Runtime
564    /// validation treats these as static-only constraints because the VM
565    /// does not monomorphize function bodies.
566    pub type_params: Vec<String>,
567    /// User-defined struct and enum names visible when this function was
568    /// compiled. These are the only non-primitive named types with runtime
569    /// nominal identity; aliases and interfaces remain static-only.
570    pub nominal_type_names: Vec<String>,
571    pub params: Vec<ParamSlot>,
572    /// Index of the first parameter with a default value, or None if all required.
573    pub default_start: Option<usize>,
574    pub chunk: ChunkRef,
575    /// True if the function body contains `yield` expressions (generator function).
576    pub is_generator: bool,
577    /// True if the function was declared as `gen fn` and should return Stream.
578    pub is_stream: bool,
579    /// True if the last parameter is a rest parameter (`...name`).
580    pub has_rest_param: bool,
581}
582
583impl CompiledFunction {
584    /// Returns just the parameter names — convenience for code paths that
585    /// don't care about types or defaults.
586    pub fn param_names(&self) -> impl Iterator<Item = &str> {
587        self.params.iter().map(|p| p.name.as_str())
588    }
589
590    /// Number of required parameters (those before `default_start`).
591    pub fn required_param_count(&self) -> usize {
592        self.default_start.unwrap_or(self.params.len())
593    }
594
595    pub fn declares_type_param(&self, name: &str) -> bool {
596        self.type_params.iter().any(|param| param == name)
597    }
598
599    pub fn has_nominal_type(&self, name: &str) -> bool {
600        self.nominal_type_names.iter().any(|ty| ty == name)
601    }
602
603    pub(crate) fn freeze_for_cache(&self) -> CachedCompiledFunction {
604        CachedCompiledFunction {
605            name: self.name.clone(),
606            type_params: self.type_params.clone(),
607            nominal_type_names: self.nominal_type_names.clone(),
608            params: self.params.clone(),
609            default_start: self.default_start,
610            chunk: self.chunk.freeze_for_cache(),
611            is_generator: self.is_generator,
612            is_stream: self.is_stream,
613            has_rest_param: self.has_rest_param,
614        }
615    }
616
617    pub(crate) fn from_cached(cached: &CachedCompiledFunction) -> Self {
618        Self {
619            name: cached.name.clone(),
620            type_params: cached.type_params.clone(),
621            nominal_type_names: cached.nominal_type_names.clone(),
622            params: cached.params.clone(),
623            default_start: cached.default_start,
624            chunk: Rc::new(Chunk::from_cached(&cached.chunk)),
625            is_generator: cached.is_generator,
626            is_stream: cached.is_stream,
627            has_rest_param: cached.has_rest_param,
628        }
629    }
630}
631
632impl Chunk {
633    pub fn new() -> Self {
634        Self {
635            code: Vec::new(),
636            constants: Vec::new(),
637            lines: Vec::new(),
638            columns: Vec::new(),
639            source_file: None,
640            current_col: 0,
641            functions: Vec::new(),
642            inline_cache_slots: BTreeMap::new(),
643            inline_caches: Rc::new(RefCell::new(Vec::new())),
644            local_slots: Vec::new(),
645        }
646    }
647
648    /// Set the current column for subsequent emit calls.
649    pub fn set_column(&mut self, col: u32) {
650        self.current_col = col;
651    }
652
653    /// Add a constant and return its index.
654    pub fn add_constant(&mut self, constant: Constant) -> u16 {
655        for (i, c) in self.constants.iter().enumerate() {
656            if c == &constant {
657                return i as u16;
658            }
659        }
660        let idx = self.constants.len();
661        self.constants.push(constant);
662        idx as u16
663    }
664
665    /// Emit a single-byte instruction.
666    pub fn emit(&mut self, op: Op, line: u32) {
667        let col = self.current_col;
668        self.code.push(op as u8);
669        self.lines.push(line);
670        self.columns.push(col);
671    }
672
673    /// Emit an instruction with a u16 argument.
674    pub fn emit_u16(&mut self, op: Op, arg: u16, line: u32) {
675        let col = self.current_col;
676        let op_offset = self.code.len();
677        self.code.push(op as u8);
678        self.code.push((arg >> 8) as u8);
679        self.code.push((arg & 0xFF) as u8);
680        self.lines.push(line);
681        self.lines.push(line);
682        self.lines.push(line);
683        self.columns.push(col);
684        self.columns.push(col);
685        self.columns.push(col);
686        if matches!(
687            op,
688            Op::GetProperty | Op::GetPropertyOpt | Op::MethodCallSpread
689        ) {
690            self.register_inline_cache(op_offset);
691        }
692    }
693
694    /// Emit an instruction with a u8 argument.
695    pub fn emit_u8(&mut self, op: Op, arg: u8, line: u32) {
696        let col = self.current_col;
697        self.code.push(op as u8);
698        self.code.push(arg);
699        self.lines.push(line);
700        self.lines.push(line);
701        self.columns.push(col);
702        self.columns.push(col);
703    }
704
705    /// Emit a direct builtin call.
706    pub fn emit_call_builtin(
707        &mut self,
708        id: crate::BuiltinId,
709        name_idx: u16,
710        arg_count: u8,
711        line: u32,
712    ) {
713        let col = self.current_col;
714        self.code.push(Op::CallBuiltin as u8);
715        self.code.extend_from_slice(&id.raw().to_be_bytes());
716        self.code.push((name_idx >> 8) as u8);
717        self.code.push((name_idx & 0xFF) as u8);
718        self.code.push(arg_count);
719        for _ in 0..12 {
720            self.lines.push(line);
721            self.columns.push(col);
722        }
723    }
724
725    /// Emit a direct builtin spread call.
726    pub fn emit_call_builtin_spread(&mut self, id: crate::BuiltinId, name_idx: u16, line: u32) {
727        let col = self.current_col;
728        self.code.push(Op::CallBuiltinSpread as u8);
729        self.code.extend_from_slice(&id.raw().to_be_bytes());
730        self.code.push((name_idx >> 8) as u8);
731        self.code.push((name_idx & 0xFF) as u8);
732        for _ in 0..11 {
733            self.lines.push(line);
734            self.columns.push(col);
735        }
736    }
737
738    /// Emit a method call: op + u16 (method name) + u8 (arg count).
739    pub fn emit_method_call(&mut self, name_idx: u16, arg_count: u8, line: u32) {
740        self.emit_method_call_inner(Op::MethodCall, name_idx, arg_count, line);
741    }
742
743    /// Emit an optional method call (?.) — returns nil if receiver is nil.
744    pub fn emit_method_call_opt(&mut self, name_idx: u16, arg_count: u8, line: u32) {
745        self.emit_method_call_inner(Op::MethodCallOpt, name_idx, arg_count, line);
746    }
747
748    fn emit_method_call_inner(&mut self, op: Op, name_idx: u16, arg_count: u8, line: u32) {
749        let col = self.current_col;
750        let op_offset = self.code.len();
751        self.code.push(op as u8);
752        self.code.push((name_idx >> 8) as u8);
753        self.code.push((name_idx & 0xFF) as u8);
754        self.code.push(arg_count);
755        self.lines.push(line);
756        self.lines.push(line);
757        self.lines.push(line);
758        self.lines.push(line);
759        self.columns.push(col);
760        self.columns.push(col);
761        self.columns.push(col);
762        self.columns.push(col);
763        self.register_inline_cache(op_offset);
764    }
765
766    /// Current code offset (for jump patching).
767    pub fn current_offset(&self) -> usize {
768        self.code.len()
769    }
770
771    /// Emit a jump instruction with a placeholder offset. Returns the position to patch.
772    pub fn emit_jump(&mut self, op: Op, line: u32) -> usize {
773        let col = self.current_col;
774        self.code.push(op as u8);
775        let patch_pos = self.code.len();
776        self.code.push(0xFF);
777        self.code.push(0xFF);
778        self.lines.push(line);
779        self.lines.push(line);
780        self.lines.push(line);
781        self.columns.push(col);
782        self.columns.push(col);
783        self.columns.push(col);
784        patch_pos
785    }
786
787    /// Patch a jump instruction at the given position to jump to the current offset.
788    pub fn patch_jump(&mut self, patch_pos: usize) {
789        let target = self.code.len() as u16;
790        self.code[patch_pos] = (target >> 8) as u8;
791        self.code[patch_pos + 1] = (target & 0xFF) as u8;
792    }
793
794    /// Patch a jump to a specific target position.
795    pub fn patch_jump_to(&mut self, patch_pos: usize, target: usize) {
796        let target = target as u16;
797        self.code[patch_pos] = (target >> 8) as u8;
798        self.code[patch_pos + 1] = (target & 0xFF) as u8;
799    }
800
801    /// Read a u16 argument at the given position.
802    pub fn read_u16(&self, pos: usize) -> u16 {
803        ((self.code[pos] as u16) << 8) | (self.code[pos + 1] as u16)
804    }
805
806    fn register_inline_cache(&mut self, op_offset: usize) {
807        if self.inline_cache_slots.contains_key(&op_offset) {
808            return;
809        }
810        let mut entries = self.inline_caches.borrow_mut();
811        let slot = entries.len();
812        entries.push(InlineCacheEntry::Empty);
813        self.inline_cache_slots.insert(op_offset, slot);
814    }
815
816    pub(crate) fn inline_cache_slot(&self, op_offset: usize) -> Option<usize> {
817        self.inline_cache_slots.get(&op_offset).copied()
818    }
819
820    pub(crate) fn inline_cache_entry(&self, slot: usize) -> InlineCacheEntry {
821        self.inline_caches
822            .borrow()
823            .get(slot)
824            .cloned()
825            .unwrap_or(InlineCacheEntry::Empty)
826    }
827
828    pub(crate) fn set_inline_cache_entry(&self, slot: usize, entry: InlineCacheEntry) {
829        if let Some(existing) = self.inline_caches.borrow_mut().get_mut(slot) {
830            *existing = entry;
831        }
832    }
833
834    pub fn freeze_for_cache(&self) -> CachedChunk {
835        CachedChunk {
836            code: self.code.clone(),
837            constants: self.constants.clone(),
838            lines: self.lines.clone(),
839            columns: self.columns.clone(),
840            source_file: self.source_file.clone(),
841            current_col: self.current_col,
842            functions: self
843                .functions
844                .iter()
845                .map(|function| function.freeze_for_cache())
846                .collect(),
847            inline_cache_slots: self.inline_cache_slots.clone(),
848            local_slots: self.local_slots.clone(),
849        }
850    }
851
852    pub fn from_cached(cached: &CachedChunk) -> Self {
853        let inline_cache_count = cached.inline_cache_slots.len();
854        Self {
855            code: cached.code.clone(),
856            constants: cached.constants.clone(),
857            lines: cached.lines.clone(),
858            columns: cached.columns.clone(),
859            source_file: cached.source_file.clone(),
860            current_col: cached.current_col,
861            functions: cached
862                .functions
863                .iter()
864                .map(|function| Rc::new(CompiledFunction::from_cached(function)))
865                .collect(),
866            inline_cache_slots: cached.inline_cache_slots.clone(),
867            inline_caches: Rc::new(RefCell::new(vec![
868                InlineCacheEntry::Empty;
869                inline_cache_count
870            ])),
871            local_slots: cached.local_slots.clone(),
872        }
873    }
874
875    pub(crate) fn add_local_slot(
876        &mut self,
877        name: String,
878        mutable: bool,
879        scope_depth: usize,
880    ) -> u16 {
881        let idx = self.local_slots.len();
882        self.local_slots.push(LocalSlotInfo {
883            name,
884            mutable,
885            scope_depth,
886        });
887        idx as u16
888    }
889
890    #[cfg(test)]
891    pub(crate) fn inline_cache_entries(&self) -> Vec<InlineCacheEntry> {
892        self.inline_caches.borrow().clone()
893    }
894
895    /// Read a u64 argument at the given position.
896    pub fn read_u64(&self, pos: usize) -> u64 {
897        u64::from_be_bytes([
898            self.code[pos],
899            self.code[pos + 1],
900            self.code[pos + 2],
901            self.code[pos + 3],
902            self.code[pos + 4],
903            self.code[pos + 5],
904            self.code[pos + 6],
905            self.code[pos + 7],
906        ])
907    }
908
909    /// Disassemble for debugging.
910    pub fn disassemble(&self, name: &str) -> String {
911        let mut out = format!("== {name} ==\n");
912        let mut ip = 0;
913        while ip < self.code.len() {
914            let op = self.code[ip];
915            let line = self.lines.get(ip).copied().unwrap_or(0);
916            out.push_str(&format!("{:04} [{:>4}] ", ip, line));
917            ip += 1;
918
919            match op {
920                x if x == Op::Constant as u8 => {
921                    let idx = self.read_u16(ip);
922                    ip += 2;
923                    let val = &self.constants[idx as usize];
924                    out.push_str(&format!("CONSTANT {:>4} ({})\n", idx, val));
925                }
926                x if x == Op::Nil as u8 => out.push_str("NIL\n"),
927                x if x == Op::True as u8 => out.push_str("TRUE\n"),
928                x if x == Op::False as u8 => out.push_str("FALSE\n"),
929                x if x == Op::GetVar as u8 => {
930                    let idx = self.read_u16(ip);
931                    ip += 2;
932                    out.push_str(&format!(
933                        "GET_VAR {:>4} ({})\n",
934                        idx, self.constants[idx as usize]
935                    ));
936                }
937                x if x == Op::DefLet as u8 => {
938                    let idx = self.read_u16(ip);
939                    ip += 2;
940                    out.push_str(&format!(
941                        "DEF_LET {:>4} ({})\n",
942                        idx, self.constants[idx as usize]
943                    ));
944                }
945                x if x == Op::DefVar as u8 => {
946                    let idx = self.read_u16(ip);
947                    ip += 2;
948                    out.push_str(&format!(
949                        "DEF_VAR {:>4} ({})\n",
950                        idx, self.constants[idx as usize]
951                    ));
952                }
953                x if x == Op::SetVar as u8 => {
954                    let idx = self.read_u16(ip);
955                    ip += 2;
956                    out.push_str(&format!(
957                        "SET_VAR {:>4} ({})\n",
958                        idx, self.constants[idx as usize]
959                    ));
960                }
961                x if x == Op::GetLocalSlot as u8 => {
962                    let slot = self.read_u16(ip);
963                    ip += 2;
964                    out.push_str(&format!("GET_LOCAL_SLOT {:>4}", slot));
965                    if let Some(info) = self.local_slots.get(slot as usize) {
966                        out.push_str(&format!(" ({})", info.name));
967                    }
968                    out.push('\n');
969                }
970                x if x == Op::DefLocalSlot as u8 => {
971                    let slot = self.read_u16(ip);
972                    ip += 2;
973                    out.push_str(&format!("DEF_LOCAL_SLOT {:>4}", slot));
974                    if let Some(info) = self.local_slots.get(slot as usize) {
975                        out.push_str(&format!(" ({})", info.name));
976                    }
977                    out.push('\n');
978                }
979                x if x == Op::SetLocalSlot as u8 => {
980                    let slot = self.read_u16(ip);
981                    ip += 2;
982                    out.push_str(&format!("SET_LOCAL_SLOT {:>4}", slot));
983                    if let Some(info) = self.local_slots.get(slot as usize) {
984                        out.push_str(&format!(" ({})", info.name));
985                    }
986                    out.push('\n');
987                }
988                x if x == Op::PushScope as u8 => out.push_str("PUSH_SCOPE\n"),
989                x if x == Op::PopScope as u8 => out.push_str("POP_SCOPE\n"),
990                x if x == Op::Add as u8 => out.push_str("ADD\n"),
991                x if x == Op::Sub as u8 => out.push_str("SUB\n"),
992                x if x == Op::Mul as u8 => out.push_str("MUL\n"),
993                x if x == Op::Div as u8 => out.push_str("DIV\n"),
994                x if x == Op::Mod as u8 => out.push_str("MOD\n"),
995                x if x == Op::Pow as u8 => out.push_str("POW\n"),
996                x if x == Op::Negate as u8 => out.push_str("NEGATE\n"),
997                x if x == Op::Equal as u8 => out.push_str("EQUAL\n"),
998                x if x == Op::NotEqual as u8 => out.push_str("NOT_EQUAL\n"),
999                x if x == Op::Less as u8 => out.push_str("LESS\n"),
1000                x if x == Op::Greater as u8 => out.push_str("GREATER\n"),
1001                x if x == Op::LessEqual as u8 => out.push_str("LESS_EQUAL\n"),
1002                x if x == Op::GreaterEqual as u8 => out.push_str("GREATER_EQUAL\n"),
1003                x if x == Op::Contains as u8 => out.push_str("CONTAINS\n"),
1004                x if x == Op::Not as u8 => out.push_str("NOT\n"),
1005                x if x == Op::Jump as u8 => {
1006                    let target = self.read_u16(ip);
1007                    ip += 2;
1008                    out.push_str(&format!("JUMP {:>4}\n", target));
1009                }
1010                x if x == Op::JumpIfFalse as u8 => {
1011                    let target = self.read_u16(ip);
1012                    ip += 2;
1013                    out.push_str(&format!("JUMP_IF_FALSE {:>4}\n", target));
1014                }
1015                x if x == Op::JumpIfTrue as u8 => {
1016                    let target = self.read_u16(ip);
1017                    ip += 2;
1018                    out.push_str(&format!("JUMP_IF_TRUE {:>4}\n", target));
1019                }
1020                x if x == Op::Pop as u8 => out.push_str("POP\n"),
1021                x if x == Op::Call as u8 => {
1022                    let argc = self.code[ip];
1023                    ip += 1;
1024                    out.push_str(&format!("CALL {:>4}\n", argc));
1025                }
1026                x if x == Op::TailCall as u8 => {
1027                    let argc = self.code[ip];
1028                    ip += 1;
1029                    out.push_str(&format!("TAIL_CALL {:>4}\n", argc));
1030                }
1031                x if x == Op::Return as u8 => out.push_str("RETURN\n"),
1032                x if x == Op::Closure as u8 => {
1033                    let idx = self.read_u16(ip);
1034                    ip += 2;
1035                    out.push_str(&format!("CLOSURE {:>4}\n", idx));
1036                }
1037                x if x == Op::BuildList as u8 => {
1038                    let count = self.read_u16(ip);
1039                    ip += 2;
1040                    out.push_str(&format!("BUILD_LIST {:>4}\n", count));
1041                }
1042                x if x == Op::BuildDict as u8 => {
1043                    let count = self.read_u16(ip);
1044                    ip += 2;
1045                    out.push_str(&format!("BUILD_DICT {:>4}\n", count));
1046                }
1047                x if x == Op::Subscript as u8 => out.push_str("SUBSCRIPT\n"),
1048                x if x == Op::SubscriptOpt as u8 => out.push_str("SUBSCRIPT_OPT\n"),
1049                x if x == Op::Slice as u8 => out.push_str("SLICE\n"),
1050                x if x == Op::GetProperty as u8 => {
1051                    let idx = self.read_u16(ip);
1052                    ip += 2;
1053                    out.push_str(&format!(
1054                        "GET_PROPERTY {:>4} ({})\n",
1055                        idx, self.constants[idx as usize]
1056                    ));
1057                }
1058                x if x == Op::GetPropertyOpt as u8 => {
1059                    let idx = self.read_u16(ip);
1060                    ip += 2;
1061                    out.push_str(&format!(
1062                        "GET_PROPERTY_OPT {:>4} ({})\n",
1063                        idx, self.constants[idx as usize]
1064                    ));
1065                }
1066                x if x == Op::SetProperty as u8 => {
1067                    let idx = self.read_u16(ip);
1068                    ip += 2;
1069                    out.push_str(&format!(
1070                        "SET_PROPERTY {:>4} ({})\n",
1071                        idx, self.constants[idx as usize]
1072                    ));
1073                }
1074                x if x == Op::SetSubscript as u8 => {
1075                    let idx = self.read_u16(ip);
1076                    ip += 2;
1077                    out.push_str(&format!(
1078                        "SET_SUBSCRIPT {:>4} ({})\n",
1079                        idx, self.constants[idx as usize]
1080                    ));
1081                }
1082                x if x == Op::MethodCall as u8 => {
1083                    let idx = self.read_u16(ip);
1084                    ip += 2;
1085                    let argc = self.code[ip];
1086                    ip += 1;
1087                    out.push_str(&format!(
1088                        "METHOD_CALL {:>4} ({}) argc={}\n",
1089                        idx, self.constants[idx as usize], argc
1090                    ));
1091                }
1092                x if x == Op::MethodCallOpt as u8 => {
1093                    let idx = self.read_u16(ip);
1094                    ip += 2;
1095                    let argc = self.code[ip];
1096                    ip += 1;
1097                    out.push_str(&format!(
1098                        "METHOD_CALL_OPT {:>4} ({}) argc={}\n",
1099                        idx, self.constants[idx as usize], argc
1100                    ));
1101                }
1102                x if x == Op::Concat as u8 => {
1103                    let count = self.read_u16(ip);
1104                    ip += 2;
1105                    out.push_str(&format!("CONCAT {:>4}\n", count));
1106                }
1107                x if x == Op::IterInit as u8 => out.push_str("ITER_INIT\n"),
1108                x if x == Op::IterNext as u8 => {
1109                    let target = self.read_u16(ip);
1110                    ip += 2;
1111                    out.push_str(&format!("ITER_NEXT {:>4}\n", target));
1112                }
1113                x if x == Op::Throw as u8 => out.push_str("THROW\n"),
1114                x if x == Op::TryCatchSetup as u8 => {
1115                    let target = self.read_u16(ip);
1116                    ip += 2;
1117                    out.push_str(&format!("TRY_CATCH_SETUP {:>4}\n", target));
1118                }
1119                x if x == Op::PopHandler as u8 => out.push_str("POP_HANDLER\n"),
1120                x if x == Op::Pipe as u8 => out.push_str("PIPE\n"),
1121                x if x == Op::Parallel as u8 => out.push_str("PARALLEL\n"),
1122                x if x == Op::ParallelMap as u8 => out.push_str("PARALLEL_MAP\n"),
1123                x if x == Op::ParallelMapStream as u8 => out.push_str("PARALLEL_MAP_STREAM\n"),
1124                x if x == Op::ParallelSettle as u8 => out.push_str("PARALLEL_SETTLE\n"),
1125                x if x == Op::Spawn as u8 => out.push_str("SPAWN\n"),
1126                x if x == Op::Import as u8 => {
1127                    let idx = self.read_u16(ip);
1128                    ip += 2;
1129                    out.push_str(&format!(
1130                        "IMPORT {:>4} ({})\n",
1131                        idx, self.constants[idx as usize]
1132                    ));
1133                }
1134                x if x == Op::SelectiveImport as u8 => {
1135                    let path_idx = self.read_u16(ip);
1136                    ip += 2;
1137                    let names_idx = self.read_u16(ip);
1138                    ip += 2;
1139                    out.push_str(&format!(
1140                        "SELECTIVE_IMPORT {:>4} ({}) names: {:>4} ({})\n",
1141                        path_idx,
1142                        self.constants[path_idx as usize],
1143                        names_idx,
1144                        self.constants[names_idx as usize]
1145                    ));
1146                }
1147                x if x == Op::SyncMutexEnter as u8 => {
1148                    let idx = self.read_u16(ip);
1149                    ip += 2;
1150                    out.push_str(&format!(
1151                        "SYNC_MUTEX_ENTER {:>4} ({})\n",
1152                        idx, self.constants[idx as usize]
1153                    ));
1154                }
1155                x if x == Op::DeadlineSetup as u8 => out.push_str("DEADLINE_SETUP\n"),
1156                x if x == Op::DeadlineEnd as u8 => out.push_str("DEADLINE_END\n"),
1157                x if x == Op::BuildEnum as u8 => {
1158                    let enum_idx = self.read_u16(ip);
1159                    ip += 2;
1160                    let variant_idx = self.read_u16(ip);
1161                    ip += 2;
1162                    let field_count = self.read_u16(ip);
1163                    ip += 2;
1164                    out.push_str(&format!(
1165                        "BUILD_ENUM {:>4} ({}) {:>4} ({}) fields={}\n",
1166                        enum_idx,
1167                        self.constants[enum_idx as usize],
1168                        variant_idx,
1169                        self.constants[variant_idx as usize],
1170                        field_count
1171                    ));
1172                }
1173                x if x == Op::MatchEnum as u8 => {
1174                    let enum_idx = self.read_u16(ip);
1175                    ip += 2;
1176                    let variant_idx = self.read_u16(ip);
1177                    ip += 2;
1178                    out.push_str(&format!(
1179                        "MATCH_ENUM {:>4} ({}) {:>4} ({})\n",
1180                        enum_idx,
1181                        self.constants[enum_idx as usize],
1182                        variant_idx,
1183                        self.constants[variant_idx as usize]
1184                    ));
1185                }
1186                x if x == Op::PopIterator as u8 => out.push_str("POP_ITERATOR\n"),
1187                x if x == Op::TryUnwrap as u8 => out.push_str("TRY_UNWRAP\n"),
1188                x if x == Op::TryWrapOk as u8 => out.push_str("TRY_WRAP_OK\n"),
1189                x if x == Op::CallSpread as u8 => out.push_str("CALL_SPREAD\n"),
1190                x if x == Op::CallBuiltin as u8 => {
1191                    let id = self.read_u64(ip);
1192                    ip += 8;
1193                    let idx = self.read_u16(ip);
1194                    ip += 2;
1195                    let argc = self.code[ip];
1196                    ip += 1;
1197                    out.push_str(&format!(
1198                        "CALL_BUILTIN {id:#018x} {:>4} ({}) argc={}\n",
1199                        idx, self.constants[idx as usize], argc
1200                    ));
1201                }
1202                x if x == Op::CallBuiltinSpread as u8 => {
1203                    let id = self.read_u64(ip);
1204                    ip += 8;
1205                    let idx = self.read_u16(ip);
1206                    ip += 2;
1207                    out.push_str(&format!(
1208                        "CALL_BUILTIN_SPREAD {id:#018x} {:>4} ({})\n",
1209                        idx, self.constants[idx as usize]
1210                    ));
1211                }
1212                x if x == Op::MethodCallSpread as u8 => {
1213                    let idx = self.read_u16(ip + 1);
1214                    ip += 2;
1215                    out.push_str(&format!("METHOD_CALL_SPREAD {idx}\n"));
1216                }
1217                x if x == Op::Dup as u8 => out.push_str("DUP\n"),
1218                x if x == Op::Swap as u8 => out.push_str("SWAP\n"),
1219                x if x == Op::AddInt as u8 => out.push_str("ADD_INT\n"),
1220                x if x == Op::SubInt as u8 => out.push_str("SUB_INT\n"),
1221                x if x == Op::MulInt as u8 => out.push_str("MUL_INT\n"),
1222                x if x == Op::DivInt as u8 => out.push_str("DIV_INT\n"),
1223                x if x == Op::ModInt as u8 => out.push_str("MOD_INT\n"),
1224                x if x == Op::AddFloat as u8 => out.push_str("ADD_FLOAT\n"),
1225                x if x == Op::SubFloat as u8 => out.push_str("SUB_FLOAT\n"),
1226                x if x == Op::MulFloat as u8 => out.push_str("MUL_FLOAT\n"),
1227                x if x == Op::DivFloat as u8 => out.push_str("DIV_FLOAT\n"),
1228                x if x == Op::ModFloat as u8 => out.push_str("MOD_FLOAT\n"),
1229                x if x == Op::EqualInt as u8 => out.push_str("EQUAL_INT\n"),
1230                x if x == Op::NotEqualInt as u8 => out.push_str("NOT_EQUAL_INT\n"),
1231                x if x == Op::LessInt as u8 => out.push_str("LESS_INT\n"),
1232                x if x == Op::GreaterInt as u8 => out.push_str("GREATER_INT\n"),
1233                x if x == Op::LessEqualInt as u8 => out.push_str("LESS_EQUAL_INT\n"),
1234                x if x == Op::GreaterEqualInt as u8 => out.push_str("GREATER_EQUAL_INT\n"),
1235                x if x == Op::EqualFloat as u8 => out.push_str("EQUAL_FLOAT\n"),
1236                x if x == Op::NotEqualFloat as u8 => out.push_str("NOT_EQUAL_FLOAT\n"),
1237                x if x == Op::LessFloat as u8 => out.push_str("LESS_FLOAT\n"),
1238                x if x == Op::GreaterFloat as u8 => out.push_str("GREATER_FLOAT\n"),
1239                x if x == Op::LessEqualFloat as u8 => out.push_str("LESS_EQUAL_FLOAT\n"),
1240                x if x == Op::GreaterEqualFloat as u8 => out.push_str("GREATER_EQUAL_FLOAT\n"),
1241                x if x == Op::EqualBool as u8 => out.push_str("EQUAL_BOOL\n"),
1242                x if x == Op::NotEqualBool as u8 => out.push_str("NOT_EQUAL_BOOL\n"),
1243                x if x == Op::EqualString as u8 => out.push_str("EQUAL_STRING\n"),
1244                x if x == Op::NotEqualString as u8 => out.push_str("NOT_EQUAL_STRING\n"),
1245                x if x == Op::Yield as u8 => out.push_str("YIELD\n"),
1246                _ => {
1247                    out.push_str(&format!("UNKNOWN(0x{:02x})\n", op));
1248                }
1249            }
1250        }
1251        out
1252    }
1253}
1254
1255impl Default for Chunk {
1256    fn default() -> Self {
1257        Self::new()
1258    }
1259}
1260
1261#[cfg(test)]
1262mod tests {
1263    use super::Op;
1264
1265    #[test]
1266    fn op_from_byte_matches_repr_order() {
1267        for (byte, op) in Op::ALL.iter().copied().enumerate() {
1268            assert_eq!(byte as u8, op as u8);
1269            assert_eq!(Op::from_byte(byte as u8), Some(op));
1270        }
1271        assert_eq!(Op::from_byte(Op::ALL.len() as u8), None);
1272    }
1273}