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. A read that finds no element throws a catchable
7//! `MissingValueError` (or gives `undefined` where the typechecker typed it
8//! `T | undefined`), and a write outside the array throws `RangeError`,
9//! matching how every other runtime fault surfaces (cast mismatch, etc.).
10
11use wasm_encoder::{BlockType, Instruction, ValType};
12
13use crate::codegen::CodegenCtx;
14use crate::codegen::function_emitter::{FunctionEmitter, emit_const_string_by_text};
15use crate::runtime::prelude::index::IndexAccess;
16use crate::{ExprId, TypedAst, TypedExprKind};
17
18/// A bounded, source-like label. Never evaluate a receiver again for diagnostics.
19pub(crate) fn receiver_name(
20    ta: &TypedAst,
21    mut receiver: ExprId,
22) -> Result<String, crate::compiler_error::CompilerFailure> {
23    let mut fields = Vec::new();
24    for _ in 0..8 {
25        let expr = ta
26            .try_expr(receiver)
27            .map_err(crate::codegen::arena_failure)?;
28        let mut name = match &expr.kind {
29            TypedExprKind::LocalRef { ident, .. } if is_identifier(&ident.name) => {
30                ident.name.clone()
31            }
32            TypedExprKind::GlobalRef { name, .. } if is_identifier(&name.name) => name.name.clone(),
33            TypedExprKind::This => "this".to_string(),
34            TypedExprKind::LocalNarrowRef { path, .. }
35                if path.chain.len() <= 8
36                    && is_identifier(&path.root.render())
37                    && path.chain.iter().all(|segment| {
38                        matches!(
39                            segment,
40                            crate::typechecker::infer::narrowing::PathElem::Field(name)
41                                if is_identifier(name)
42                        )
43                    }) =>
44            {
45                path.render()
46            }
47            TypedExprKind::Narrowed { inner, .. } => {
48                receiver = *inner;
49                continue;
50            }
51            TypedExprKind::Cast { value, .. } | TypedExprKind::NonNullAssert { value } => {
52                receiver = *value;
53                continue;
54            }
55            TypedExprKind::FieldAccess {
56                receiver: base,
57                name,
58            }
59            | TypedExprKind::InterfacePropertyAccess {
60                receiver: base,
61                name,
62                ..
63            } => {
64                if !is_identifier(&name.name) {
65                    return Ok(String::new());
66                }
67                fields.push(name.name.as_str());
68                receiver = *base;
69                continue;
70            }
71            _ => return Ok(String::new()),
72        };
73        for field in fields.iter().rev() {
74            name.push('.');
75            name.push_str(field);
76        }
77        if name.len() <= 64 {
78            return Ok(name);
79        }
80        return Ok(String::new());
81    }
82    Ok(String::new())
83}
84
85pub(crate) fn chain_receiver_name(
86    ta: &TypedAst,
87    base: ExprId,
88    preceding: &[crate::TypedChainPart],
89) -> Result<String, crate::compiler_error::CompilerFailure> {
90    let mut name = receiver_name(ta, base)?;
91    for part in preceding {
92        if name.is_empty() {
93            break;
94        }
95        match part {
96            crate::TypedChainPart::Field { name: field, .. }
97            | crate::TypedChainPart::InterfaceProperty { name: field, .. } => {
98                if !is_identifier(&field.name)
99                    || name
100                        .len()
101                        .saturating_add(field.name.len())
102                        .saturating_add(1)
103                        > 64
104                {
105                    return Ok(String::new());
106                }
107                name.push('.');
108                name.push_str(&field.name);
109            }
110            crate::TypedChainPart::NonNull { .. } | crate::TypedChainPart::Constraint { .. } => {}
111            _ => return Ok(String::new()),
112        }
113    }
114    Ok(name)
115}
116
117fn is_identifier(name: &str) -> bool {
118    let mut chars = name.chars();
119    chars
120        .next()
121        .is_some_and(|c| c == '_' || c == '$' || unicode_ident::is_xid_start(c))
122        && chars.all(|c| c == '_' || c == '$' || unicode_ident::is_xid_continue(c))
123}
124
125/// Stashes the `f64` index sitting on the stack into a fresh local, ready for
126/// [`emit_checked_index`]. Kept separate from the check because a write must
127/// evaluate its RHS between the two — left-to-right evaluation order puts the
128/// RHS's side effects before the check's throw.
129pub fn stash_index_operand(
130    emitter: &mut FunctionEmitter,
131) -> Result<u32, crate::compiler_error::CompilerFailure> {
132    let idx_f64_local = emitter.add_anonymous_local(ValType::F64)?;
133    emitter.instruction(Instruction::LocalSet(idx_f64_local));
134    Ok(idx_f64_local)
135}
136
137/// Throws unless `idx >= 0 && idx < len && idx === Math.floor(idx)`, then returns
138/// a fresh `i32` local holding the index truncated.
139///
140/// Checking the `f64` rather than the truncated `i32` is what makes the negative
141/// and fractional halves of the domain reachable at all: `i32.trunc_sat_f64_u`
142/// saturates `NaN`, `-1`, `-Infinity`, and `0.5` alike to `0`, an in-bounds slot,
143/// so a post-truncation `>= len` test sees only indices past the end. Stack is
144/// left untouched on the in-bounds path. NaN fails every comparison, so the
145/// first conjunct alone rejects it; `-0` passes all three and truncates to `0`,
146/// as in JS.
147pub(crate) fn emit_checked_index(
148    emitter: &mut FunctionEmitter,
149    ctx: &CodegenCtx,
150    raw_arr_local: u32,
151    idx_f64_local: u32,
152    receiver: &str,
153    access: IndexAccess,
154) -> Result<u32, crate::compiler_error::CompilerFailure> {
155    let len = emitter.add_anonymous_local(ValType::I32)?;
156    emitter.instruction(Instruction::LocalGet(raw_arr_local));
157    emitter.instruction(Instruction::ArrayLen);
158    emitter.instruction(Instruction::LocalSet(len));
159    emit_checked_index_with_length(emitter, ctx, len, idx_f64_local, receiver, access)
160}
161
162/// Capture the logical length while leaving the array receiver on the stack.
163pub fn stash_array_length(
164    emitter: &mut FunctionEmitter,
165    array_type: u32,
166) -> Result<u32, crate::compiler_error::CompilerFailure> {
167    let receiver = emitter.add_anonymous_local(ValType::Ref(wasm_encoder::RefType {
168        nullable: false,
169        heap_type: wasm_encoder::HeapType::Concrete(array_type),
170    }))?;
171    let len = emitter.add_anonymous_local(ValType::I32)?;
172    emitter.instruction(Instruction::LocalTee(receiver));
173    emitter.instruction(Instruction::StructGet {
174        struct_type_index: array_type,
175        field_index: 2,
176    });
177    emitter.instruction(Instruction::LocalSet(len));
178    emitter.instruction(Instruction::LocalGet(receiver));
179    Ok(len)
180}
181
182pub(crate) fn emit_checked_index_with_length(
183    emitter: &mut FunctionEmitter,
184    ctx: &CodegenCtx,
185    len: u32,
186    idx_f64_local: u32,
187    receiver: &str,
188    access: IndexAccess,
189) -> Result<u32, crate::compiler_error::CompilerFailure> {
190    emit_index_in_range(emitter, len, idx_f64_local);
191    // throw unless all three hold
192    emitter.instruction(Instruction::I32Eqz);
193    emitter.emit_if(BlockType::Empty);
194    emit_index_oob_throw(emitter, ctx, len, idx_f64_local, receiver, access)?;
195    emitter.emit_end();
196
197    let idx_local = emitter.add_anonymous_local(ValType::I32)?;
198    emitter.instruction(Instruction::LocalGet(idx_f64_local));
199    emitter.instruction(Instruction::I32TruncSatF64U);
200    emitter.instruction(Instruction::LocalSet(idx_local));
201    Ok(idx_local)
202}
203
204/// Pushes whether `idx >= 0 && idx < len && idx === Math.floor(idx)`, as an
205/// `i32`: whether a read at the `f64` index finds an element. See
206/// [`emit_checked_index`] for why the `f64` is tested.
207pub(crate) fn emit_index_in_range(emitter: &mut FunctionEmitter, len: u32, idx_f64_local: u32) {
208    // idx >= 0
209    emitter.instruction(Instruction::LocalGet(idx_f64_local));
210    emitter.instruction(Instruction::F64Const(0.0.into()));
211    emitter.instruction(Instruction::F64Ge);
212    // && idx < len
213    emitter.instruction(Instruction::LocalGet(idx_f64_local));
214    emitter.instruction(Instruction::LocalGet(len));
215    emitter.instruction(Instruction::F64ConvertI32U);
216    emitter.instruction(Instruction::F64Lt);
217    emitter.instruction(Instruction::I32And);
218    // && idx === floor(idx)
219    emitter.instruction(Instruction::LocalGet(idx_f64_local));
220    emitter.instruction(Instruction::LocalGet(idx_f64_local));
221    emitter.instruction(Instruction::F64Floor);
222    emitter.instruction(Instruction::F64Eq);
223    emitter.instruction(Instruction::I32And);
224}
225
226/// With the element just read from a regex match array on the stack, throws
227/// `MissingValueError` when it is `undefined`: a group that did not take part
228/// in the match. Leaves the element on the stack otherwise.
229pub(crate) fn emit_group_participation_check(
230    emitter: &mut FunctionEmitter,
231    ctx: &CodegenCtx,
232    len: u32,
233    idx_f64_local: u32,
234    receiver: &str,
235) -> Result<(), crate::compiler_error::CompilerFailure> {
236    let undefined_idx = ctx
237        .symbols
238        .intrinsic_type_indices()
239        .map(|indices| indices.undefined)
240        .ok_or_else(|| crate::codegen::internal_failure("undefined intrinsic registered"))?;
241    let element = emitter.add_anonymous_local(ctx.symbols.value_type(&crate::Type::Unknown)?)?;
242    emitter.instruction(Instruction::LocalTee(element));
243    emitter.instruction(Instruction::RefTestNonNull(
244        wasm_encoder::HeapType::Concrete(undefined_idx),
245    ));
246    emitter.emit_if(BlockType::Empty);
247    emit_index_oob_throw(
248        emitter,
249        ctx,
250        len,
251        idx_f64_local,
252        receiver,
253        IndexAccess::Group,
254    )?;
255    emitter.emit_end();
256    emitter.instruction(Instruction::LocalGet(element));
257    Ok(())
258}
259
260fn emit_index_oob_throw(
261    emitter: &mut FunctionEmitter,
262    ctx: &CodegenCtx,
263    len: u32,
264    index: u32,
265    receiver: &str,
266    access: IndexAccess,
267) -> Result<(), crate::compiler_error::CompilerFailure> {
268    let helper = ctx
269        .symbols
270        .prelude_func_idx("index_error")
271        .ok_or_else(|| crate::codegen::internal_failure("index error helper imported"))?;
272    emitter.instruction(Instruction::LocalGet(index));
273    emitter.instruction(Instruction::LocalGet(len));
274    emitter.instruction(Instruction::F64ConvertI32U);
275    emit_const_string_by_text(emitter, ctx, receiver)?;
276    emitter.instruction(Instruction::F64Const((access as u32 as f64).into()));
277    emitter.instruction(Instruction::Call(helper));
278    // The host helper always returns an error, never a value.
279    emitter.instruction(Instruction::Unreachable);
280    Ok(())
281}