Skip to main content

submilli_engine/codegen/
bounds.rs

1//! Bounds-checked array indexing.
2//!
3//! `arr[i]` lowers to a raw `array.get`/`array.set`, which traps uncatchably on
4//! an out-of-range index. A trap halts the program and can't be intercepted by
5//! user `try/catch`, turning a user-controlled index into a DoS vector. Instead
6//! we range-check the index and `throw` a catchable `RangeError` on a miss,
7//! matching how every other runtime fault surfaces (cast mismatch, etc.).
8
9use wasm_encoder::{BlockType, Instruction, ValType};
10
11use crate::codegen::CodegenCtx;
12use crate::codegen::function_emitter::{FunctionEmitter, emit_const_string_by_text};
13
14pub const INDEX_OOB_MESSAGE: &str = "index out of range";
15
16/// Stashes the `f64` index sitting on the stack into a fresh local, ready for
17/// [`emit_checked_index`]. Kept separate from the check because a write must
18/// evaluate its RHS between the two — left-to-right evaluation order puts the
19/// RHS's side effects before the check's throw.
20pub fn stash_index_operand(
21    emitter: &mut FunctionEmitter,
22) -> Result<u32, crate::compiler_error::CompilerFailure> {
23    let idx_f64_local = emitter.add_anonymous_local(ValType::F64)?;
24    emitter.instruction(Instruction::LocalSet(idx_f64_local));
25    Ok(idx_f64_local)
26}
27
28/// Throws unless `idx >= 0 && idx < len && idx === Math.floor(idx)`, then returns
29/// a fresh `i32` local holding the index truncated.
30///
31/// Checking the `f64` rather than the truncated `i32` is what makes the negative
32/// and fractional halves of the domain reachable at all: `i32.trunc_sat_f64_u`
33/// saturates `NaN`, `-1`, `-Infinity`, and `0.5` alike to `0`, an in-bounds slot,
34/// so a post-truncation `>= len` test sees only indices past the end. Stack is
35/// left untouched on the in-bounds path. NaN fails every comparison, so the
36/// first conjunct alone rejects it; `-0` passes all three and truncates to `0`,
37/// as in JS.
38pub fn emit_checked_index(
39    emitter: &mut FunctionEmitter,
40    ctx: &CodegenCtx,
41    raw_arr_local: u32,
42    idx_f64_local: u32,
43) -> Result<u32, crate::compiler_error::CompilerFailure> {
44    let len = emitter.add_anonymous_local(ValType::I32)?;
45    emitter.instruction(Instruction::LocalGet(raw_arr_local));
46    emitter.instruction(Instruction::ArrayLen);
47    emitter.instruction(Instruction::LocalSet(len));
48    emit_checked_index_with_length(emitter, ctx, len, idx_f64_local)
49}
50
51/// Capture the logical length while leaving the array receiver on the stack.
52pub fn stash_array_length(
53    emitter: &mut FunctionEmitter,
54    array_type: u32,
55) -> Result<u32, crate::compiler_error::CompilerFailure> {
56    let receiver = emitter.add_anonymous_local(ValType::Ref(wasm_encoder::RefType {
57        nullable: false,
58        heap_type: wasm_encoder::HeapType::Concrete(array_type),
59    }))?;
60    let len = emitter.add_anonymous_local(ValType::I32)?;
61    emitter.instruction(Instruction::LocalTee(receiver));
62    emitter.instruction(Instruction::StructGet {
63        struct_type_index: array_type,
64        field_index: 2,
65    });
66    emitter.instruction(Instruction::LocalSet(len));
67    emitter.instruction(Instruction::LocalGet(receiver));
68    Ok(len)
69}
70
71pub fn emit_checked_index_with_length(
72    emitter: &mut FunctionEmitter,
73    ctx: &CodegenCtx,
74    len: u32,
75    idx_f64_local: u32,
76) -> Result<u32, crate::compiler_error::CompilerFailure> {
77    // idx >= 0
78    emitter.instruction(Instruction::LocalGet(idx_f64_local));
79    emitter.instruction(Instruction::F64Const(0.0.into()));
80    emitter.instruction(Instruction::F64Ge);
81    // && idx < len
82    emitter.instruction(Instruction::LocalGet(idx_f64_local));
83    emitter.instruction(Instruction::LocalGet(len));
84    emitter.instruction(Instruction::F64ConvertI32U);
85    emitter.instruction(Instruction::F64Lt);
86    emitter.instruction(Instruction::I32And);
87    // && idx === floor(idx)
88    emitter.instruction(Instruction::LocalGet(idx_f64_local));
89    emitter.instruction(Instruction::LocalGet(idx_f64_local));
90    emitter.instruction(Instruction::F64Floor);
91    emitter.instruction(Instruction::F64Eq);
92    emitter.instruction(Instruction::I32And);
93    // throw unless all three hold
94    emitter.instruction(Instruction::I32Eqz);
95    emitter.emit_if(BlockType::Empty);
96    emit_index_oob_throw(emitter, ctx);
97    emitter.emit_end();
98
99    let idx_local = emitter.add_anonymous_local(ValType::I32)?;
100    emitter.instruction(Instruction::LocalGet(idx_f64_local));
101    emitter.instruction(Instruction::I32TruncSatF64U);
102    emitter.instruction(Instruction::LocalSet(idx_local));
103    Ok(idx_local)
104}
105
106fn emit_index_oob_throw(emitter: &mut FunctionEmitter, ctx: &CodegenCtx) {
107    if ctx
108        .latch(emit_const_string_by_text(emitter, ctx, INDEX_OOB_MESSAGE))
109        .is_none()
110    {
111        return;
112    }
113
114    let Some(new_idx) = ctx.latch(
115        ctx.symbols
116            .prelude_func_idx("RangeError#constructor")
117            .ok_or_else(|| crate::codegen::internal_failure("RangeError constructor missing")),
118    ) else {
119        return;
120    };
121    emitter.instruction(Instruction::Call(new_idx));
122
123    crate::codegen::throw::emit_error_throw(emitter, ctx);
124}