submilli-engine 0.6.2

Compiles Submilli programs, a TypeScript subset, to WebAssembly and runs them under capability policy
Documentation
//! Bounds-checked array indexing.
//!
//! `arr[i]` lowers to a raw `array.get`/`array.set`, which traps uncatchably on
//! an out-of-range index. A trap halts the program and can't be intercepted by
//! user `try/catch`, turning a user-controlled index into a DoS vector. Instead
//! we range-check the index. A read that finds no element throws a catchable
//! `MissingValueError` (or gives `undefined` where the typechecker typed it
//! `T | undefined`), and a write outside the array throws `RangeError`,
//! matching how every other runtime fault surfaces (cast mismatch, etc.).

use wasm_encoder::{BlockType, Instruction, ValType};

use crate::codegen::CodegenCtx;
use crate::codegen::function_emitter::{FunctionEmitter, emit_const_string_by_text};
use crate::runtime::prelude::index::IndexAccess;
use crate::{ExprId, TypedAst, TypedExprKind};

/// A bounded, source-like label. Never evaluate a receiver again for diagnostics.
pub(crate) fn receiver_name(
    ta: &TypedAst,
    mut receiver: ExprId,
) -> Result<String, crate::compiler_error::CompilerFailure> {
    let mut fields = Vec::new();
    for _ in 0..8 {
        let expr = ta
            .try_expr(receiver)
            .map_err(crate::codegen::arena_failure)?;
        let mut name = match &expr.kind {
            TypedExprKind::LocalRef { ident, .. } if is_identifier(&ident.name) => {
                ident.name.clone()
            }
            TypedExprKind::GlobalRef { name, .. } if is_identifier(&name.name) => name.name.clone(),
            TypedExprKind::This => "this".to_string(),
            TypedExprKind::LocalNarrowRef { path, .. }
                if path.chain.len() <= 8
                    && is_identifier(&path.root.render())
                    && path.chain.iter().all(|segment| {
                        matches!(
                            segment,
                            crate::typechecker::infer::narrowing::PathElem::Field(name)
                                if is_identifier(name)
                        )
                    }) =>
            {
                path.render()
            }
            TypedExprKind::Narrowed { inner, .. } => {
                receiver = *inner;
                continue;
            }
            TypedExprKind::Cast { value, .. } | TypedExprKind::NonNullAssert { value } => {
                receiver = *value;
                continue;
            }
            TypedExprKind::FieldAccess {
                receiver: base,
                name,
            }
            | TypedExprKind::InterfacePropertyAccess {
                receiver: base,
                name,
                ..
            } => {
                if !is_identifier(&name.name) {
                    return Ok(String::new());
                }
                fields.push(name.name.as_str());
                receiver = *base;
                continue;
            }
            _ => return Ok(String::new()),
        };
        for field in fields.iter().rev() {
            name.push('.');
            name.push_str(field);
        }
        if name.len() <= 64 {
            return Ok(name);
        }
        return Ok(String::new());
    }
    Ok(String::new())
}

pub(crate) fn chain_receiver_name(
    ta: &TypedAst,
    base: ExprId,
    preceding: &[crate::TypedChainPart],
) -> Result<String, crate::compiler_error::CompilerFailure> {
    let mut name = receiver_name(ta, base)?;
    for part in preceding {
        if name.is_empty() {
            break;
        }
        match part {
            crate::TypedChainPart::Field { name: field, .. }
            | crate::TypedChainPart::InterfaceProperty { name: field, .. } => {
                if !is_identifier(&field.name)
                    || name
                        .len()
                        .saturating_add(field.name.len())
                        .saturating_add(1)
                        > 64
                {
                    return Ok(String::new());
                }
                name.push('.');
                name.push_str(&field.name);
            }
            crate::TypedChainPart::NonNull { .. } | crate::TypedChainPart::Constraint { .. } => {}
            _ => return Ok(String::new()),
        }
    }
    Ok(name)
}

fn is_identifier(name: &str) -> bool {
    let mut chars = name.chars();
    chars
        .next()
        .is_some_and(|c| c == '_' || c == '$' || unicode_ident::is_xid_start(c))
        && chars.all(|c| c == '_' || c == '$' || unicode_ident::is_xid_continue(c))
}

/// Stashes the `f64` index sitting on the stack into a fresh local, ready for
/// [`emit_checked_index`]. Kept separate from the check because a write must
/// evaluate its RHS between the two — left-to-right evaluation order puts the
/// RHS's side effects before the check's throw.
pub fn stash_index_operand(
    emitter: &mut FunctionEmitter,
) -> Result<u32, crate::compiler_error::CompilerFailure> {
    let idx_f64_local = emitter.add_anonymous_local(ValType::F64)?;
    emitter.instruction(Instruction::LocalSet(idx_f64_local));
    Ok(idx_f64_local)
}

/// Throws unless `idx >= 0 && idx < len && idx === Math.floor(idx)`, then returns
/// a fresh `i32` local holding the index truncated.
///
/// Checking the `f64` rather than the truncated `i32` is what makes the negative
/// and fractional halves of the domain reachable at all: `i32.trunc_sat_f64_u`
/// saturates `NaN`, `-1`, `-Infinity`, and `0.5` alike to `0`, an in-bounds slot,
/// so a post-truncation `>= len` test sees only indices past the end. Stack is
/// left untouched on the in-bounds path. NaN fails every comparison, so the
/// first conjunct alone rejects it; `-0` passes all three and truncates to `0`,
/// as in JS.
pub(crate) fn emit_checked_index(
    emitter: &mut FunctionEmitter,
    ctx: &CodegenCtx,
    raw_arr_local: u32,
    idx_f64_local: u32,
    receiver: &str,
    access: IndexAccess,
) -> Result<u32, crate::compiler_error::CompilerFailure> {
    let len = emitter.add_anonymous_local(ValType::I32)?;
    emitter.instruction(Instruction::LocalGet(raw_arr_local));
    emitter.instruction(Instruction::ArrayLen);
    emitter.instruction(Instruction::LocalSet(len));
    emit_checked_index_with_length(emitter, ctx, len, idx_f64_local, receiver, access)
}

