Skip to main content

oxide_core/bytecode/
validate.rs

1//! The publication validator: run once per code unit, so the main loop can
2//! index registers, constants, children and targets without further checks.
3//! A failure is an internal error of the lowering, never a JavaScript
4//! exception.
5
6use std::fmt;
7
8use super::{Capture, CodeUnit, Format, NO_SPILL, Opcode, flags};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum ValidateErrorKind {
12    EmptyCode,
13    /// The last instruction falls through past the end of the code.
14    FallsOffEnd,
15    /// `register_count` is below `1 + parameters + locals`.
16    LayoutOverflow,
17    Register(u32),
18    WideRegister(u32),
19    /// A call's callee, receiver or arguments reach past `register_count`.
20    CallWindow,
21    /// The element range of `NewArray` reaches past `register_count`.
22    RegisterRange,
23    Target(u32),
24    Constant(u32),
25    Child(u32),
26    /// A child id that is not yet published.
27    UnpublishedChild(u32),
28    Capture(u32),
29    /// A child captures a register this unit does not have.
30    ChildCapture(u32),
31    /// `start < end <= code.len()` does not hold.
32    HandlerRange,
33    /// Two handler ranges overlap without nesting, or an enclosing handler
34    /// precedes one nested in it.
35    HandlerNesting,
36    /// The position table is out of `pc` order or names a `pc` past the
37    /// code.
38    LineTable,
39    /// An arrow function marked as a constructor.
40    Flags,
41    /// A cache site at or past the unit's `ic_count`.
42    Site(u32),
43    /// A spill access that does not name the unit's spill register and one
44    /// of its slots.
45    Spill,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct ValidateError {
50    /// The instruction or handler index the error is at, when there is one.
51    pub at: Option<u32>,
52    pub kind: ValidateErrorKind,
53}
54
55impl fmt::Display for ValidateError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        write!(f, "invalid code unit: {:?}", self.kind)?;
58        if let Some(at) = self.at {
59            write!(f, " at {at}")?;
60        }
61        Ok(())
62    }
63}
64
65impl std::error::Error for ValidateError {}
66
67fn fail<T>(at: Option<u32>, kind: ValidateErrorKind) -> Result<T, ValidateError> {
68    Err(ValidateError { at, kind })
69}
70
71/// Checks every operand of `unit`. `published` finds the units already in
72/// the store; children must be among them.
73pub fn validate<'a>(
74    unit: &CodeUnit,
75    published: &dyn Fn(super::CodeUnitId) -> Option<&'a CodeUnit>,
76) -> Result<(), ValidateError> {
77    use ValidateErrorKind as E;
78    let len = unit.code.len() as u32;
79    let registers = unit.register_count as u32;
80    let Some(last) = unit.code.last() else {
81        return fail(None, E::EmptyCode);
82    };
83    if !last.op.is_terminator() {
84        return fail(Some(len - 1), E::FallsOffEnd);
85    }
86    if unit.temp_base() > registers {
87        return fail(None, E::LayoutOverflow);
88    }
89    if unit.has(flags::ARROW) && unit.has(flags::CONSTRUCTOR) {
90        return fail(None, E::Flags);
91    }
92    let positions = &unit.debug.positions;
93    if positions.windows(2).any(|pair| pair[0].pc >= pair[1].pc)
94        || positions
95            .last()
96            .is_some_and(|last| last.pc as usize >= unit.code.len())
97    {
98        return fail(None, E::LineTable);
99    }
100    for (index, &child) in unit.children.iter().enumerate() {
101        let Some(child_unit) = published(child) else {
102            return fail(Some(index as u32), E::UnpublishedChild(child.0));
103        };
104        for capture in &child_unit.captures {
105            match *capture {
106                Capture::None => {}
107                Capture::Register(r) if (r as u32) < registers => {}
108                Capture::Outer(i) if (i as usize) < unit.captures.len() => {}
109                Capture::Slot(i) if unit.spill_register != NO_SPILL && i < unit.spill_slots => {}
110                Capture::Register(r) | Capture::Outer(r) | Capture::Slot(r) => {
111                    return fail(Some(index as u32), E::ChildCapture(r as u32));
112                }
113            }
114        }
115    }
116
117    for (pc, insn) in unit.code.iter().enumerate() {
118        let at = Some(pc as u32);
119        let register = |r: u8| {
120            if (r as u32) < registers {
121                Ok(())
122            } else {
123                fail(at, E::Register(r as u32))
124            }
125        };
126        let target = |t: u32| {
127            if t < len {
128                Ok(())
129            } else {
130                fail(at, E::Target(t))
131            }
132        };
133        match insn.op.format() {
134            Format::None | Format::Key => {}
135            Format::A | Format::AInt | Format::AKey | Format::ACount => {
136                register(insn.a)?;
137            }
138            Format::ACapture => {
139                register(insn.a)?;
140                if insn.imm as usize >= unit.captures.len() {
141                    return fail(at, E::Capture(insn.imm));
142                }
143            }
144            Format::AB | Format::ABInt | Format::ABKey => {
145                register(insn.a)?;
146                register(insn.b)?;
147            }
148            Format::ABC => {
149                register(insn.a)?;
150                register(insn.b)?;
151                register(insn.c)?;
152            }
153            Format::AConst => {
154                register(insn.a)?;
155                // A regular expression reads its source and its flags.
156                let last = insn.imm as u64 + u64::from(insn.op == Opcode::NewRegExp);
157                if last >= u64::from(unit.constant_count) {
158                    return fail(at, E::Constant(insn.imm));
159                }
160            }
161            Format::ABRange => {
162                register(insn.a)?;
163                if insn.imm > 0 && insn.b as u64 + insn.imm as u64 > registers as u64 {
164                    return fail(at, E::RegisterRange);
165                }
166            }
167            Format::AChild => {
168                register(insn.a)?;
169                if insn.imm as usize >= unit.children.len() {
170                    return fail(at, E::Child(insn.imm));
171                }
172            }
173            Format::Call => {
174                register(insn.a)?;
175                // Call and TailCall: callee, arguments. The others: receiver
176                // or callee, then callee or new.target, then arguments.
177                let operands = match insn.op {
178                    Opcode::Call | Opcode::TailCall => 1,
179                    Opcode::CallSpread | Opcode::NewSpread => 3,
180                    _ => 2,
181                };
182                if insn.b as u32 + operands + insn.c as u32 > registers {
183                    return fail(at, E::CallWindow);
184                }
185            }
186            Format::Jump => target(insn.imm)?,
187            Format::AJump | Format::AShortJump => {
188                register(insn.a)?;
189                target(insn.imm)?;
190            }
191            Format::ABJump => {
192                register(insn.a)?;
193                register(insn.b)?;
194                target(insn.imm)?;
195            }
196            Format::AWide => {
197                register(insn.a)?;
198                if insn.imm >= registers {
199                    return fail(at, E::WideRegister(insn.imm));
200                }
201            }
202        }
203        // A spill access names the unit's spill register and one of its slots.
204        if matches!(
205            insn.op,
206            Opcode::LoadSlot | Opcode::StoreSlot | Opcode::NewSpill
207        ) {
208            let spill = if insn.op == Opcode::NewSpill {
209                insn.a
210            } else {
211                insn.b
212            };
213            let slot = if insn.op == Opcode::NewSpill {
214                u32::from(unit.spill_slots).saturating_sub(1)
215            } else {
216                insn.imm
217            };
218            if unit.spill_register == NO_SPILL
219                || u16::from(spill) != unit.spill_register
220                || (insn.op != Opcode::NewSpill && slot >= u32::from(unit.spill_slots))
221                || (insn.op == Opcode::NewSpill && insn.imm != u32::from(unit.spill_slots))
222            {
223                return fail(at, E::Spill);
224            }
225        }
226        if let Some(site) = insn.site()
227            && site >= unit.ic_count
228        {
229            return fail(at, E::Site(site));
230        }
231    }
232
233    for (index, handler) in unit.handlers.iter().enumerate() {
234        let at = Some(index as u32);
235        if handler.start >= handler.end || handler.end > len {
236            return fail(at, E::HandlerRange);
237        }
238        if handler.target >= len {
239            return fail(at, E::Target(handler.target));
240        }
241        if handler.register as u32 >= registers {
242            return fail(at, E::Register(handler.register as u32));
243        }
244        // Lookup takes the first covering entry, so an entry may only be
245        // followed by entries that are disjoint from it or enclose it.
246        for later in &unit.handlers[index + 1..] {
247            let disjoint = later.end <= handler.start || handler.end <= later.start;
248            let encloses = later.start <= handler.start && handler.end <= later.end;
249            if !disjoint && !encloses {
250                return fail(at, E::HandlerNesting);
251            }
252        }
253    }
254    Ok(())
255}