plotnik_compiler/bytecode/
ir.rs

1//! Instruction IR with symbolic labels.
2//!
3//! Pre-layout instructions use `Label` for symbolic references.
4//! After layout, labels are resolved to step addresses (u16) for serialization.
5//! Member indices use deferred resolution via `MemberRef`.
6
7use std::collections::BTreeMap;
8use std::num::NonZeroU16;
9
10use crate::analyze::type_check::TypeId;
11use plotnik_bytecode::{
12    Call, EffectOp, EffectOpcode, Nav, Opcode, PredicateOp, Return, StepAddr, StepId, Trampoline,
13    select_match_opcode,
14};
15
16/// Node type constraint for Match instructions.
17///
18/// Distinguishes between named nodes (`(identifier)`), anonymous nodes (`"text"`),
19/// and wildcards (`_`, `(_)`). Encoded in bytecode header byte bits 5-4.
20///
21/// | `node_kind` | Value | Meaning      | `node_type=0`       | `node_type>0`     |
22/// | ----------- | ----- | ------------ | ------------------- | ----------------- |
23/// | `00`        | Any   | `_` pattern  | No check            | (invalid)         |
24/// | `01`        | Named | `(_)`/`(t)`  | Check `is_named()`  | Check `kind_id()` |
25/// | `10`        | Anon  | `"text"`     | Check `!is_named()` | Check `kind_id()` |
26/// | `11`        | -     | Reserved     | Error               | Error             |
27#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
28pub enum NodeTypeIR {
29    /// Any node (`_` pattern) - no type check performed.
30    #[default]
31    Any,
32    /// Named node constraint (`(_)` or `(identifier)`).
33    /// - `None` = any named node (check `is_named()`)
34    /// - `Some(id)` = specific named type (check `kind_id()`)
35    Named(Option<NonZeroU16>),
36    /// Anonymous node constraint (`"text"` literals).
37    /// - `None` = any anonymous node (check `!is_named()`)
38    /// - `Some(id)` = specific anonymous type (check `kind_id()`)
39    Anonymous(Option<NonZeroU16>),
40}
41
42impl NodeTypeIR {
43    /// Encode to bytecode: returns (node_kind bits, node_type value).
44    ///
45    /// `node_kind` is 2 bits for header byte bits 5-4.
46    /// `node_type` is u16 for bytes 2-3.
47    pub fn to_bytes(self) -> (u8, u16) {
48        match self {
49            Self::Any => (0b00, 0),
50            Self::Named(opt) => (0b01, opt.map(|n| n.get()).unwrap_or(0)),
51            Self::Anonymous(opt) => (0b10, opt.map(|n| n.get()).unwrap_or(0)),
52        }
53    }
54
55    /// Decode from bytecode: node_kind bits (2 bits) and node_type value (u16).
56    pub fn from_bytes(node_kind: u8, node_type: u16) -> Self {
57        match node_kind {
58            0b00 => Self::Any,
59            0b01 => Self::Named(NonZeroU16::new(node_type)),
60            0b10 => Self::Anonymous(NonZeroU16::new(node_type)),
61            _ => panic!("invalid node_kind: {node_kind}"),
62        }
63    }
64
65    /// Check if this represents a specific type ID (not a wildcard).
66    pub fn type_id(&self) -> Option<NonZeroU16> {
67        match self {
68            Self::Any => None,
69            Self::Named(opt) | Self::Anonymous(opt) => *opt,
70        }
71    }
72
73    /// Check if this is the Any wildcard.
74    pub fn is_any(&self) -> bool {
75        matches!(self, Self::Any)
76    }
77
78    /// Check if this is a Named constraint (wildcard or specific).
79    pub fn is_named(&self) -> bool {
80        matches!(self, Self::Named(_))
81    }
82
83    /// Check if this is an Anonymous constraint (wildcard or specific).
84    pub fn is_anonymous(&self) -> bool {
85        matches!(self, Self::Anonymous(_))
86    }
87}
88
89/// Symbolic reference, resolved to step address at layout time.
90#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
91pub struct Label(pub u32);
92
93impl Label {
94    /// Resolve this label to a step address using the layout mapping.
95    #[inline]
96    pub fn resolve(self, map: &BTreeMap<Label, StepAddr>) -> StepAddr {
97        *map.get(&self).expect("label not in layout")
98    }
99}
100
101/// Symbolic reference to a struct field or enum variant.
102/// Resolved to absolute member index during bytecode emission.
103///
104/// Struct field indices are deduplicated globally: same (name, type) pair → same index.
105/// This enables call-site scoping where uncaptured refs share the caller's scope.
106///
107/// Enum variant indices use the traditional (parent_type, relative_index) approach
108/// since enum variants don't bubble between scopes.
109#[derive(Clone, Copy, Debug, PartialEq, Eq)]
110pub enum MemberRef {
111    /// Already resolved to absolute index (for cases where it's known).
112    Absolute(u16),
113    /// Deferred resolution by field identity (for struct fields).
114    /// The same (field_name, field_type) pair resolves to the same member index
115    /// regardless of which struct type contains it.
116    Deferred {
117        /// The Symbol of the field name (from query interner).
118        field_name: plotnik_core::Symbol,
119        /// The TypeId of the field's value type (from query TypeContext).
120        field_type: TypeId,
121    },
122    /// Deferred resolution by parent type + relative index (for enum variants).
123    /// Uses the parent enum's member_base + relative_index.
124    DeferredByIndex {
125        /// The TypeId of the parent enum type.
126        parent_type: TypeId,
127        /// Relative index within the parent type's members.
128        relative_index: u16,
129    },
130}
131
132impl MemberRef {
133    /// Create an absolute reference.
134    pub fn absolute(index: u16) -> Self {
135        Self::Absolute(index)
136    }
137
138    /// Create a deferred reference by field identity (for struct fields).
139    pub fn deferred(field_name: plotnik_core::Symbol, field_type: TypeId) -> Self {
140        Self::Deferred {
141            field_name,
142            field_type,
143        }
144    }
145
146    /// Create a deferred reference by parent type + index (for enum variants).
147    pub fn deferred_by_index(parent_type: TypeId, relative_index: u16) -> Self {
148        Self::DeferredByIndex {
149            parent_type,
150            relative_index,
151        }
152    }
153
154    /// Resolve this reference using lookup functions.
155    ///
156    /// - `lookup_member`: maps (field_name Symbol, field_type TypeId) to member index
157    /// - `get_member_base`: maps parent TypeId to member base index
158    pub fn resolve<F, G>(self, lookup_member: F, get_member_base: G) -> u16
159    where
160        F: Fn(plotnik_core::Symbol, TypeId) -> Option<u16>,
161        G: Fn(TypeId) -> Option<u16>,
162    {
163        match self {
164            Self::Absolute(n) => n,
165            Self::Deferred {
166                field_name,
167                field_type,
168            } => lookup_member(field_name, field_type)
169                .expect("deferred member reference must resolve"),
170            Self::DeferredByIndex {
171                parent_type,
172                relative_index,
173            } => {
174                get_member_base(parent_type).expect("deferred member base must resolve")
175                    + relative_index
176            }
177        }
178    }
179}
180
181/// Effect operation with symbolic member references.
182/// Used during compilation; resolved to EffectOp during emission.
183#[derive(Clone, Debug, PartialEq, Eq)]
184pub struct EffectIR {
185    pub opcode: EffectOpcode,
186    /// Payload for effects that don't use member indices.
187    pub payload: usize,
188    /// Member reference for Set/E effects (None for other effects).
189    pub member_ref: Option<MemberRef>,
190}
191
192impl EffectIR {
193    /// Create a simple effect without member reference.
194    pub fn simple(opcode: EffectOpcode, payload: usize) -> Self {
195        Self {
196            opcode,
197            payload,
198            member_ref: None,
199        }
200    }
201
202    /// Create an effect with a member reference.
203    pub fn with_member(opcode: EffectOpcode, member_ref: MemberRef) -> Self {
204        Self {
205            opcode,
206            payload: 0,
207            member_ref: Some(member_ref),
208        }
209    }
210
211    /// Capture current node value.
212    pub fn node() -> Self {
213        Self::simple(EffectOpcode::Node, 0)
214    }
215
216    /// Capture current node text.
217    pub fn text() -> Self {
218        Self::simple(EffectOpcode::Text, 0)
219    }
220
221    /// Push null value.
222    pub fn null() -> Self {
223        Self::simple(EffectOpcode::Null, 0)
224    }
225
226    /// Push accumulated value to array.
227    pub fn push() -> Self {
228        Self::simple(EffectOpcode::Push, 0)
229    }
230
231    /// Begin array scope.
232    pub fn start_arr() -> Self {
233        Self::simple(EffectOpcode::Arr, 0)
234    }
235
236    /// End array scope.
237    pub fn end_arr() -> Self {
238        Self::simple(EffectOpcode::EndArr, 0)
239    }
240
241    /// Begin object scope.
242    pub fn start_obj() -> Self {
243        Self::simple(EffectOpcode::Obj, 0)
244    }
245
246    /// End object scope.
247    pub fn end_obj() -> Self {
248        Self::simple(EffectOpcode::EndObj, 0)
249    }
250
251    /// Begin enum scope.
252    pub fn start_enum() -> Self {
253        Self::simple(EffectOpcode::Enum, 0)
254    }
255
256    /// End enum scope.
257    pub fn end_enum() -> Self {
258        Self::simple(EffectOpcode::EndEnum, 0)
259    }
260
261    /// Begin suppression (suppress effects within).
262    pub fn suppress_begin() -> Self {
263        Self::simple(EffectOpcode::SuppressBegin, 0)
264    }
265
266    /// End suppression.
267    pub fn suppress_end() -> Self {
268        Self::simple(EffectOpcode::SuppressEnd, 0)
269    }
270
271    /// Resolve this IR effect to a concrete EffectOp.
272    ///
273    /// - `lookup_member`: maps (field_name Symbol, field_type TypeId) to member index
274    /// - `get_member_base`: maps parent TypeId to member base index
275    pub fn resolve<F, G>(&self, lookup_member: F, get_member_base: G) -> EffectOp
276    where
277        F: Fn(plotnik_core::Symbol, TypeId) -> Option<u16>,
278        G: Fn(TypeId) -> Option<u16>,
279    {
280        let payload = if let Some(member_ref) = self.member_ref {
281            member_ref.resolve(&lookup_member, &get_member_base) as usize
282        } else {
283            self.payload
284        };
285        EffectOp::new(self.opcode, payload)
286    }
287}
288
289/// Predicate value: string or regex pattern.
290///
291/// Both variants store StringId (index into StringTable). For regex predicates,
292/// the pattern string is also compiled to a DFA during emit.
293#[derive(Clone, Debug, PartialEq, Eq)]
294pub enum PredicateValueIR {
295    /// String comparison value.
296    String(plotnik_bytecode::StringId),
297    /// Regex pattern (StringId for pattern, compiled to DFA during emit).
298    Regex(plotnik_bytecode::StringId),
299}
300
301/// Predicate IR for node text filtering.
302///
303/// Applied after node type/field matching. Compares node text against
304/// a string literal or regex pattern.
305#[derive(Clone, Debug, PartialEq, Eq)]
306pub struct PredicateIR {
307    pub op: PredicateOp,
308    pub value: PredicateValueIR,
309}
310
311impl PredicateIR {
312    /// Create a string predicate (==, !=, ^=, $=, *=).
313    pub fn string(op: PredicateOp, value: plotnik_bytecode::StringId) -> Self {
314        Self {
315            op,
316            value: PredicateValueIR::String(value),
317        }
318    }
319
320    /// Create a regex predicate (=~, !~).
321    pub fn regex(op: PredicateOp, pattern_id: plotnik_bytecode::StringId) -> Self {
322        Self {
323            op,
324            value: PredicateValueIR::Regex(pattern_id),
325        }
326    }
327
328    /// Returns the operator as a u8 for bytecode encoding.
329    pub fn op_byte(&self) -> u8 {
330        self.op.to_byte()
331    }
332}
333
334/// Pre-layout instruction with symbolic references.
335#[derive(Clone, Debug)]
336pub enum InstructionIR {
337    Match(MatchIR),
338    Call(CallIR),
339    Return(ReturnIR),
340    Trampoline(TrampolineIR),
341}
342
343impl InstructionIR {
344    /// Get the label where this instruction lives.
345    #[inline]
346    pub fn label(&self) -> Label {
347        match self {
348            Self::Match(m) => m.label,
349            Self::Call(c) => c.label,
350            Self::Return(r) => r.label,
351            Self::Trampoline(t) => t.label,
352        }
353    }
354
355    /// Compute instruction size in bytes (8, 16, 24, 32, 48, or 64).
356    pub fn size(&self) -> usize {
357        match self {
358            Self::Match(m) => m.size(),
359            Self::Call(_) | Self::Return(_) | Self::Trampoline(_) => 8,
360        }
361    }
362
363    /// Get all successor labels (for graph building).
364    pub fn successors(&self) -> Vec<Label> {
365        match self {
366            Self::Match(m) => m.successors.clone(),
367            Self::Call(c) => vec![c.next],
368            Self::Return(_) => vec![],
369            Self::Trampoline(t) => vec![t.next],
370        }
371    }
372
373    /// Resolve labels and serialize to bytecode bytes.
374    ///
375    /// - `lookup_member`: maps (field_name Symbol, field_type TypeId) to member index
376    /// - `get_member_base`: maps parent TypeId to member base index
377    /// - `lookup_regex`: maps pattern to RegexTable index (for predicate regexes)
378    pub fn resolve<F, G, R>(
379        &self,
380        map: &BTreeMap<Label, StepAddr>,
381        lookup_member: F,
382        get_member_base: G,
383        lookup_regex: R,
384    ) -> Vec<u8>
385    where
386        F: Fn(plotnik_core::Symbol, TypeId) -> Option<u16>,
387        G: Fn(TypeId) -> Option<u16>,
388        R: Fn(plotnik_bytecode::StringId) -> Option<u16>,
389    {
390        match self {
391            Self::Match(m) => m.resolve(map, lookup_member, get_member_base, lookup_regex),
392            Self::Call(c) => c.resolve(map).to_vec(),
393            Self::Return(r) => r.resolve().to_vec(),
394            Self::Trampoline(t) => t.resolve(map).to_vec(),
395        }
396    }
397}
398
399/// Match instruction IR with symbolic successors.
400#[derive(Clone, Debug)]
401pub struct MatchIR {
402    /// Where this instruction lives.
403    pub label: Label,
404    /// Navigation command. `Epsilon` means pure control flow (no node check).
405    pub nav: Nav,
406    /// Node type constraint (Any = wildcard, Named/Anonymous for specific checks).
407    pub node_type: NodeTypeIR,
408    /// Field constraint (None = wildcard).
409    pub node_field: Option<NonZeroU16>,
410    /// Effects to execute before match attempt.
411    pub pre_effects: Vec<EffectIR>,
412    /// Fields that must NOT be present on the node.
413    pub neg_fields: Vec<u16>,
414    /// Effects to execute after successful match.
415    pub post_effects: Vec<EffectIR>,
416    /// Predicate for node text filtering (None = no text check).
417    pub predicate: Option<PredicateIR>,
418    /// Successor labels (empty = accept, 1 = linear, 2+ = branch).
419    pub successors: Vec<Label>,
420}
421
422impl MatchIR {
423    /// Create a terminal/accept state (empty successors).
424    pub fn terminal(label: Label) -> Self {
425        Self {
426            label,
427            nav: Nav::Epsilon,
428            node_type: NodeTypeIR::Any,
429            node_field: None,
430            pre_effects: vec![],
431            neg_fields: vec![],
432            post_effects: vec![],
433            predicate: None,
434            successors: vec![],
435        }
436    }
437
438    /// Start building a match instruction at the given label.
439    pub fn at(label: Label) -> Self {
440        Self::terminal(label)
441    }
442
443    /// Create an epsilon transition (no node interaction) to a single successor.
444    pub fn epsilon(label: Label, next: Label) -> Self {
445        Self::at(label).next(next)
446    }
447
448    /// Set the navigation command.
449    pub fn nav(mut self, nav: Nav) -> Self {
450        self.nav = nav;
451        self
452    }
453
454    /// Set the node type constraint.
455    pub fn node_type(mut self, t: NodeTypeIR) -> Self {
456        self.node_type = t;
457        self
458    }
459
460    /// Set the field constraint.
461    pub fn node_field(mut self, f: impl Into<Option<NonZeroU16>>) -> Self {
462        self.node_field = f.into();
463        self
464    }
465
466    /// Add a negated field constraint.
467    pub fn neg_field(mut self, f: u16) -> Self {
468        self.neg_fields.push(f);
469        self
470    }
471
472    /// Add a pre-match effect.
473    pub fn pre_effect(mut self, e: EffectIR) -> Self {
474        self.pre_effects.push(e);
475        self
476    }
477
478    /// Add a post-match effect.
479    pub fn post_effect(mut self, e: EffectIR) -> Self {
480        self.post_effects.push(e);
481        self
482    }
483
484    /// Add multiple negated field constraints.
485    pub fn neg_fields(mut self, fields: impl IntoIterator<Item = u16>) -> Self {
486        self.neg_fields.extend(fields);
487        self
488    }
489
490    /// Add multiple pre-match effects.
491    pub fn pre_effects(mut self, effects: impl IntoIterator<Item = EffectIR>) -> Self {
492        self.pre_effects.extend(effects);
493        self
494    }
495
496    /// Add multiple post-match effects.
497    pub fn post_effects(mut self, effects: impl IntoIterator<Item = EffectIR>) -> Self {
498        self.post_effects.extend(effects);
499        self
500    }
501
502    /// Set the predicate for node text filtering.
503    pub fn predicate(mut self, p: PredicateIR) -> Self {
504        self.predicate = Some(p);
505        self
506    }
507
508    /// Set a single successor.
509    pub fn next(mut self, s: Label) -> Self {
510        self.successors = vec![s];
511        self
512    }
513
514    /// Set multiple successors (for branches).
515    pub fn next_many(mut self, s: Vec<Label>) -> Self {
516        self.successors = s;
517        self
518    }
519
520    /// Compute instruction size in bytes.
521    pub fn size(&self) -> usize {
522        // Match8 can be used if: no effects, no neg_fields, no predicate, and at most 1 successor
523        let can_use_match8 = self.pre_effects.is_empty()
524            && self.neg_fields.is_empty()
525            && self.post_effects.is_empty()
526            && self.predicate.is_none()
527            && self.successors.len() <= 1;
528
529        if can_use_match8 {
530            return 8;
531        }
532
533        // Extended match: count all payload slots
534        // Predicate uses 2 slots: op_byte(u8) + is_regex(u8) | value_ref(u16)
535        let predicate_slots = if self.predicate.is_some() { 2 } else { 0 };
536        let slots = self.pre_effects.len()
537            + self.neg_fields.len()
538            + self.post_effects.len()
539            + predicate_slots
540            + self.successors.len();
541
542        select_match_opcode(slots).map(|op| op.size()).unwrap_or(64)
543    }
544
545    /// Resolve labels and serialize to bytecode bytes.
546    ///
547    /// - `lookup_member`: maps (field_name Symbol, field_type TypeId) to member index
548    /// - `get_member_base`: maps parent TypeId to member base index
549    /// - `lookup_regex`: maps pattern to RegexTable index (for predicate regexes)
550    pub fn resolve<F, G, R>(
551        &self,
552        map: &BTreeMap<Label, StepAddr>,
553        lookup_member: F,
554        get_member_base: G,
555        lookup_regex: R,
556    ) -> Vec<u8>
557    where
558        F: Fn(plotnik_core::Symbol, TypeId) -> Option<u16>,
559        G: Fn(TypeId) -> Option<u16>,
560        R: Fn(plotnik_bytecode::StringId) -> Option<u16>,
561    {
562        let can_use_match8 = self.pre_effects.is_empty()
563            && self.neg_fields.is_empty()
564            && self.post_effects.is_empty()
565            && self.predicate.is_none()
566            && self.successors.len() <= 1;
567
568        let opcode = if can_use_match8 {
569            Opcode::Match8
570        } else {
571            let predicate_slots = if self.predicate.is_some() { 2 } else { 0 };
572            let slots_needed = self.pre_effects.len()
573                + self.neg_fields.len()
574                + self.post_effects.len()
575                + predicate_slots
576                + self.successors.len();
577            select_match_opcode(slots_needed).expect("instruction too large")
578        };
579
580        let size = opcode.size();
581        let mut bytes = vec![0u8; size];
582
583        // Header byte layout: segment(2) | node_kind(2) | opcode(4)
584        let (node_kind, node_type_val) = self.node_type.to_bytes();
585        bytes[0] = (node_kind << 4) | (opcode as u8); // segment 0
586        bytes[1] = self.nav.to_byte();
587        bytes[2..4].copy_from_slice(&node_type_val.to_le_bytes());
588        let node_field_val = self.node_field.map(|n| n.get()).unwrap_or(0);
589        bytes[4..6].copy_from_slice(&node_field_val.to_le_bytes());
590
591        if opcode == Opcode::Match8 {
592            let next = self
593                .successors
594                .first()
595                .map(|&l| l.resolve(map))
596                .unwrap_or(0);
597            bytes[6..8].copy_from_slice(&next.to_le_bytes());
598        } else {
599            let pre_count = self.pre_effects.len();
600            let neg_count = self.neg_fields.len();
601            let post_count = self.post_effects.len();
602            let succ_count = self.successors.len();
603            let has_predicate = self.predicate.is_some();
604
605            // Validate bit-packed field limits
606            // counts layout: pre(3) | neg(3) | post(3) | succ(5) | has_pred(1) | reserved(1)
607            assert!(
608                pre_count <= 7,
609                "pre_effects overflow: {pre_count} > 7 (lowering should have cascaded)"
610            );
611            assert!(
612                neg_count <= 7,
613                "neg_fields overflow: {neg_count} > 7 (lowering should have cascaded)"
614            );
615            assert!(
616                post_count <= 7,
617                "post_effects overflow: {post_count} > 7 (lowering should have cascaded)"
618            );
619            assert!(
620                succ_count <= 31,
621                "successors overflow: {succ_count} > 31 (lowering should have cascaded)"
622            );
623
624            let counts = ((pre_count as u16) << 13)
625                | ((neg_count as u16) << 10)
626                | ((post_count as u16) << 7)
627                | ((succ_count as u16) << 2)
628                | ((has_predicate as u16) << 1);
629            bytes[6..8].copy_from_slice(&counts.to_le_bytes());
630
631            let mut offset = 8;
632            for effect in &self.pre_effects {
633                let resolved = effect.resolve(&lookup_member, &get_member_base);
634                bytes[offset..offset + 2].copy_from_slice(&resolved.to_bytes());
635                offset += 2;
636            }
637            for &field in &self.neg_fields {
638                bytes[offset..offset + 2].copy_from_slice(&field.to_le_bytes());
639                offset += 2;
640            }
641            for effect in &self.post_effects {
642                let resolved = effect.resolve(&lookup_member, &get_member_base);
643                bytes[offset..offset + 2].copy_from_slice(&resolved.to_bytes());
644                offset += 2;
645            }
646            // Emit predicate: op_byte(u8) + is_regex(u8) | value_ref(u16)
647            if let Some(pred) = &self.predicate {
648                let is_regex = matches!(pred.value, PredicateValueIR::Regex(_));
649                let op_and_flags = (pred.op_byte() as u16) | ((is_regex as u16) << 8);
650                bytes[offset..offset + 2].copy_from_slice(&op_and_flags.to_le_bytes());
651                offset += 2;
652
653                let value_ref = match &pred.value {
654                    PredicateValueIR::String(string_id) => string_id.get(),
655                    PredicateValueIR::Regex(string_id) => {
656                        lookup_regex(*string_id).expect("regex predicate must be interned")
657                    }
658                };
659                bytes[offset..offset + 2].copy_from_slice(&value_ref.to_le_bytes());
660                offset += 2;
661            }
662            for &label in &self.successors {
663                let addr = label.resolve(map);
664                bytes[offset..offset + 2].copy_from_slice(&addr.to_le_bytes());
665                offset += 2;
666            }
667        }
668
669        bytes
670    }
671
672    /// Check if this is an epsilon transition (no node interaction).
673    #[inline]
674    pub fn is_epsilon(&self) -> bool {
675        self.nav == Nav::Epsilon
676    }
677}
678
679impl From<MatchIR> for InstructionIR {
680    fn from(m: MatchIR) -> Self {
681        Self::Match(m)
682    }
683}
684
685/// Call instruction IR with symbolic target.
686#[derive(Clone, Debug)]
687pub struct CallIR {
688    /// Where this instruction lives.
689    pub label: Label,
690    /// Navigation to apply before jumping to target.
691    pub nav: Nav,
692    /// Field constraint (None = no constraint).
693    pub node_field: Option<NonZeroU16>,
694    /// Return address (where to continue after callee returns).
695    pub next: Label,
696    /// Callee entry point.
697    pub target: Label,
698}
699
700impl CallIR {
701    /// Create a call instruction with default nav (Stay) and no field constraint.
702    pub fn new(label: Label, target: Label, next: Label) -> Self {
703        Self {
704            label,
705            nav: Nav::Stay,
706            node_field: None,
707            next,
708            target,
709        }
710    }
711
712    /// Set the navigation command.
713    pub fn nav(mut self, nav: Nav) -> Self {
714        self.nav = nav;
715        self
716    }
717
718    /// Set the field constraint.
719    pub fn node_field(mut self, f: impl Into<Option<NonZeroU16>>) -> Self {
720        self.node_field = f.into();
721        self
722    }
723
724    /// Resolve labels and serialize to bytecode bytes.
725    pub fn resolve(&self, map: &BTreeMap<Label, StepAddr>) -> [u8; 8] {
726        Call::new(
727            self.nav,
728            self.node_field,
729            StepId::new(self.next.resolve(map)),
730            StepId::new(self.target.resolve(map)),
731        )
732        .to_bytes()
733    }
734}
735
736impl From<CallIR> for InstructionIR {
737    fn from(c: CallIR) -> Self {
738        Self::Call(c)
739    }
740}
741
742/// Return instruction IR.
743#[derive(Clone, Debug)]
744pub struct ReturnIR {
745    /// Where this instruction lives.
746    pub label: Label,
747}
748
749impl ReturnIR {
750    /// Create a return instruction at the given label.
751    pub fn new(label: Label) -> Self {
752        Self { label }
753    }
754
755    /// Serialize to bytecode bytes (no labels to resolve).
756    pub fn resolve(&self) -> [u8; 8] {
757        Return::new().to_bytes()
758    }
759}
760
761impl From<ReturnIR> for InstructionIR {
762    fn from(r: ReturnIR) -> Self {
763        Self::Return(r)
764    }
765}
766
767/// Trampoline instruction IR with symbolic return address.
768///
769/// Trampoline is like Call, but the target comes from VM context (external parameter)
770/// rather than being encoded in the instruction. Used for universal entry preamble.
771#[derive(Clone, Debug)]
772pub struct TrampolineIR {
773    /// Where this instruction lives.
774    pub label: Label,
775    /// Return address (where to continue after entrypoint returns).
776    pub next: Label,
777}
778
779impl TrampolineIR {
780    /// Create a trampoline instruction.
781    pub fn new(label: Label, next: Label) -> Self {
782        Self { label, next }
783    }
784
785    /// Resolve labels and serialize to bytecode bytes.
786    pub fn resolve(&self, map: &BTreeMap<Label, StepAddr>) -> [u8; 8] {
787        Trampoline::new(StepId::new(self.next.resolve(map))).to_bytes()
788    }
789}
790
791impl From<TrampolineIR> for InstructionIR {
792    fn from(t: TrampolineIR) -> Self {
793        Self::Trampoline(t)
794    }
795}
796
797/// Result of layout: maps labels to step addresses.
798#[derive(Clone, Debug)]
799pub struct LayoutResult {
800    /// Mapping from symbolic labels to concrete step addresses (raw u16).
801    pub(crate) label_to_step: BTreeMap<Label, StepAddr>,
802    /// Total number of steps (for header).
803    pub(crate) total_steps: u16,
804}
805
806impl LayoutResult {
807    /// Create a new layout result.
808    pub fn new(label_to_step: BTreeMap<Label, StepAddr>, total_steps: u16) -> Self {
809        Self {
810            label_to_step,
811            total_steps,
812        }
813    }
814
815    /// Create an empty layout result.
816    pub fn empty() -> Self {
817        Self {
818            label_to_step: BTreeMap::new(),
819            total_steps: 0,
820        }
821    }
822
823    pub fn label_to_step(&self) -> &BTreeMap<Label, StepAddr> {
824        &self.label_to_step
825    }
826    pub fn total_steps(&self) -> u16 {
827        self.total_steps
828    }
829}