// verify_ir.cm — validates mcode IR structure after optimizer passes // Used to catch structural bugs introduced by optimization. // Operand positions that are slots for each opcode. // Positions are 0-indexed from the first operand (after the opcode), // excluding the trailing line/col pair. var slot_positions = { // Constant loaders — only dest access: [0], int: [0], true: [0], false: [0], null: [0], function: [0], array: [0], record: [0], // Unary — dest, src move: [0, 1], not: [0, 1], negate: [0, 1], bitnot: [0, 1], length: [0, 1], typeof: [0, 1], is_int: [0, 1], is_text: [0, 1], is_num: [0, 1], is_bool: [0, 1], is_null: [0, 1], is_array: [0, 1], is_func: [0, 1], is_record: [0, 1], is_stone: [0, 1], is_identical: [0, 1, 2], // Binary arithmetic/comparison — dest, src1, src2 add: [0, 1, 2], subtract: [0, 1, 2], multiply: [0, 1, 2], divide: [0, 1, 2], modulo: [0, 1, 2], pow: [0, 1, 2], eq: [0, 1, 2], ne: [0, 1, 2], lt: [0, 1, 2], le: [0, 1, 2], gt: [0, 1, 2], ge: [0, 1, 2], eq_int: [0, 1, 2], ne_int: [0, 1, 2], lt_int: [0, 1, 2], gt_int: [0, 1, 2], le_int: [0, 1, 2], ge_int: [0, 1, 2], eq_float: [0, 1, 2], ne_float: [0, 1, 2], lt_float: [0, 1, 2], gt_float: [0, 1, 2], le_float: [0, 1, 2], ge_float: [0, 1, 2], eq_text: [0, 1, 2], ne_text: [0, 1, 2], lt_text: [0, 1, 2], gt_text: [0, 1, 2], le_text: [0, 1, 2], ge_text: [0, 1, 2], eq_bool: [0, 1, 2], ne_bool: [0, 1, 2], eq_tol: [0, 1, 2], ne_tol: [0, 1, 2], concat: [0, 1, 2], and: [0, 1, 2], or: [0, 1, 2], bitand: [0, 1, 2], bitor: [0, 1, 2], bitxor: [0, 1, 2], shl: [0, 1, 2], shr: [0, 1, 2], ushr: [0, 1, 2], in: [0, 1, 2], // Element access — all operands are slots load_index: [0, 1, 2], load_dynamic: [0, 1, 2], load_field: [0, 1], store_index: [0, 1, 2], store_dynamic: [0, 1, 2], store_field: [0, 1], // Push/pop push: [0, 1], pop: [0, 1], get: [0, 1], // Control flow — slot positions only return: [0], jump: [], jump_true: [0], jump_false: [0], jump_not_null: [0], disrupt: [], // Invoke invoke: [0, 1], tail_invoke: [0, 1], goinvoke: [0], frame: [0, 1], setarg: [0, 2] } // Opcodes that write to their first operand (position 0) var writes_dest = { access: true, int: true, true: true, false: true, null: true, function: true, array: true, record: true, move: true, not: true, negate: true, bitnot: true, length: true, typeof: true, is_int: true, is_text: true, is_num: true, is_bool: true, is_null: true, is_array: true, is_func: true, is_record: true, is_stone: true, is_identical: true, add: true, subtract: true, multiply: true, divide: true, modulo: true, pow: true, eq: true, ne: true, lt: true, le: true, gt: true, ge: true, eq_int: true, ne_int: true, lt_int: true, gt_int: true, le_int: true, ge_int: true, eq_float: true, ne_float: true, lt_float: true, gt_float: true, le_float: true, ge_float: true, eq_text: true, ne_text: true, lt_text: true, gt_text: true, le_text: true, ge_text: true, eq_bool: true, ne_bool: true, eq_tol: true, ne_tol: true, concat: true, and: true, or: true, bitand: true, bitor: true, bitxor: true, shl: true, shr: true, ushr: true, in: true, load_index: true, load_dynamic: true, load_field: true, pop: true, get: true, invoke: true, tail_invoke: true } // Opcodes where invoke writes to position 1 (result slot), not position 0 var invoke_result_pos = 1 // Jump opcodes and the position of their label operand (0-indexed from first operand) var jump_label_pos = { jump: 0, jump_true: 1, jump_false: 1, jump_not_null: 1 } // --- Check: slot_bounds --- // Verifies every slot operand is in 0..nr_slots-1. var check_slot_bounds = function(func) { var instructions = func.instructions var nr_slots = func.nr_slots var errors = [] var i = 0 var instr = null var op = null var positions = null var j = 0 var pos = null var val = null if (instructions == null) return errors while (i < length(instructions)) { instr = instructions[i] if (is_array(instr)) { op = instr[0] positions = slot_positions[op] if (positions != null) { j = 0 while (j < length(positions)) { pos = positions[j] + 1 if (pos < length(instr) - 2) { val = instr[pos] if (is_number(val) && (val < 0 || val >= nr_slots)) { push(errors, `slot_bounds: instr ${text(i)} op=${op} slot[${text(positions[j])}]=${text(val)} out of range 0..${text(nr_slots - 1)}`) } } j = j + 1 } } } i = i + 1 } return errors } // --- Check: jump_targets --- // Verifies every jump target label exists in the instruction stream. var check_jump_targets = function(func) { var instructions = func.instructions var errors = [] var labels = {} var i = 0 var instr = null var op = null var label_pos = null var target = null if (instructions == null) return errors // Collect all labels (non-nop strings) while (i < length(instructions)) { instr = instructions[i] if (is_text(instr) && !starts_with(instr, "_nop_")) { labels[instr] = true } i = i + 1 } // Check jump targets i = 0 while (i < length(instructions)) { instr = instructions[i] if (is_array(instr)) { op = instr[0] label_pos = jump_label_pos[op] if (label_pos != null) { target = instr[label_pos + 1] if (is_text(target) && labels[target] != true) { push(errors, `jump_targets: instr ${text(i)} op=${op} target label "${target}" not found`) } } } i = i + 1 } return errors } // --- Check: type_consistency --- // Verifies typed operators receive compatible known types. var check_type_consistency = function(func) { var instructions = func.instructions var errors = [] var slot_types = {} var i = 0 var instr = null var op = null var s2 = null var s3 = null var t2 = null var t3 = null if (instructions == null) return errors // Type constants var T_INT = "int" var T_FLOAT = "float" var T_TEXT = "text" var T_BOOL = "bool" var int_ops = { eq_int: true, ne_int: true, lt_int: true, gt_int: true, le_int: true, ge_int: true } var float_ops = { eq_float: true, ne_float: true, lt_float: true, gt_float: true, le_float: true, ge_float: true } var text_ops = { eq_text: true, ne_text: true, lt_text: true, gt_text: true, le_text: true, ge_text: true, concat: true } var bool_ops = { eq_bool: true, ne_bool: true, not: true, and: true, or: true } while (i < length(instructions)) { instr = instructions[i] // Reset type info at labels (basic block boundaries) if (is_text(instr) && !starts_with(instr, "_nop_")) { slot_types = {} i = i + 1 continue } if (!is_array(instr)) { i = i + 1 continue } op = instr[0] // Track known types from constant-producing ops if (op == "int") { slot_types[text(instr[1])] = T_INT } else if (op == "access") { if (is_number(instr[2])) { if (is_integer(instr[2])) { slot_types[text(instr[1])] = T_INT } else { slot_types[text(instr[1])] = T_FLOAT } } else if (is_text(instr[2])) { slot_types[text(instr[1])] = T_TEXT } } else if (op == "true" || op == "false") { slot_types[text(instr[1])] = T_BOOL } // Check typed binary ops if (int_ops[op] == true && length(instr) >= 5) { s2 = text(instr[2]) t2 = slot_types[s2] if (t2 != null && t2 != T_INT && t2 != "unknown") { push(errors, `type_consistency: instr ${text(i)} op=${op} src1 slot ${s2} has type ${t2}, expected int`) } if (length(instr) >= 6) { s3 = text(instr[3]) t3 = slot_types[s3] if (t3 != null && t3 != T_INT && t3 != "unknown") { push(errors, `type_consistency: instr ${text(i)} op=${op} src2 slot ${s3} has type ${t3}, expected int`) } } } else if (float_ops[op] == true && length(instr) >= 5) { s2 = text(instr[2]) t2 = slot_types[s2] if (t2 != null && t2 != T_FLOAT && t2 != "unknown") { push(errors, `type_consistency: instr ${text(i)} op=${op} src1 slot ${s2} has type ${t2}, expected float`) } if (length(instr) >= 6) { s3 = text(instr[3]) t3 = slot_types[s3] if (t3 != null && t3 != T_FLOAT && t3 != "unknown") { push(errors, `type_consistency: instr ${text(i)} op=${op} src2 slot ${s3} has type ${t3}, expected float`) } } } else if (text_ops[op] == true && length(instr) >= 5) { s2 = text(instr[2]) t2 = slot_types[s2] if (t2 != null && t2 != T_TEXT && t2 != "unknown") { push(errors, `type_consistency: instr ${text(i)} op=${op} src1 slot ${s2} has type ${t2}, expected text`) } if (length(instr) >= 6) { s3 = text(instr[3]) t3 = slot_types[s3] if (t3 != null && t3 != T_TEXT && t3 != "unknown") { push(errors, `type_consistency: instr ${text(i)} op=${op} src2 slot ${s3} has type ${t3}, expected text`) } } } else if (bool_ops[op] == true && length(instr) >= 5) { s2 = text(instr[2]) t2 = slot_types[s2] if (t2 != null && t2 != T_BOOL && t2 != "unknown") { push(errors, `type_consistency: instr ${text(i)} op=${op} src1 slot ${s2} has type ${t2}, expected bool`) } } // Clear type info for dest-producing ops if (writes_dest[op] == true) { slot_types[text(instr[1])] = null // Restore type for known-result ops if (op == "int" || (op == "access" && is_number(instr[2]))) { // already set above } } if (op == "invoke") { slot_types[text(instr[2])] = null } i = i + 1 } return errors } // --- Check: nop_consistency --- // Verifies nop markers are not referenced by jumps. var check_nop_consistency = function(func) { var instructions = func.instructions var errors = [] var nops = {} var i = 0 var instr = null var op = null var label_pos = null var target = null if (instructions == null) return errors // Collect all nop markers while (i < length(instructions)) { instr = instructions[i] if (is_text(instr) && starts_with(instr, "_nop_")) { nops[instr] = true } i = i + 1 } // Check that no jump targets a nop i = 0 while (i < length(instructions)) { instr = instructions[i] if (is_array(instr)) { op = instr[0] label_pos = jump_label_pos[op] if (label_pos != null) { target = instr[label_pos + 1] if (is_text(target) && nops[target] == true) { push(errors, `nop_consistency: instr ${text(i)} op=${op} jumps to nop marker "${target}"`) } } } i = i + 1 } return errors } // --- verify_all --- // Runs all checks on a function. Returns array of error strings (empty = pass). var verify_all = function(func, pass_name) { var all_errors = [] var check_errors = null var i = 0 var prefix = pass_name != null ? pass_name + ": " : "" var fn_name = func.name != null ? func.name : "" check_errors = check_slot_bounds(func) i = 0 while (i < length(check_errors)) { push(all_errors, `${prefix}${fn_name}: ${check_errors[i]}`) i = i + 1 } check_errors = check_jump_targets(func) i = 0 while (i < length(check_errors)) { push(all_errors, `${prefix}${fn_name}: ${check_errors[i]}`) i = i + 1 } check_errors = check_type_consistency(func) i = 0 while (i < length(check_errors)) { push(all_errors, `${prefix}${fn_name}: ${check_errors[i]}`) i = i + 1 } check_errors = check_nop_consistency(func) i = 0 while (i < length(check_errors)) { push(all_errors, `${prefix}${fn_name}: ${check_errors[i]}`) i = i + 1 } return all_errors } return { verify_all: verify_all, check_slot_bounds: check_slot_bounds, check_jump_targets: check_jump_targets, check_type_consistency: check_type_consistency, check_nop_consistency: check_nop_consistency }