// IR Validator
// Validates that IR is well-formed and type-correct
///|
/// Result of IR validation
pub struct ValidationResult {
mut valid : Bool
errors : Array[String]
}
///|
pub fn ValidationResult::new() -> ValidationResult {
{ valid: true, errors: [] }
}
///|
fn ValidationResult::add_error(self : ValidationResult, error : String) -> Unit {
self.valid = false
self.errors.push(error)
}
///|
/// Validate a function's IR
pub fn validate_function(func : Function) -> ValidationResult {
let result = ValidationResult::new()
// Track all defined values
let defined_values : @hashmap.HashMap[Int, Type] = HashMap([])
// Add function parameters as defined
for param in func.params {
let (v, ty) = param
defined_values.set(v.id, ty)
}
// Validate each block
for block in func.blocks {
// Add block parameters as defined
for param in block.params {
let (v, ty) = param
defined_values.set(v.id, ty)
}
// Validate instructions
for inst in block.instructions {
validate_instruction(inst, defined_values, result)
// Add result to defined values
if inst.first_result() is Some(v) {
defined_values.set(v.id, v.ty)
}
}
// Validate terminator
match block.terminator {
Some(term) => validate_terminator(term, defined_values, func, result)
None => result.add_error("Block \{block.id} has no terminator")
}
}
// Check that entry block exists
if func.blocks.length() == 0 {
result.add_error("Function has no blocks")
}
result
}
///|
/// Validate an instruction
fn validate_instruction(
inst : Inst,
defined : @hashmap.HashMap[Int, Type],
result : ValidationResult,
) -> Unit {
// Check that all operands are defined
for op in inst.operands {
if !defined.contains(op.id) {
result.add_error("Operand v\{op.id} is not defined")
}
}
// Type-check based on opcode
match inst.opcode {
// Binary integer operations
Iadd
| Isub
| Imul
| Umulh
| Smulh
| Sdiv
| Udiv
| Srem
| Urem
| Band
| Bor
| Bxor
| Ishl
| Sshr
| Ushr
| Rotl
| Rotr =>
if inst.operands.length() != 2 {
result.add_error(
"Binary operation expects 2 operands, got \{inst.operands.length()}",
)
} else {
let a = inst.operands[0]
let b = inst.operands[1]
// Operands should have matching integer types
if !is_integer_type(a.ty) {
result.add_error(
"Binary integer op expects integer operand, got \{a.ty}",
)
}
if !types_match(a.ty, b.ty) {
result.add_error(
"Binary op operands have mismatched types: \{a.ty} vs \{b.ty}",
)
}
// mulhi ops are only defined for i64
if (inst.opcode is Umulh || inst.opcode is Smulh) &&
!types_match(a.ty, I64) {
result.add_error("mulhi expects i64 operands, got \{a.ty}")
}
}
// Bitwise not (unary)
Bnot =>
if inst.operands.length() != 1 {
result.add_error(
"Bnot expects 1 operand, got \{inst.operands.length()}",
)
}
// Bit counting operations (unary)
Clz | Ctz | Popcnt =>
if inst.operands.length() != 1 {
result.add_error(
"Bit counting op expects 1 operand, got \{inst.operands.length()}",
)
}
// Integer comparisons
Icmp(_) => {
if inst.operands.length() != 2 {
result.add_error(
"Icmp expects 2 operands, got \{inst.operands.length()}",
)
}
// Result should be i32
match inst.first_result() {
Some(r) =>
if !types_match(r.ty, I32) {
result.add_error("Icmp result should be i32, got \{r.ty}")
}
None => ()
}
}
// Binary float operations
Fadd | Fsub | Fmul | Fdiv | Fmin | Fmax =>
if inst.operands.length() != 2 {
result.add_error(
"Binary float operation expects 2 operands, got \{inst.operands.length()}",
)
} else {
let a = inst.operands[0]
let b = inst.operands[1]
if !is_float_type(a.ty) {
result.add_error("Binary float op expects float operand, got \{a.ty}")
}
if !types_match(a.ty, b.ty) {
result.add_error(
"Binary op operands have mismatched types: \{a.ty} vs \{b.ty}",
)
}
}
// Float comparisons
Fcmp(_) => {
if inst.operands.length() != 2 {
result.add_error(
"Fcmp expects 2 operands, got \{inst.operands.length()}",
)
}
// Result should be i32
match inst.first_result() {
Some(r) =>
if !types_match(r.ty, I32) {
result.add_error("Fcmp result should be i32, got \{r.ty}")
}
None => ()
}
}
// Unary float operations
Fneg | Fabs | Fsqrt | Fceil | Ffloor | Ftrunc | Fnearest =>
if inst.operands.length() != 1 {
result.add_error(
"Unary float op expects 1 operand, got \{inst.operands.length()}",
)
}
// Constants don't have operands
Iconst(_) | Fconst(_) =>
if inst.operands.length() != 0 {
result.add_error(
"Constant expects 0 operands, got \{inst.operands.length()}",
)
}
// Select needs 3 operands (cond, true_val, false_val)
Select =>
if inst.operands.length() != 3 {
result.add_error(
"Select expects 3 operands, got \{inst.operands.length()}",
)
}
// Note: Load/Store opcodes removed - now use LoadPtr/StorePtr
// Conversions need 1 operand
Ireduce
| Sextend
| Uextend
| Sextend8
| Sextend16
| Sextend32
| Fpromote
| Fdemote
| FcvtToSint
| FcvtToUint
| FcvtToSintSat
| FcvtToUintSat
| SintToFcvt
| UintToFcvt
| Bitcast =>
if inst.operands.length() != 1 {
result.add_error(
"Conversion expects 1 operand, got \{inst.operands.length()}",
)
}
// Copy needs 1 operand
Copy =>
if inst.operands.length() != 1 {
result.add_error(
"Copy expects 1 operand, got \{inst.operands.length()}",
)
}
// Dialect extension operands are validated by dialect-owned adapters.
Ext(_) => ()
// Raw pointer calls can have any number of operands.
CallPtr(_, _) => ()
// Raw pointer operations (for trampolines)
LoadPtr(_) =>
if inst.operands.length() < 1 {
result.add_error(
"LoadPtr expects at least 1 operand (base), got \{inst.operands.length()}",
)
}
StorePtr(_) =>
if inst.operands.length() < 2 {
result.add_error(
"StorePtr expects at least 2 operands (base, value), got \{inst.operands.length()}",
)
}
LoadPtrNarrow(_, _, _) =>
if inst.operands.length() < 1 {
result.add_error(
"LoadPtrNarrow expects at least 1 operand (base), got \{inst.operands.length()}",
)
}
StorePtrNarrow(_) =>
if inst.operands.length() < 2 {
result.add_error(
"StorePtrNarrow expects at least 2 operands (base, value), got \{inst.operands.length()}",
)
}
opcode => validate_simd_instruction(opcode, inst, result)
}
}
///|
fn validate_simd_instruction(
opcode : Opcode,
inst : Inst,
result : ValidationResult,
) -> Unit {
match opcode {
V128Const(_) =>
if validate_simd_operand_count(inst, result, 0) {
validate_simd_result_type(inst, result, V128)
}
V128Splat8 | V128Splat16 | V128Splat32 =>
validate_simd_splat(inst, result, I32)
V128Splat64 => validate_simd_splat(inst, result, I64)
V128SplatF32 => validate_simd_splat(inst, result, F32)
V128SplatF64 => validate_simd_splat(inst, result, F64)
V128ExtractLane8S(_)
| V128ExtractLane8U(_)
| V128ExtractLane16S(_)
| V128ExtractLane16U(_)
| V128ExtractLane32(_) => validate_simd_extract_lane(inst, result, I32)
V128ExtractLane64(_) => validate_simd_extract_lane(inst, result, I64)
V128ExtractLaneF32(_) => validate_simd_extract_lane(inst, result, F32)
V128ExtractLaneF64(_) => validate_simd_extract_lane(inst, result, F64)
V128ReplaceLane8(_) | V128ReplaceLane16(_) | V128ReplaceLane32(_) =>
validate_simd_replace_lane(inst, result, I32)
V128ReplaceLane64(_) => validate_simd_replace_lane(inst, result, I64)
V128ReplaceLaneF32(_) => validate_simd_replace_lane(inst, result, F32)
V128ReplaceLaneF64(_) => validate_simd_replace_lane(inst, result, F64)
V128AnyTrue
| V128AllTrue8
| V128AllTrue16
| V128AllTrue32
| V128AllTrue64
| V128Bitmask8
| V128Bitmask16
| V128Bitmask32
| V128Bitmask64 => validate_simd_unary_v128_to_i32(inst, result)
V128Load8x8S(_, _, _)
| V128Load8x8U(_, _, _)
| V128Load16x4S(_, _, _)
| V128Load16x4U(_, _, _)
| V128Load32x2S(_, _, _)
| V128Load32x2U(_, _, _)
| V128Load8Splat(_, _, _)
| V128Load16Splat(_, _, _)
| V128Load32Splat(_, _, _)
| V128Load64Splat(_, _, _)
| V128Load32Zero(_, _, _)
| V128Load64Zero(_, _, _) => validate_simd_load(inst, result)
V128Load8Lane(_, _, _, _)
| V128Load16Lane(_, _, _, _)
| V128Load32Lane(_, _, _, _)
| V128Load64Lane(_, _, _, _) => validate_simd_load_lane(inst, result)
V128Store8Lane(_, _, _, _)
| V128Store16Lane(_, _, _, _)
| V128Store32Lane(_, _, _, _)
| V128Store64Lane(_, _, _, _) => validate_simd_store_lane(inst, result)
V128Shl8
| V128Shl16
| V128Shl32
| V128Shl64
| V128Shr8S
| V128Shr8U
| V128Shr16S
| V128Shr16U
| V128Shr32S
| V128Shr32U
| V128Shr64S
| V128Shr64U => validate_simd_shift(inst, result)
V128Bitselect
| V128RelaxedMaddF32
| V128RelaxedNmaddF32
| V128RelaxedMaddF64
| V128RelaxedNmaddF64
| V128RelaxedLaneselect8
| V128RelaxedLaneselect16
| V128RelaxedLaneselect32
| V128RelaxedLaneselect64
| V128RelaxedDot8to32AddS =>
validate_simd_ternary_v128_to_v128(inst, result)
V128Not
| V128Abs8
| V128Abs16
| V128Abs32
| V128Abs64
| V128Neg8
| V128Neg16
| V128Neg32
| V128Neg64
| V128Popcnt8
| V128ExtendLow8to16S
| V128ExtendHigh8to16S
| V128ExtendLow8to16U
| V128ExtendHigh8to16U
| V128ExtendLow16to32S
| V128ExtendHigh16to32S
| V128ExtendLow16to32U
| V128ExtendHigh16to32U
| V128ExtendLow32to64S
| V128ExtendHigh32to64S
| V128ExtendLow32to64U
| V128ExtendHigh32to64U
| V128ExtAddPairwise8to16S
| V128ExtAddPairwise8to16U
| V128ExtAddPairwise16to32S
| V128ExtAddPairwise16to32U
| V128AbsF32
| V128AbsF64
| V128NegF32
| V128NegF64
| V128SqrtF32
| V128SqrtF64
| V128CeilF32
| V128CeilF64
| V128FloorF32
| V128FloorF64
| V128TruncF32
| V128TruncF64
| V128NearestF32
| V128NearestF64
| V128TruncSatF32toI32S
| V128TruncSatF32toI32U
| V128TruncSatF64toI32SZero
| V128TruncSatF64toI32UZero
| V128ConvertI32toF32S
| V128ConvertI32toF32U
| V128ConvertLowI32toF64S
| V128ConvertLowI32toF64U
| V128DemoteF64toF32Zero
| V128PromoteLowF32toF64
| V128RelaxedTruncF32toI32S
| V128RelaxedTruncF32toI32U
| V128RelaxedTruncF64toI32SZero
| V128RelaxedTruncF64toI32UZero =>
validate_simd_unary_v128_to_v128(inst, result)
V128Shuffle(_)
| V128Swizzle
| V128And
| V128AndNot
| V128Or
| V128Xor
| V128Add8
| V128Add16
| V128Add32
| V128Add64
| V128Sub8
| V128Sub16
| V128Sub32
| V128Sub64
| V128Mul16
| V128Mul32
| V128Mul64
| V128AddSat8S
| V128AddSat8U
| V128AddSat16S
| V128AddSat16U
| V128SubSat8S
| V128SubSat8U
| V128SubSat16S
| V128SubSat16U
| V128Min8S
| V128Min8U
| V128Min16S
| V128Min16U
| V128Min32S
| V128Min32U
| V128Max8S
| V128Max8U
| V128Max16S
| V128Max16U
| V128Max32S
| V128Max32U
| V128Avgr8U
| V128Avgr16U
| V128Eq8
| V128Eq16
| V128Eq32
| V128Eq64
| V128Ne8
| V128Ne16
| V128Ne32
| V128Ne64
| V128Lt8S
| V128Lt8U
| V128Lt16S
| V128Lt16U
| V128Lt32S
| V128Lt32U
| V128Lt64S
| V128Gt8S
| V128Gt8U
| V128Gt16S
| V128Gt16U
| V128Gt32S
| V128Gt32U
| V128Gt64S
| V128Le8S
| V128Le8U
| V128Le16S
| V128Le16U
| V128Le32S
| V128Le32U
| V128Le64S
| V128Ge8S
| V128Ge8U
| V128Ge16S
| V128Ge16U
| V128Ge32S
| V128Ge32U
| V128Ge64S
| V128Narrow16to8S
| V128Narrow16to8U
| V128Narrow32to16S
| V128Narrow32to16U
| V128ExtMulLow8to16S
| V128ExtMulHigh8to16S
| V128ExtMulLow8to16U
| V128ExtMulHigh8to16U
| V128ExtMulLow16to32S
| V128ExtMulHigh16to32S
| V128ExtMulLow16to32U
| V128ExtMulHigh16to32U
| V128ExtMulLow32to64S
| V128ExtMulHigh32to64S
| V128ExtMulLow32to64U
| V128ExtMulHigh32to64U
| V128Dot16to32S
| V128Q15MulrSat16S
| V128AddF32
| V128AddF64
| V128SubF32
| V128SubF64
| V128MulF32
| V128MulF64
| V128DivF32
| V128DivF64
| V128MinF32
| V128MinF64
| V128MaxF32
| V128MaxF64
| V128PMinF32
| V128PMinF64
| V128PMaxF32
| V128PMaxF64
| V128EqF32
| V128EqF64
| V128NeF32
| V128NeF64
| V128LtF32
| V128LtF64
| V128GtF32
| V128GtF64
| V128LeF32
| V128LeF64
| V128GeF32
| V128GeF64
| V128RelaxedSwizzle
| V128RelaxedMinF32
| V128RelaxedMaxF32
| V128RelaxedMinF64
| V128RelaxedMaxF64
| V128RelaxedQ15MulrS
| V128RelaxedDot8to16S => validate_simd_binary_v128_to_v128(inst, result)
_ => ()
}
}
///|
fn validate_simd_operand_count(
inst : Inst,
result : ValidationResult,
expected_count : Int,
) -> Bool {
let actual_count = inst.operands.length()
if actual_count != expected_count {
result.add_error(
"\{inst.opcode} expects \{expected_count} operands, got \{actual_count}",
)
false
} else {
true
}
}
///|
fn validate_simd_result_type(
inst : Inst,
result : ValidationResult,
expected_type : Type,
) -> Unit {
match inst.first_result() {
Some(value) =>
if !types_match(value.ty, expected_type) {
result.add_error(
"\{inst.opcode} expects result type \{expected_type}, got \{value.ty}",
)
}
None =>
result.add_error(
"\{inst.opcode} expects result type \{expected_type}, but has no result",
)
}
}
///|
fn validate_simd_no_result(inst : Inst, result : ValidationResult) -> Unit {
if inst.first_result() is Some(value) {
result.add_error(
"\{inst.opcode} must not produce a result, got \{value.ty}",
)
}
}
///|
fn validate_simd_operand_type(
inst : Inst,
result : ValidationResult,
operand_index : Int,
expected_type : Type,
) -> Unit {
let operand = inst.operands[operand_index]
if !types_match(operand.ty, expected_type) {
result.add_error(
"\{inst.opcode} expects operand \{operand_index} type \{expected_type}, got \{operand.ty}",
)
}
}
///|
fn validate_simd_operand_is_integer(
inst : Inst,
result : ValidationResult,
operand_index : Int,
) -> Unit {
let operand = inst.operands[operand_index]
if !is_integer_type(operand.ty) {
result.add_error(
"\{inst.opcode} expects operand \{operand_index} to be integer address/index, got \{operand.ty}",
)
}
}
///|
fn validate_simd_unary_v128_to_v128(
inst : Inst,
result : ValidationResult,
) -> Unit {
if validate_simd_operand_count(inst, result, 1) {
validate_simd_operand_type(inst, result, 0, V128)
validate_simd_result_type(inst, result, V128)
}
}
///|
fn validate_simd_unary_v128_to_i32(
inst : Inst,
result : ValidationResult,
) -> Unit {
if validate_simd_operand_count(inst, result, 1) {
validate_simd_operand_type(inst, result, 0, V128)
validate_simd_result_type(inst, result, I32)
}
}
///|
fn validate_simd_binary_v128_to_v128(
inst : Inst,
result : ValidationResult,
) -> Unit {
if validate_simd_operand_count(inst, result, 2) {
validate_simd_operand_type(inst, result, 0, V128)
validate_simd_operand_type(inst, result, 1, V128)
validate_simd_result_type(inst, result, V128)
}
}
///|
fn validate_simd_ternary_v128_to_v128(
inst : Inst,
result : ValidationResult,
) -> Unit {
if validate_simd_operand_count(inst, result, 3) {
validate_simd_operand_type(inst, result, 0, V128)
validate_simd_operand_type(inst, result, 1, V128)
validate_simd_operand_type(inst, result, 2, V128)
validate_simd_result_type(inst, result, V128)
}
}
///|
fn validate_simd_shift(inst : Inst, result : ValidationResult) -> Unit {
if validate_simd_operand_count(inst, result, 2) {
validate_simd_operand_type(inst, result, 0, V128)
validate_simd_operand_type(inst, result, 1, I32)
validate_simd_result_type(inst, result, V128)
}
}
///|
fn validate_simd_splat(
inst : Inst,
result : ValidationResult,
scalar_type : Type,
) -> Unit {
if validate_simd_operand_count(inst, result, 1) {
validate_simd_operand_type(inst, result, 0, scalar_type)
validate_simd_result_type(inst, result, V128)
}
}
///|
fn validate_simd_extract_lane(
inst : Inst,
result : ValidationResult,
lane_type : Type,
) -> Unit {
if validate_simd_operand_count(inst, result, 1) {
validate_simd_operand_type(inst, result, 0, V128)
validate_simd_result_type(inst, result, lane_type)
}
}
///|
fn validate_simd_replace_lane(
inst : Inst,
result : ValidationResult,
lane_type : Type,
) -> Unit {
if validate_simd_operand_count(inst, result, 2) {
validate_simd_operand_type(inst, result, 0, V128)
validate_simd_operand_type(inst, result, 1, lane_type)
validate_simd_result_type(inst, result, V128)
}
}
///|
fn validate_simd_load(inst : Inst, result : ValidationResult) -> Unit {
if validate_simd_operand_count(inst, result, 1) {
validate_simd_operand_is_integer(inst, result, 0)
validate_simd_result_type(inst, result, V128)
}
}
///|
fn validate_simd_load_lane(inst : Inst, result : ValidationResult) -> Unit {
if validate_simd_operand_count(inst, result, 2) {
validate_simd_operand_is_integer(inst, result, 0)
validate_simd_operand_type(inst, result, 1, V128)
validate_simd_result_type(inst, result, V128)
}
}
///|
fn validate_simd_store_lane(inst : Inst, result : ValidationResult) -> Unit {
if validate_simd_operand_count(inst, result, 2) {
validate_simd_operand_is_integer(inst, result, 0)
validate_simd_operand_type(inst, result, 1, V128)
validate_simd_no_result(inst, result)
}
}
///|
/// Validate a terminator
fn validate_terminator(
term : Terminator,
defined : @hashmap.HashMap[Int, Type],
func : Function,
result : ValidationResult,
) -> Unit {
match term {
Jump(target, args) => {
// Check target block exists
if target >= func.blocks.length() {
result.add_error("Jump target block\{target} does not exist")
} else {
let target_block = func.blocks[target]
// Check argument count matches
if args.length() != target_block.params.length() {
result.add_error(
"Jump to block\{target} expects \{target_block.params.length()} args, got \{args.length()}",
)
}
}
// Check all args are defined
for arg in args {
if !defined.contains(arg.id) {
result.add_error("Jump argument v\{arg.id} is not defined")
}
}
}
Brz(cond, then_target, else_target)
| Brnz(cond, then_target, else_target) => {
// Check condition is defined
if !defined.contains(cond.id) {
result.add_error("Branch condition v\{cond.id} is not defined")
}
// Check targets exist
if then_target >= func.blocks.length() {
result.add_error(
"Branch then target block\{then_target} does not exist",
)
}
if else_target >= func.blocks.length() {
result.add_error(
"Branch else target block\{else_target} does not exist",
)
}
}
Branch(cond, true_target, true_args, false_target, false_args) => {
if !defined.contains(cond.id) {
result.add_error("Branch condition v\{cond.id} is not defined")
}
if true_target >= func.blocks.length() {
result.add_error(
"Branch true target block\{true_target} does not exist",
)
} else {
let target_block = func.blocks[true_target]
if true_args.length() != target_block.params.length() {
result.add_error(
"Branch true target block\{true_target} expects \{target_block.params.length()} args, got \{true_args.length()}",
)
}
}
if false_target >= func.blocks.length() {
result.add_error(
"Branch false target block\{false_target} does not exist",
)
} else {
let target_block = func.blocks[false_target]
if false_args.length() != target_block.params.length() {
result.add_error(
"Branch false target block\{false_target} expects \{target_block.params.length()} args, got \{false_args.length()}",
)
}
}
for arg in true_args {
if !defined.contains(arg.id) {
result.add_error("Branch true argument v\{arg.id} is not defined")
}
}
for arg in false_args {
if !defined.contains(arg.id) {
result.add_error("Branch false argument v\{arg.id} is not defined")
}
}
}
BrTable(index, targets, default_target) => {
// Check index is defined
if !defined.contains(index.id) {
result.add_error("BrTable index v\{index.id} is not defined")
}
// Check all targets exist
for t in targets {
if t >= func.blocks.length() {
result.add_error("BrTable target block\{t} does not exist")
}
}
if default_target >= func.blocks.length() {
result.add_error(
"BrTable default target block\{default_target} does not exist",
)
}
}
Return(values) => {
// Check return value count matches function signature
if values.length() != func.results.length() {
result.add_error(
"Return expects \{func.results.length()} values, got \{values.length()}",
)
}
// Check all values are defined
for v in values {
if !defined.contains(v.id) {
result.add_error("Return value v\{v.id} is not defined")
}
}
}
Trap(_) | TrapExit(_) => () // Always valid
}
}
///|
/// Check if a type is an integer type
fn is_integer_type(ty : Type) -> Bool {
match ty {
I32 | I64 => true
_ => false
}
}
///|
/// Check if a type is a float type
fn is_float_type(ty : Type) -> Bool {
match ty {
F32 | F64 => true
_ => false
}
}
///|
/// Check if two types match
fn types_match(a : Type, b : Type) -> Bool {
match (a, b) {
(I32, I32)
| (I64, I64)
| (F32, F32)
| (F64, F64)
| (V128, V128)
| (Ptr, Ptr)
| (Ref, Ref)
| (CallableRef, CallableRef)
| (OpaqueRef, OpaqueRef) => true
_ => false
}
}