/// Capture the logical length while leaving the array receiver on the stack.
pub fn stash_array_length(
    emitter: &mut FunctionEmitter,
    array_type: u32,
) -> Result<u32, crate::compiler_error::CompilerFailure> {
    let receiver = emitter.add_anonymous_local(ValType::Ref(wasm_encoder::RefType {
        nullable: false,
        heap_type: wasm_encoder::HeapType::Concrete(array_type),
    }))?;
    let len = emitter.add_anonymous_local(ValType::I32)?;
    emitter.instruction(Instruction::LocalTee(receiver));
    emitter.instruction(Instruction::StructGet {
        struct_type_index: array_type,
        field_index: 2,
    });
    emitter.instruction(Instruction::LocalSet(len));
    emitter.instruction(Instruction::LocalGet(receiver));
    Ok(len)
}

pub(crate) fn emit_checked_index_with_length(
    emitter: &mut FunctionEmitter,
    ctx: &CodegenCtx,
    len: u32,
    idx_f64_local: u32,
    receiver: &str,
    access: IndexAccess,
) -> Result<u32, crate::compiler_error::CompilerFailure> {
    emit_index_in_range(emitter, len, idx_f64_local);
    // throw unless all three hold
    emitter.instruction(Instruction::I32Eqz);
    emitter.emit_if(BlockType::Empty);
    emit_index_oob_throw(emitter, ctx, len, idx_f64_local, receiver, access)?;
    emitter.emit_end();

    let idx_local = emitter.add_anonymous_local(ValType::I32)?;
    emitter.instruction(Instruction::LocalGet(idx_f64_local));
    emitter.instruction(Instruction::I32TruncSatF64U);
    emitter.instruction(Instruction::LocalSet(idx_local));
    Ok(idx_local)
}

/// Pushes whether `idx >= 0 && idx < len && idx === Math.floor(idx)`, as an
/// `i32`: whether a read at the `f64` index finds an element. See
/// [`emit_checked_index`] for why the `f64` is tested.
pub(crate) fn emit_index_in_range(emitter: &mut FunctionEmitter, len: u32, idx_f64_local: u32) {
    // idx >= 0
    emitter.instruction(Instruction::LocalGet(idx_f64_local));
    emitter.instruction(Instruction::F64Const(0.0.into()));
    emitter.instruction(Instruction::F64Ge);
    // && idx < len
    emitter.instruction(Instruction::LocalGet(idx_f64_local));
    emitter.instruction(Instruction::LocalGet(len));
    emitter.instruction(Instruction::F64ConvertI32U);
    emitter.instruction(Instruction::F64Lt);
    emitter.instruction(Instruction::I32And);
    // && idx === floor(idx)
    emitter.instruction(Instruction::LocalGet(idx_f64_local));
    emitter.instruction(Instruction::LocalGet(idx_f64_local));
    emitter.instruction(Instruction::F64Floor);
    emitter.instruction(Instruction::F64Eq);
    emitter.instruction(Instruction::I32And);
}

/// With the element just read from a regex match array on the stack, throws
/// `MissingValueError` when it is `undefined`: a group that did not take part
/// in the match. Leaves the element on the stack otherwise.
pub(crate) fn emit_group_participation_check(
    emitter: &mut FunctionEmitter,
    ctx: &CodegenCtx,
    len: u32,
    idx_f64_local: u32,
    receiver: &str,
) -> Result<(), crate::compiler_error::CompilerFailure> {
    let undefined_idx = ctx
        .symbols
        .intrinsic_type_indices()
        .map(|indices| indices.undefined)
        .ok_or_else(|| crate::codegen::internal_failure("undefined intrinsic registered"))?;
    let element = emitter.add_anonymous_local(ctx.symbols.value_type(&crate::Type::Unknown)?)?;
    emitter.instruction(Instruction::LocalTee(element));
    emitter.instruction(Instruction::RefTestNonNull(
        wasm_encoder::HeapType::Concrete(undefined_idx),
    ));
    emitter.emit_if(BlockType::Empty);
    emit_index_oob_throw(
        emitter,
        ctx,
        len,
        idx_f64_local,
        receiver,
        IndexAccess::Group,
    )?;
    emitter.emit_end();
    emitter.instruction(Instruction::LocalGet(element));
    Ok(())
}

fn emit_index_oob_throw(
    emitter: &mut FunctionEmitter,
    ctx: &CodegenCtx,
    len: u32,
    index: u32,
    receiver: &str,
    access: IndexAccess,
) -> Result<(), crate::compiler_error::CompilerFailure> {
    let helper = ctx
        .symbols
        .prelude_func_idx("index_error")
        .ok_or_else(|| crate::codegen::internal_failure("index error helper imported"))?;
    emitter.instruction(Instruction::LocalGet(index));
    emitter.instruction(Instruction::LocalGet(len));
    emitter.instruction(Instruction::F64ConvertI32U);
    emit_const_string_by_text(emitter, ctx, receiver)?;
    emitter.instruction(Instruction::F64Const((access as u32 as f64).into()));
    emitter.instruction(Instruction::Call(helper));
    // The host helper always returns an error, never a value.
    emitter.instruction(Instruction::Unreachable);
    Ok(())
}