///|
/// Validate a constant expression (used in data/elem offsets and global init)
/// Returns the result type of the expression
/// Supports extended-const proposal: allows i32/i64 arithmetic (add, sub, mul)
fn validate_const_expr(
globals : ArrayView[@types.GlobalType],
expr : ArrayView[@types.Instruction],
expected_type : @types.ValueType,
funcs~ : Array[Int32] = [],
types~ : Array[@types.SubType] = [],
subtyping_ctx~ : @types.SubtypingContext? = None,
stack_switching~ : Bool = false,
) -> Unit raise ValidationError {
if expr.is_empty() {
raise TypeMismatch("empty constant expression")
}
// Use stack-based type checking for extended constant expressions
let type_stack : Array[@types.ValueType] = []
fn pop_expected(
expected : @types.ValueType,
op : String,
) -> Unit raise ValidationError {
if type_stack.length() < 1 {
raise StackUnderflow("\{op} in constant expression")
}
let actual = type_stack.pop().unwrap()
if !is_const_expr_type_compatible(actual, expected, subtyping_ctx~) {
raise TypeMismatch("\{op}: expected \{expected}, got \{actual}")
}
}
for instr in expr {
match instr {
I32Const(_) => type_stack.push(I32)
I64Const(_) => type_stack.push(I64)
F32Const(_) => type_stack.push(F32)
F64Const(_) => type_stack.push(F64)
V128Const(bytes) => {
if bytes.length() != 16 {
raise TypeMismatch("v128 constant requires 16 bytes")
}
type_stack.push(V128)
}
RefNull(t) => {
validate_value_type(t, types, stack_switching~)
type_stack.push(t)
}
RefFunc(func_idx) => {
if func_idx < 0 || func_idx >= funcs.length() {
raise InvalidFunctionIndex(func_idx)
}
// ref.func produces (ref $t) where $t is the function's type index
let type_idx = funcs[func_idx]
type_stack.push(RefFuncTyped(type_idx))
}
GlobalGet(idx) => {
if idx < 0 || idx >= globals.length() {
raise InvalidGlobalIndex(idx)
}
let global = globals[idx]
if global.mutable {
raise MutableGlobalInConstExpr
}
type_stack.push(global.value_type)
}
RefI31 => {
pop_expected(I32, "ref.i31")
type_stack.push(RefI31)
}
AnyConvertExtern => {
pop_expected(ExternRef, "any.convert_extern")
type_stack.push(AnyRef)
}
ExternConvertAny => {
pop_expected(AnyRef, "extern.convert_any")
type_stack.push(ExternRef)
}
StructNew(type_idx) => {
let struct_type = get_const_expr_struct_type(types, type_idx)
for i = struct_type.fields.length() - 1; i >= 0; i = i - 1 {
let field_ty = const_expr_storage_type_to_value_type(
struct_type.fields[i].storage_type,
)
pop_expected(field_ty, "struct.new")
}
type_stack.push(RefStruct(type_idx))
}
StructNewDefault(type_idx) => {
ignore(get_const_expr_struct_type(types, type_idx))
type_stack.push(RefStruct(type_idx))
}
ArrayNew(type_idx) => {
let array_type = get_const_expr_array_type(types, type_idx)
pop_expected(I32, "array.new length")
let elem_ty = const_expr_storage_type_to_value_type(
array_type.element.storage_type,
)
pop_expected(elem_ty, "array.new value")
type_stack.push(RefArray(type_idx))
}
ArrayNewDefault(type_idx) => {
ignore(get_const_expr_array_type(types, type_idx))
pop_expected(I32, "array.new_default length")
type_stack.push(RefArray(type_idx))
}
ArrayNewFixed(type_idx, len) => {
if len < 0 {
raise TypeMismatch(
"array.new_fixed element count exceeds supported stack size",
)
}
let array_type = get_const_expr_array_type(types, type_idx)
let elem_ty = const_expr_storage_type_to_value_type(
array_type.element.storage_type,
)
for _ in 0.. {
if type_stack.length() < 2 {
raise StackUnderflow("i32 arithmetic in constant expression")
}
let b = type_stack.pop().unwrap()
let a = type_stack.pop().unwrap()
if a != I32 || b != I32 {
raise TypeMismatch("i32 arithmetic requires i32 operands")
}
type_stack.push(I32)
}
// Extended-const: i64 arithmetic
I64Add | I64Sub | I64Mul => {
if type_stack.length() < 2 {
raise StackUnderflow("i64 arithmetic in constant expression")
}
let b = type_stack.pop().unwrap()
let a = type_stack.pop().unwrap()
if a != I64 || b != I64 {
raise TypeMismatch("i64 arithmetic requires i64 operands")
}
type_stack.push(I64)
}
_ =>
// Any other instruction is not allowed in constant expressions
raise ConstantExpressionRequired
}
}
// Constant expression must produce exactly one value
if type_stack.length() != 1 {
raise TypeMismatch(
"constant expression must produce exactly one value, got \{type_stack.length()}",
)
}
// Check the result type matches expected (with subtyping for references)
let result_type = type_stack[0]
if !is_const_expr_type_compatible(result_type, expected_type, subtyping_ctx~) {
raise TypeMismatch(
"constant expression: expected \{expected_type}, got \{result_type}",
)
}
}
///|
fn const_expr_storage_type_to_value_type(
storage_type : @types.StorageType,
) -> @types.ValueType {
match storage_type {
Val(v) => v
Packed(_) => I32
}
}
///|
fn get_const_expr_struct_type(
types : Array[@types.SubType],
type_idx : Int32,
) -> @types.StructType raise ValidationError {
if type_idx < 0 || type_idx >= types.length() {
raise InvalidTypeIndex(type_idx)
}
match types[type_idx].composite {
Struct(s) => s
_ =>
raise TypeMismatch(
"constant expression type \{type_idx} is not a struct type",
)
}
}
///|
fn get_const_expr_array_type(
types : Array[@types.SubType],
type_idx : Int32,
) -> @types.ArrayType raise ValidationError {
if type_idx < 0 || type_idx >= types.length() {
raise InvalidTypeIndex(type_idx)
}
match types[type_idx].composite {
Array(a) => a
_ =>
raise TypeMismatch(
"constant expression type \{type_idx} is not an array type",
)
}
}
///|
/// Check if a result type is compatible with an expected type in constant expressions
fn is_const_expr_type_compatible(
result : @types.ValueType,
expected : @types.ValueType,
subtyping_ctx~ : @types.SubtypingContext? = None,
) -> Bool {
if subtyping_ctx is Some(ctx) {
if ctx.value_type_subtype(result, expected) {
return true
}
}
is_type_subtype(result, expected)
}