3301 lines
96 KiB
Plaintext
3301 lines
96 KiB
Plaintext
// streamline.cm — mcode IR optimizer
|
|
// Composed of independent passes, each a separate function.
|
|
// Optional `log` parameter enables structured observability.
|
|
|
|
var streamline = function(ir, log) {
|
|
// IR verification support — verifier module passed via ir._verify_mod
|
|
// (streamline's use() is use_basic from bootstrap, which can't load source)
|
|
var verify_fn = null
|
|
var verifier = null
|
|
if (ir._verify && ir._verify_mod) {
|
|
verifier = ir._verify_mod
|
|
verify_fn = function(func, pass_name) {
|
|
var errs = verifier.verify_all(func, pass_name)
|
|
var i = 0
|
|
while (i < length(errs)) {
|
|
log.error(`[verify_ir] ${errs[i]}`)
|
|
i = i + 1
|
|
}
|
|
if (length(errs) > 0) {
|
|
log.error(`[verify_ir] ${text(length(errs))} errors after ${pass_name}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Type constants
|
|
var T_UNKNOWN = "unknown"
|
|
var T_INT = "int"
|
|
var T_FLOAT = "float"
|
|
var T_NUM = "num"
|
|
var T_TEXT = "text"
|
|
var T_BOOL = "bool"
|
|
var T_NULL = "null"
|
|
var T_ARRAY = "array"
|
|
var T_RECORD = "record"
|
|
var T_FUNCTION = "function"
|
|
var T_BLOB = "blob"
|
|
|
|
var numeric_ops = {
|
|
add: true, subtract: true, multiply: true,
|
|
divide: true, modulo: true, remainder: true,
|
|
max: true, min: true, pow: true
|
|
}
|
|
var bool_result_ops = {
|
|
eq: true, ne: true, lt: true, gt: true, le: true, ge: true,
|
|
eq_tol: true, ne_tol: true, in: true,
|
|
not: true, and: true, or: true,
|
|
is_int: true, is_text: true, is_num: true,
|
|
is_bool: true, is_null: true, is_identical: true,
|
|
is_array: true, is_func: true, is_record: true, is_stone: true,
|
|
is_blob: true, is_data: true,
|
|
is_true: true, is_false: true, is_fit: true,
|
|
is_char: true, is_digit: true, is_letter: true,
|
|
is_lower: true, is_upper: true, is_ws: true, is_actor: true
|
|
}
|
|
var type_check_map = {
|
|
is_int: T_INT, is_text: T_TEXT, is_num: T_NUM,
|
|
is_bool: T_BOOL, is_null: T_NULL,
|
|
is_array: T_ARRAY, is_func: T_FUNCTION,
|
|
is_record: T_RECORD, is_blob: T_BLOB
|
|
}
|
|
|
|
// simplify_algebra dispatch tables
|
|
var self_true_ops = {
|
|
eq: true, is_identical: true, le: true, ge: true
|
|
}
|
|
var self_false_ops = {
|
|
ne: true, lt: true, gt: true
|
|
}
|
|
var no_clear_ops = {
|
|
int: true, access: true, true: true, false: true, move: true, null: true,
|
|
jump: true, jump_true: true, jump_false: true, jump_not_null: true,
|
|
wary_true: true, wary_false: true, jump_null: true, jump_empty: true,
|
|
return: true, disrupt: true,
|
|
store_field: true, store_index: true, store_dynamic: true,
|
|
push: true, setarg: true, invoke: true, tail_invoke: true,
|
|
stone_text: true
|
|
}
|
|
|
|
var is_cond_jump = function(op) {
|
|
return op == "jump_true" || op == "jump_false" || op == "jump_not_null"
|
|
|| op == "wary_true" || op == "wary_false"
|
|
|| op == "jump_null" || op == "jump_empty"
|
|
}
|
|
|
|
// --- Logging support ---
|
|
|
|
var ir_stats = null
|
|
var time_mod = null
|
|
if (log != null) {
|
|
ir_stats = use("ir_stats")
|
|
time_mod = use("time")
|
|
}
|
|
|
|
var run_pass = function(func, pass_name, pass_fn) {
|
|
var before = null
|
|
var after = null
|
|
var t0 = null
|
|
var t1 = null
|
|
var ms = null
|
|
var changed = false
|
|
var result = null
|
|
if (log == null) {
|
|
return pass_fn()
|
|
}
|
|
before = ir_stats.detailed_stats(func)
|
|
t0 = time_mod.number()
|
|
result = pass_fn()
|
|
t1 = time_mod.number()
|
|
after = ir_stats.detailed_stats(func)
|
|
ms = (t1 - t0) * 1000
|
|
changed = before.instr != after.instr ||
|
|
before.nop != after.nop ||
|
|
before.guard != after.guard
|
|
log.passes[] = {
|
|
pass: pass_name,
|
|
fn: func.name,
|
|
ms: ms,
|
|
before: before,
|
|
after: after,
|
|
changed: changed,
|
|
changes: {
|
|
nops_added: after.nop - before.nop,
|
|
guards_removed: before.guard - after.guard
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// --- Shared helpers ---
|
|
|
|
var access_value_type = function(val) {
|
|
if (is_number(val)) {
|
|
if (is_integer(val)) {
|
|
return T_INT
|
|
}
|
|
return T_FLOAT
|
|
}
|
|
if (is_text(val)) {
|
|
return T_TEXT
|
|
}
|
|
return T_UNKNOWN
|
|
}
|
|
|
|
var slot_is = null
|
|
var write_rules = null
|
|
|
|
// track_types reuses write_rules table; move handled specially
|
|
// Ops safe to narrow from T_NUM to T_INT when both operands are T_INT.
|
|
// Excludes divide (int/int can produce float) and pow (int**neg produces float).
|
|
var int_narrowable_ops = {
|
|
add: true, subtract: true, multiply: true,
|
|
remainder: true, modulo: true, max: true, min: true
|
|
}
|
|
|
|
var track_types = function(slot_types, instr) {
|
|
var op = instr[0]
|
|
var rule = null
|
|
var src_type = null
|
|
var typ = null
|
|
if (op == "move") {
|
|
src_type = slot_types[instr[2]]
|
|
slot_types[instr[1]] = src_type != null ? src_type : T_UNKNOWN
|
|
return null
|
|
}
|
|
if (op == "load_index") {
|
|
slot_types[instr[2]] = T_ARRAY
|
|
slot_types[instr[3]] = T_INT
|
|
} else if (op == "store_index") {
|
|
slot_types[instr[1]] = T_ARRAY
|
|
slot_types[instr[3]] = T_INT
|
|
} else if (op == "load_field") {
|
|
slot_types[instr[2]] = T_RECORD
|
|
} else if (op == "store_field") {
|
|
slot_types[instr[1]] = T_RECORD
|
|
} else if (op == "push") {
|
|
slot_types[instr[1]] = T_ARRAY
|
|
} else if (op == "pop") {
|
|
slot_types[instr[2]] = T_ARRAY
|
|
}
|
|
rule = write_rules[op]
|
|
if (rule != null) {
|
|
typ = rule[1]
|
|
if (typ == null) {
|
|
typ = access_value_type(instr[2])
|
|
}
|
|
// Narrow T_NUM to T_INT when both operands are T_INT
|
|
if (typ == T_NUM && instr[3] != null && int_narrowable_ops[op] == true) {
|
|
if (slot_is(slot_types, instr[2], T_INT)
|
|
&& slot_is(slot_types, instr[3], T_INT)) {
|
|
typ = T_INT
|
|
}
|
|
}
|
|
slot_types[instr[rule[0]]] = typ
|
|
}
|
|
return null
|
|
}
|
|
|
|
slot_is = function(slot_types, slot, typ) {
|
|
var known = slot_types[slot]
|
|
if (known == null) {
|
|
return false
|
|
}
|
|
if (known == typ) {
|
|
return true
|
|
}
|
|
if (typ == T_NUM && (known == T_INT || known == T_FLOAT)) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var merge_backward = function(backward_types, slot, typ) {
|
|
var existing = null
|
|
if (!is_number(slot)) {
|
|
return null
|
|
}
|
|
existing = backward_types[slot]
|
|
if (existing == null) {
|
|
backward_types[slot] = typ
|
|
} else if (existing != typ && existing != T_UNKNOWN) {
|
|
if ((existing == T_INT || existing == T_FLOAT) && typ == T_NUM) {
|
|
backward_types[slot] = T_NUM
|
|
} else if (existing == T_NUM && (typ == T_INT || typ == T_FLOAT)) {
|
|
// Keep wider T_NUM
|
|
} else if ((existing == T_INT && typ == T_FLOAT) || (existing == T_FLOAT && typ == T_INT)) {
|
|
backward_types[slot] = T_NUM
|
|
} else {
|
|
backward_types[slot] = T_UNKNOWN
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
var seed_params = function(slot_types, param_types, nr_args) {
|
|
var j = 1
|
|
while (j <= nr_args) {
|
|
if (param_types[j] != null) {
|
|
slot_types[j] = param_types[j]
|
|
}
|
|
j = j + 1
|
|
}
|
|
return null
|
|
}
|
|
|
|
var seed_writes = function(slot_types, write_types) {
|
|
var k = 0
|
|
while (k < length(write_types)) {
|
|
if (write_types[k] != null) {
|
|
slot_types[k] = write_types[k]
|
|
}
|
|
k = k + 1
|
|
}
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: infer_param_types — backward type inference
|
|
// Scans typed operators to infer immutable parameter types.
|
|
// Uses data-driven dispatch: each rule is [pos1, type1] or
|
|
// [pos1, type1, pos2, type2] for operand positions to merge.
|
|
// =========================================================
|
|
var param_rules = {
|
|
add: [2, T_NUM, 3, T_NUM],
|
|
subtract: [2, T_NUM, 3, T_NUM], multiply: [2, T_NUM, 3, T_NUM],
|
|
divide: [2, T_NUM, 3, T_NUM], modulo: [2, T_NUM, 3, T_NUM],
|
|
remainder: [2, T_NUM, 3, T_NUM], max: [2, T_NUM, 3, T_NUM],
|
|
min: [2, T_NUM, 3, T_NUM], pow: [2, T_NUM, 3, T_NUM],
|
|
negate: [2, T_NUM], abs: [2, T_NUM], sign: [2, T_NUM],
|
|
fraction: [2, T_NUM], integer: [2, T_NUM],
|
|
floor: [2, T_NUM], ceiling: [2, T_NUM],
|
|
round: [2, T_NUM], trunc: [2, T_NUM],
|
|
bitand: [2, T_INT, 3, T_INT], bitor: [2, T_INT, 3, T_INT],
|
|
bitxor: [2, T_INT, 3, T_INT], shl: [2, T_INT, 3, T_INT],
|
|
shr: [2, T_INT, 3, T_INT], ushr: [2, T_INT, 3, T_INT],
|
|
bitnot: [2, T_INT],
|
|
concat: [2, T_TEXT, 3, T_TEXT],
|
|
and: [2, T_BOOL, 3, T_BOOL], or: [2, T_BOOL, 3, T_BOOL],
|
|
store_index: [1, T_ARRAY, 2, T_INT], store_field: [1, T_RECORD],
|
|
push: [1, T_ARRAY],
|
|
load_index: [2, T_ARRAY, 3, T_INT], load_field: [2, T_RECORD],
|
|
pop: [2, T_ARRAY],
|
|
is_text: [2, T_UNKNOWN], is_int: [2, T_UNKNOWN], is_num: [2, T_UNKNOWN],
|
|
is_bool: [2, T_UNKNOWN], is_null: [2, T_UNKNOWN],
|
|
is_array: [2, T_UNKNOWN], is_func: [2, T_UNKNOWN],
|
|
is_record: [2, T_UNKNOWN], is_blob: [2, T_UNKNOWN]
|
|
}
|
|
|
|
var infer_param_types = function(func) {
|
|
var instructions = func.instructions
|
|
var nr_args = func.nr_args != null ? func.nr_args : 0
|
|
var num_instr = 0
|
|
var backward_types = null
|
|
var param_types = null
|
|
var i = 0
|
|
var j = 0
|
|
var iter = 0
|
|
var instr = null
|
|
var bt = null
|
|
var src = 0
|
|
var dst = 0
|
|
var old_bt = null
|
|
var changed = false
|
|
var rule = null
|
|
|
|
if (instructions == null || nr_args == 0) {
|
|
return array(func.nr_slots)
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
backward_types = array(func.nr_slots)
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
if (is_array(instr)) {
|
|
rule = param_rules[instr[0]]
|
|
if (rule != null) {
|
|
merge_backward(backward_types, instr[rule[0]], rule[1])
|
|
if (length(rule) > 2) {
|
|
merge_backward(backward_types, instr[rule[2]], rule[3])
|
|
}
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
// Propagate typed constraints backward through move chains.
|
|
changed = true
|
|
iter = 0
|
|
while (changed && iter < num_instr + 4) {
|
|
changed = false
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
if (is_array(instr) && instr[0] == "move") {
|
|
dst = instr[1]
|
|
src = instr[2]
|
|
bt = backward_types[dst]
|
|
if (bt != null && bt != T_UNKNOWN) {
|
|
old_bt = backward_types[src]
|
|
merge_backward(backward_types, src, bt)
|
|
if (backward_types[src] != old_bt) {
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
iter = iter + 1
|
|
}
|
|
|
|
param_types = array(func.nr_slots)
|
|
j = 1
|
|
while (j <= nr_args) {
|
|
bt = backward_types[j]
|
|
if (bt != null && bt != T_UNKNOWN) {
|
|
param_types[j] = bt
|
|
}
|
|
j = j + 1
|
|
}
|
|
return param_types
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: infer_slot_write_types — slot write-type invariance
|
|
// Scans all instructions to find non-parameter slots where
|
|
// every write produces the same type. These types persist
|
|
// across label join points.
|
|
// Uses data-driven dispatch: each rule is [dest_pos, type].
|
|
// =========================================================
|
|
write_rules = {
|
|
int: [1, T_INT], true: [1, T_BOOL], false: [1, T_BOOL],
|
|
null: [1, T_NULL], access: [1, null],
|
|
array: [1, T_ARRAY], record: [1, T_RECORD],
|
|
function: [1, T_FUNCTION], length: [1, T_INT],
|
|
bitnot: [1, T_INT], bitand: [1, T_INT], bitor: [1, T_INT],
|
|
bitxor: [1, T_INT], shl: [1, T_INT], shr: [1, T_INT], ushr: [1, T_INT],
|
|
negate: [1, T_NUM], concat: [1, T_TEXT],
|
|
abs: [1, T_NUM], sign: [1, T_INT], fraction: [1, T_NUM],
|
|
integer: [1, T_NUM], floor: [1, T_NUM], ceiling: [1, T_NUM],
|
|
round: [1, T_NUM], trunc: [1, T_NUM],
|
|
eq: [1, T_BOOL], ne: [1, T_BOOL], lt: [1, T_BOOL],
|
|
le: [1, T_BOOL], gt: [1, T_BOOL], ge: [1, T_BOOL], in: [1, T_BOOL],
|
|
add: [1, T_NUM], subtract: [1, T_NUM], multiply: [1, T_NUM],
|
|
divide: [1, T_NUM], modulo: [1, T_NUM], remainder: [1, T_NUM],
|
|
max: [1, T_NUM], min: [1, T_NUM], pow: [1, T_NUM],
|
|
move: [1, T_UNKNOWN], load_field: [1, T_UNKNOWN],
|
|
load_index: [1, T_UNKNOWN], load_dynamic: [1, T_UNKNOWN],
|
|
pop: [1, T_UNKNOWN], get: [1, T_UNKNOWN],
|
|
invoke: [2, T_UNKNOWN], tail_invoke: [2, T_UNKNOWN],
|
|
eq_tol: [1, T_BOOL], ne_tol: [1, T_BOOL],
|
|
not: [1, T_BOOL], and: [1, T_BOOL], or: [1, T_BOOL],
|
|
is_int: [1, T_BOOL], is_text: [1, T_BOOL], is_num: [1, T_BOOL],
|
|
is_bool: [1, T_BOOL], is_null: [1, T_BOOL], is_identical: [1, T_BOOL],
|
|
is_array: [1, T_BOOL], is_func: [1, T_BOOL],
|
|
is_record: [1, T_BOOL], is_stone: [1, T_BOOL],
|
|
is_blob: [1, T_BOOL], is_data: [1, T_BOOL],
|
|
is_true: [1, T_BOOL], is_false: [1, T_BOOL], is_fit: [1, T_BOOL],
|
|
is_char: [1, T_BOOL], is_digit: [1, T_BOOL], is_letter: [1, T_BOOL],
|
|
is_lower: [1, T_BOOL], is_upper: [1, T_BOOL], is_ws: [1, T_BOOL],
|
|
is_actor: [1, T_BOOL]
|
|
}
|
|
|
|
// Known intrinsic return types for invoke result inference.
|
|
var intrinsic_return_types = {
|
|
abs: T_NUM, floor: T_NUM, ceiling: T_NUM,
|
|
round: T_NUM, trunc: T_NUM, fraction: T_NUM,
|
|
integer: T_NUM, whole: T_NUM, sign: T_NUM,
|
|
max: T_NUM, min: T_NUM, remainder: T_NUM, modulo: T_NUM,
|
|
is_integer: T_BOOL, is_text: T_BOOL, is_number: T_BOOL,
|
|
is_null: T_BOOL, is_array: T_BOOL, is_function: T_BOOL,
|
|
is_object: T_BOOL, is_logical: T_BOOL, is_stone: T_BOOL,
|
|
is_blob: T_BOOL, starts_with: T_BOOL, ends_with: T_BOOL,
|
|
some: T_BOOL, every: T_BOOL
|
|
}
|
|
|
|
var narrow_arith_type = function(write_types, param_types, instr, typ) {
|
|
var s2 = null
|
|
var s3 = null
|
|
var t2 = null
|
|
var t3 = null
|
|
if (typ != T_NUM || instr[3] == null || int_narrowable_ops[instr[0]] != true) {
|
|
return typ
|
|
}
|
|
s2 = instr[2]
|
|
s3 = instr[3]
|
|
if (is_number(s2)) {
|
|
t2 = write_types[s2]
|
|
if (t2 == null && param_types != null && s2 < length(param_types)) {
|
|
t2 = param_types[s2]
|
|
}
|
|
}
|
|
if (is_number(s3)) {
|
|
t3 = write_types[s3]
|
|
if (t3 == null && param_types != null && s3 < length(param_types)) {
|
|
t3 = param_types[s3]
|
|
}
|
|
}
|
|
if (t2 == T_INT && t3 == T_INT) {
|
|
return T_INT
|
|
}
|
|
return typ
|
|
}
|
|
|
|
var infer_slot_write_types = function(func, param_types) {
|
|
var instructions = func.instructions
|
|
var nr_args = func.nr_args != null ? func.nr_args : 0
|
|
var num_instr = 0
|
|
var write_types = null
|
|
var frame_callee = null
|
|
var intrinsic_slots = null
|
|
var move_dests = null
|
|
var move_srcs = null
|
|
var i = 0
|
|
var k = 0
|
|
var iter = 0
|
|
var instr = null
|
|
var op = null
|
|
var src = 0
|
|
var slot = 0
|
|
var old_typ = null
|
|
var src_typ = null
|
|
var typ = null
|
|
var callee_slot = null
|
|
var changed = false
|
|
var rule = null
|
|
var cw_keys = null
|
|
|
|
if (instructions == null) {
|
|
return array(func.nr_slots)
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
write_types = array(func.nr_slots)
|
|
frame_callee = array(func.nr_slots)
|
|
intrinsic_slots = array(func.nr_slots)
|
|
move_dests = []
|
|
move_srcs = []
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
if (is_array(instr)) {
|
|
op = instr[0]
|
|
if (op == "access") {
|
|
slot = instr[1]
|
|
if (slot > 0 && slot > nr_args) {
|
|
merge_backward(write_types, slot, access_value_type(instr[2]))
|
|
}
|
|
if (is_object(instr[2]) && instr[2].make == "intrinsic") {
|
|
typ = intrinsic_return_types[instr[2].name]
|
|
if (typ != null && slot >= 0 && slot < length(intrinsic_slots)) {
|
|
intrinsic_slots[slot] = typ
|
|
}
|
|
}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (op == "move") {
|
|
slot = instr[1]
|
|
if (slot > 0 && slot > nr_args) {
|
|
move_dests[] = slot
|
|
move_srcs[] = instr[2]
|
|
}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (op == "frame" || op == "goframe") {
|
|
if (is_number(instr[1]) && instr[1] >= 0 && instr[1] < length(frame_callee)) {
|
|
frame_callee[instr[1]] = instr[2]
|
|
}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (op == "invoke" || op == "tail_invoke") {
|
|
slot = instr[2]
|
|
typ = T_UNKNOWN
|
|
callee_slot = frame_callee[instr[1]]
|
|
if (is_number(callee_slot) && callee_slot >= 0 && callee_slot < length(intrinsic_slots)) {
|
|
if (intrinsic_slots[callee_slot] != null) {
|
|
typ = intrinsic_slots[callee_slot]
|
|
}
|
|
}
|
|
if (slot > 0 && slot > nr_args) {
|
|
merge_backward(write_types, slot, typ)
|
|
}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (op == "get" && func._closure_slot_types != null) {
|
|
slot = instr[1]
|
|
typ = T_UNKNOWN
|
|
src_typ = func._closure_slot_types[text(instr[2]) + "_" + text(instr[3])]
|
|
if (src_typ != null) {
|
|
typ = src_typ
|
|
}
|
|
if (slot > 0 && slot > nr_args) {
|
|
merge_backward(write_types, slot, typ)
|
|
}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
rule = write_rules[op]
|
|
if (rule != null) {
|
|
slot = instr[rule[0]]
|
|
typ = rule[1]
|
|
if (typ == null) {
|
|
typ = access_value_type(instr[2])
|
|
}
|
|
typ = narrow_arith_type(write_types, param_types, instr, typ)
|
|
if (slot > 0 && slot > nr_args) {
|
|
merge_backward(write_types, slot, typ)
|
|
}
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
// Resolve move writes from known source invariants (fixed-point).
|
|
changed = true
|
|
iter = 0
|
|
while (changed && iter < length(write_types) + 4) {
|
|
changed = false
|
|
k = 0
|
|
while (k < length(move_dests)) {
|
|
slot = move_dests[k]
|
|
src = move_srcs[k]
|
|
src_typ = null
|
|
if (is_number(src) && src >= 0) {
|
|
if (src < length(write_types) && write_types[src] != null) {
|
|
src_typ = write_types[src]
|
|
} else if (param_types != null && src < length(param_types) && param_types[src] != null) {
|
|
src_typ = param_types[src]
|
|
}
|
|
}
|
|
if (src_typ != null) {
|
|
old_typ = write_types[slot]
|
|
merge_backward(write_types, slot, src_typ)
|
|
if (write_types[slot] != old_typ) {
|
|
changed = true
|
|
}
|
|
}
|
|
k = k + 1
|
|
}
|
|
iter = iter + 1
|
|
}
|
|
|
|
// Any remaining unresolved move write can carry arbitrary type.
|
|
k = 0
|
|
while (k < length(move_dests)) {
|
|
slot = move_dests[k]
|
|
src = move_srcs[k]
|
|
src_typ = null
|
|
if (is_number(src) && src >= 0) {
|
|
if (src < length(write_types) && write_types[src] != null) {
|
|
src_typ = write_types[src]
|
|
} else if (param_types != null && src < length(param_types) && param_types[src] != null) {
|
|
src_typ = param_types[src]
|
|
}
|
|
}
|
|
if (src_typ == null && slot > 0 && slot > nr_args) {
|
|
merge_backward(write_types, slot, T_UNKNOWN)
|
|
}
|
|
k = k + 1
|
|
}
|
|
|
|
// Closure-written slots can have any type at runtime — mark unknown
|
|
if (func.closure_written != null) {
|
|
cw_keys = array(func.closure_written)
|
|
k = 0
|
|
while (k < length(cw_keys)) {
|
|
slot = number(cw_keys[k])
|
|
if (slot >= 0 && slot < length(write_types)) {
|
|
write_types[slot] = T_UNKNOWN
|
|
}
|
|
k = k + 1
|
|
}
|
|
}
|
|
|
|
// Filter to only slots with known (non-unknown) types
|
|
k = 0
|
|
while (k < length(write_types)) {
|
|
if (write_types[k] == T_UNKNOWN) {
|
|
write_types[k] = null
|
|
}
|
|
k = k + 1
|
|
}
|
|
return write_types
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: eliminate_type_checks — language-level type narrowing
|
|
// Eliminates is_<type>/jump pairs when type is known.
|
|
// Reduces load_dynamic/store_dynamic to field/index forms.
|
|
// =========================================================
|
|
var eliminate_type_checks = function(func, param_types, write_types, log) {
|
|
var instructions = func.instructions
|
|
var nr_args = func.nr_args != null ? func.nr_args : 0
|
|
var num_instr = 0
|
|
var base_types = null
|
|
var slot_types = null
|
|
var nc = 0
|
|
var i = 0
|
|
var j = 0
|
|
var instr = null
|
|
var op = null
|
|
var dest = 0
|
|
var src = 0
|
|
var checked_type = null
|
|
var next = null
|
|
var next_op = null
|
|
var target_label = null
|
|
var src_known = null
|
|
var jlen = 0
|
|
var events = null
|
|
var old_op = null
|
|
|
|
if (instructions == null || length(instructions) == 0) {
|
|
return {}
|
|
}
|
|
|
|
if (log != null && log.events != null) {
|
|
events = log.events
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
|
|
// Pre-compute base types: params + write-invariant types
|
|
base_types = array(func.nr_slots)
|
|
j = 1
|
|
while (j <= nr_args) {
|
|
if (param_types[j] != null) {
|
|
base_types[j] = param_types[j]
|
|
}
|
|
j = j + 1
|
|
}
|
|
j = 0
|
|
while (j < length(write_types)) {
|
|
if (write_types[j] != null) {
|
|
base_types[j] = write_types[j]
|
|
}
|
|
j = j + 1
|
|
}
|
|
|
|
slot_types = array(base_types)
|
|
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
|
|
if (is_text(instr)) {
|
|
slot_types = array(base_types)
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
if (!is_array(instr)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
op = instr[0]
|
|
|
|
// Type-check + jump elimination
|
|
if (type_check_map[op] != null && i + 1 < num_instr) {
|
|
dest = instr[1]
|
|
src = instr[2]
|
|
checked_type = type_check_map[op]
|
|
next = instructions[i + 1]
|
|
|
|
if (is_array(next)) {
|
|
next_op = next[0]
|
|
|
|
// is_null + jump fusion: replace with jump_null / jump_not_null
|
|
if (op == "is_null" && (next_op == "jump_true" || next_op == "wary_true") && next[1] == dest) {
|
|
jlen = length(next)
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_tc_" + text(nc)
|
|
instructions[i + 1] = ["jump_null", src, next[2], next[jlen - 2], next[jlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "is_null_jump_fusion",
|
|
at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]],
|
|
why: {slot: src, fused_to: "jump_null"}
|
|
}
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
if (op == "is_null" && (next_op == "jump_false" || next_op == "wary_false") && next[1] == dest) {
|
|
jlen = length(next)
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_tc_" + text(nc)
|
|
instructions[i + 1] = ["jump_not_null", src, next[2], next[jlen - 2], next[jlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "is_null_jump_fusion",
|
|
at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]],
|
|
why: {slot: src, fused_to: "jump_not_null"}
|
|
}
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
|
|
if ((next_op == "jump_false" || next_op == "wary_false") && next[1] == dest) {
|
|
target_label = next[2]
|
|
if (slot_is(slot_types, src, checked_type)) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_tc_" + text(nc)
|
|
nc = nc + 1
|
|
instructions[i + 1] = "_nop_tc_" + text(nc)
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "known_type_eliminates_guard",
|
|
at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]],
|
|
why: {slot: src, known_type: slot_types[src], checked_type: checked_type}
|
|
}
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
src_known = slot_types[src]
|
|
if (src_known != null && src_known != T_UNKNOWN && src_known != checked_type) {
|
|
if (checked_type == T_NUM && (src_known == T_INT || src_known == T_FLOAT)) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_tc_" + text(nc)
|
|
nc = nc + 1
|
|
instructions[i + 1] = "_nop_tc_" + text(nc)
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "num_subsumes_int_float",
|
|
at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]],
|
|
why: {slot: src, known_type: src_known, checked_type: checked_type}
|
|
}
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
if ((checked_type == T_INT || checked_type == T_FLOAT) && src_known == T_NUM) {
|
|
// T_NUM could be int or float — not a mismatch, keep check
|
|
slot_types[dest] = T_BOOL
|
|
slot_types[src] = checked_type
|
|
i = i + 2
|
|
continue
|
|
}
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_tc_" + text(nc)
|
|
jlen = length(next)
|
|
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "incompatible_type_forces_jump",
|
|
at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]],
|
|
why: {slot: src, known_type: src_known, checked_type: checked_type}
|
|
}
|
|
}
|
|
slot_types[dest] = T_UNKNOWN
|
|
i = i + 2
|
|
continue
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
slot_types[src] = checked_type
|
|
i = i + 2
|
|
continue
|
|
}
|
|
|
|
if ((next_op == "jump_true" || next_op == "wary_true") && next[1] == dest) {
|
|
target_label = next[2]
|
|
if (slot_is(slot_types, src, checked_type)) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_tc_" + text(nc)
|
|
jlen = length(next)
|
|
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "known_type_eliminates_guard",
|
|
at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]],
|
|
why: {slot: src, known_type: slot_types[src], checked_type: checked_type}
|
|
}
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
src_known = slot_types[src]
|
|
if (src_known != null && src_known != T_UNKNOWN && src_known != checked_type) {
|
|
if (checked_type == T_NUM && (src_known == T_INT || src_known == T_FLOAT)) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_tc_" + text(nc)
|
|
jlen = length(next)
|
|
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "num_subsumes_int_float",
|
|
at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]],
|
|
why: {slot: src, known_type: src_known, checked_type: checked_type}
|
|
}
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
if ((checked_type == T_INT || checked_type == T_FLOAT) && src_known == T_NUM) {
|
|
// T_NUM could be int or float — not a mismatch, keep check
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_tc_" + text(nc)
|
|
nc = nc + 1
|
|
instructions[i + 1] = "_nop_tc_" + text(nc)
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "incompatible_type_forces_jump",
|
|
at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]],
|
|
why: {slot: src, known_type: src_known, checked_type: checked_type}
|
|
}
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 2
|
|
continue
|
|
}
|
|
}
|
|
|
|
slot_types[dest] = T_BOOL
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
// Dynamic access reduction
|
|
if (op == "load_dynamic") {
|
|
old_op = op
|
|
if (slot_is(slot_types, instr[2], T_RECORD) && slot_is(slot_types, instr[3], T_TEXT)) {
|
|
instr[0] = "load_field"
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "dynamic_record_to_field",
|
|
at: i, before: old_op, after: instr[0],
|
|
why: {
|
|
object_slot: instr[2], object_type: slot_types[instr[2]],
|
|
key_slot: instr[3], key_type: slot_types[instr[3]]
|
|
}
|
|
}
|
|
}
|
|
} else if (slot_is(slot_types, instr[2], T_ARRAY) && slot_is(slot_types, instr[3], T_INT)) {
|
|
instr[0] = "load_index"
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "dynamic_array_to_index",
|
|
at: i, before: old_op, after: instr[0],
|
|
why: {
|
|
object_slot: instr[2], object_type: slot_types[instr[2]],
|
|
key_slot: instr[3], key_type: slot_types[instr[3]]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
slot_types[instr[1]] = T_UNKNOWN
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (op == "store_dynamic") {
|
|
old_op = op
|
|
if (slot_is(slot_types, instr[1], T_RECORD) && slot_is(slot_types, instr[3], T_TEXT)) {
|
|
instr[0] = "store_field"
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "dynamic_record_to_field",
|
|
at: i, before: old_op, after: instr[0],
|
|
why: {
|
|
object_slot: instr[1], object_type: slot_types[instr[1]],
|
|
key_slot: instr[3], key_type: slot_types[instr[3]]
|
|
}
|
|
}
|
|
}
|
|
} else if (slot_is(slot_types, instr[1], T_ARRAY) && slot_is(slot_types, instr[3], T_INT)) {
|
|
instr[0] = "store_index"
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "dynamic_array_to_index",
|
|
at: i, before: old_op, after: instr[0],
|
|
why: {
|
|
object_slot: instr[1], object_type: slot_types[instr[1]],
|
|
key_slot: instr[3], key_type: slot_types[instr[3]]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
// Wary-to-certain: if slot type is T_BOOL, downgrade wary to certain jump
|
|
if (op == "wary_true" && slot_is(slot_types, instr[1], T_BOOL)) {
|
|
instr[0] = "jump_true"
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "wary_to_certain",
|
|
at: i, before: "wary_true", after: "jump_true",
|
|
why: {slot: instr[1], known_type: T_BOOL}
|
|
}
|
|
}
|
|
}
|
|
if (op == "wary_false" && slot_is(slot_types, instr[1], T_BOOL)) {
|
|
instr[0] = "jump_false"
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite",
|
|
pass: "eliminate_type_checks",
|
|
rule: "wary_to_certain",
|
|
at: i, before: "wary_false", after: "jump_false",
|
|
why: {slot: instr[1], known_type: T_BOOL}
|
|
}
|
|
}
|
|
}
|
|
|
|
track_types(slot_types, instr)
|
|
i = i + 1
|
|
}
|
|
|
|
return slot_types
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: simplify_algebra — algebraic identity & comparison
|
|
// Tracks known constant values. Rewrites identity ops to
|
|
// moves or constants. Folds same-slot comparisons.
|
|
// =========================================================
|
|
var simplify_algebra = function(func, log) {
|
|
var instructions = func.instructions
|
|
var num_instr = 0
|
|
var slot_values = null
|
|
var nc = 0
|
|
var i = 0
|
|
var instr = null
|
|
var op = null
|
|
var ilen = 0
|
|
var sv = null
|
|
var events = null
|
|
|
|
if (instructions == null || length(instructions) == 0) {
|
|
return null
|
|
}
|
|
|
|
if (log != null && log.events != null) {
|
|
events = log.events
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
slot_values = array(func.nr_slots)
|
|
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
|
|
if (is_text(instr)) {
|
|
slot_values = array(func.nr_slots)
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (!is_array(instr)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
op = instr[0]
|
|
ilen = length(instr)
|
|
|
|
// Track known constant values
|
|
if (op == "int") {
|
|
slot_values[instr[1]] = instr[2]
|
|
} else if (op == "access" && is_number(instr[2])) {
|
|
slot_values[instr[1]] = instr[2]
|
|
} else if (op == "true") {
|
|
slot_values[instr[1]] = true
|
|
} else if (op == "false") {
|
|
slot_values[instr[1]] = false
|
|
} else if (op == "move") {
|
|
sv = slot_values[instr[2]]
|
|
if (sv != null) {
|
|
slot_values[instr[1]] = sv
|
|
} else {
|
|
slot_values[instr[1]] = null
|
|
}
|
|
}
|
|
|
|
// Same-slot comparisons
|
|
if (is_number(instr[2]) && instr[2] == instr[3]) {
|
|
if (self_true_ops[op] == true) {
|
|
instructions[i] = ["true", instr[1], instr[ilen - 2], instr[ilen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "simplify_algebra",
|
|
rule: "self_eq", at: i,
|
|
before: instr, after: instructions[i],
|
|
why: {op: op, slot: instr[2]}
|
|
}
|
|
}
|
|
slot_values[instr[1]] = true
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (self_false_ops[op] == true) {
|
|
instructions[i] = ["false", instr[1], instr[ilen - 2], instr[ilen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "simplify_algebra",
|
|
rule: "self_ne", at: i,
|
|
before: instr, after: instructions[i],
|
|
why: {op: op, slot: instr[2]}
|
|
}
|
|
}
|
|
slot_values[instr[1]] = false
|
|
i = i + 1
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Clear value tracking for dest-producing ops (not reads-only)
|
|
if (op == "invoke" || op == "tail_invoke") {
|
|
slot_values[instr[2]] = null
|
|
} else if (no_clear_ops[op] != true) {
|
|
if (is_number(instr[1])) {
|
|
slot_values[instr[1]] = null
|
|
}
|
|
}
|
|
|
|
i = i + 1
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: simplify_booleans — not+jump fusion, double-not
|
|
// =========================================================
|
|
var simplify_booleans = function(func, log) {
|
|
var instructions = func.instructions
|
|
var num_instr = 0
|
|
var nc = 0
|
|
var i = 0
|
|
var instr = null
|
|
var next = null
|
|
var next_op = null
|
|
var nlen = 0
|
|
var events = null
|
|
|
|
if (instructions == null || length(instructions) == 0) {
|
|
return null
|
|
}
|
|
|
|
if (log != null && log.events != null) {
|
|
events = log.events
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
if (!is_array(instr) || instr[0] != "not" || i + 1 >= num_instr) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
next = instructions[i + 1]
|
|
if (!is_array(next)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
next_op = next[0]
|
|
nlen = length(next)
|
|
|
|
// not d, x; jump_false d, label → wary_true x, label
|
|
// (removing `not` removes coercion, so result must be wary)
|
|
if (next_op == "jump_false" && next[1] == instr[1]) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_bl_" + text(nc)
|
|
instructions[i + 1] = ["wary_true", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "simplify_booleans",
|
|
rule: "not_jump_false_fusion", at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]]
|
|
}
|
|
}
|
|
i = i + 2
|
|
continue
|
|
}
|
|
|
|
// not d, x; jump_true d, label → wary_false x, label
|
|
if (next_op == "jump_true" && next[1] == instr[1]) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_bl_" + text(nc)
|
|
instructions[i + 1] = ["wary_false", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "simplify_booleans",
|
|
rule: "not_jump_true_fusion", at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]]
|
|
}
|
|
}
|
|
i = i + 2
|
|
continue
|
|
}
|
|
|
|
// not d, x; wary_false d, label → wary_true x, label
|
|
if (next_op == "wary_false" && next[1] == instr[1]) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_bl_" + text(nc)
|
|
instructions[i + 1] = ["wary_true", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "simplify_booleans",
|
|
rule: "not_wary_false_fusion", at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]]
|
|
}
|
|
}
|
|
i = i + 2
|
|
continue
|
|
}
|
|
|
|
// not d, x; wary_true d, label → wary_false x, label
|
|
if (next_op == "wary_true" && next[1] == instr[1]) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_bl_" + text(nc)
|
|
instructions[i + 1] = ["wary_false", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "simplify_booleans",
|
|
rule: "not_wary_true_fusion", at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]]
|
|
}
|
|
}
|
|
i = i + 2
|
|
continue
|
|
}
|
|
|
|
// not d1, x; not d2, d1 → move d2, x
|
|
if (next_op == "not" && next[2] == instr[1]) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_bl_" + text(nc)
|
|
instructions[i + 1] = ["move", next[1], instr[2], next[nlen - 2], next[nlen - 1]]
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "simplify_booleans",
|
|
rule: "double_not", at: i,
|
|
before: [instr, next],
|
|
after: [instructions[i], instructions[i + 1]]
|
|
}
|
|
}
|
|
i = i + 2
|
|
continue
|
|
}
|
|
|
|
i = i + 1
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
var slot_idx_special = null
|
|
var get_slot_refs = null
|
|
var slot_def_special = null
|
|
var slot_use_special = null
|
|
var get_slot_defs = null
|
|
var get_slot_uses = null
|
|
|
|
// =========================================================
|
|
// Pass: eliminate_moves — copy propagation + self-move nop
|
|
// Tracks move chains within basic blocks, substitutes read
|
|
// operands to use the original source, and nops self-moves.
|
|
// =========================================================
|
|
var eliminate_moves = function(func, log) {
|
|
var instructions = func.instructions
|
|
var num_instr = 0
|
|
var nc = 0
|
|
var i = 0
|
|
var instr = null
|
|
var events = null
|
|
var copies = null
|
|
var key = null
|
|
var actual = null
|
|
var dest = 0
|
|
var src = 0
|
|
var wr = null
|
|
var write_pos = null
|
|
var op = null
|
|
var j = 0
|
|
var k = 0
|
|
var keys = null
|
|
var special = null
|
|
var limit = 0
|
|
|
|
if (instructions == null || length(instructions) == 0) {
|
|
return null
|
|
}
|
|
|
|
if (log != null && log.events != null) {
|
|
events = log.events
|
|
}
|
|
|
|
copies = {}
|
|
num_instr = length(instructions)
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
|
|
// Labels: clear copies at join points
|
|
if (is_text(instr)) {
|
|
if (!starts_with(instr, "_nop_")) {
|
|
copies = {}
|
|
}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
if (!is_array(instr)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
op = instr[0]
|
|
|
|
// Control flow without reads: clear copies
|
|
if (op == "jump" || op == "disrupt") {
|
|
copies = {}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
// Control flow with a read at position 1: substitute then clear
|
|
if (op == "return" || is_cond_jump(op)) {
|
|
actual = copies[text(instr[1])]
|
|
if (actual != null) {
|
|
instr[1] = actual
|
|
}
|
|
copies = {}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
// Move: copy propagation
|
|
if (op == "move") {
|
|
dest = instr[1]
|
|
src = instr[2]
|
|
|
|
// Follow transitive chain for src
|
|
actual = copies[text(src)]
|
|
if (actual == null) {
|
|
actual = src
|
|
}
|
|
|
|
// Rewrite the move's src operand
|
|
instr[2] = actual
|
|
|
|
// Kill stale entries for dest
|
|
key = text(dest)
|
|
copies[key] = null
|
|
keys = array(copies)
|
|
k = 0
|
|
while (k < length(keys)) {
|
|
if (copies[keys[k]] == dest) {
|
|
copies[keys[k]] = null
|
|
}
|
|
k = k + 1
|
|
}
|
|
|
|
// Record the new copy
|
|
copies[text(dest)] = actual
|
|
|
|
// Self-move after substitution → nop
|
|
if (dest == actual) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_mv_" + text(nc)
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "eliminate_moves",
|
|
rule: "self_move", at: i,
|
|
before: ["move", dest, src], after: instructions[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
// General instruction: substitute reads, then kill write
|
|
wr = write_rules[op]
|
|
write_pos = null
|
|
if (wr != null) {
|
|
write_pos = wr[0]
|
|
}
|
|
|
|
// Substitute read operands
|
|
special = slot_idx_special[op]
|
|
if (special != null) {
|
|
j = 0
|
|
while (j < length(special)) {
|
|
k = special[j]
|
|
if (k != write_pos && is_number(instr[k])) {
|
|
actual = copies[text(instr[k])]
|
|
if (actual != null) {
|
|
instr[k] = actual
|
|
}
|
|
}
|
|
j = j + 1
|
|
}
|
|
} else {
|
|
limit = length(instr) - 2
|
|
j = 1
|
|
while (j < limit) {
|
|
if (j != write_pos && is_number(instr[j])) {
|
|
actual = copies[text(instr[j])]
|
|
if (actual != null) {
|
|
instr[j] = actual
|
|
}
|
|
}
|
|
j = j + 1
|
|
}
|
|
}
|
|
|
|
// Kill write destination
|
|
if (write_pos != null && is_number(instr[write_pos])) {
|
|
dest = instr[write_pos]
|
|
key = text(dest)
|
|
copies[key] = null
|
|
keys = array(copies)
|
|
k = 0
|
|
while (k < length(keys)) {
|
|
if (copies[keys[k]] == dest) {
|
|
copies[keys[k]] = null
|
|
}
|
|
k = k + 1
|
|
}
|
|
}
|
|
|
|
i = i + 1
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: insert_stone_text — freeze mutable text at escape points
|
|
// Only inserts stone_text when the slot is provably T_TEXT.
|
|
// Escape points: setfield, setindex, store_field, store_index,
|
|
// store_dynamic, push, setarg, put (value leaving its slot).
|
|
// move: stone source only if source is still live after the move.
|
|
// =========================================================
|
|
|
|
// Map: escape opcode → index of the escaping slot in the instruction
|
|
var escape_slot_index = {
|
|
setfield: 3, setindex: 3,
|
|
store_field: 3, store_index: 3, store_dynamic: 3,
|
|
push: 2, setarg: 3, put: 1
|
|
}
|
|
|
|
// Build last_ref liveness array for a function's instructions.
|
|
// Returns array where last_ref[slot] = last instruction index referencing that slot.
|
|
// Uses get_slot_refs to only visit actual slot reference positions.
|
|
var build_slot_liveness = function(instructions, nr_slots) {
|
|
var last_ref = array(nr_slots, -1)
|
|
var n = length(instructions)
|
|
var refs = null
|
|
var i = 0
|
|
var j = 0
|
|
var s = 0
|
|
var instr = null
|
|
var label_map = null
|
|
var changed = false
|
|
var op = null
|
|
var target = null
|
|
var tpos = 0
|
|
|
|
// Scan instructions for slot references
|
|
while (i < n) {
|
|
instr = instructions[i]
|
|
if (is_array(instr)) {
|
|
refs = get_slot_refs(instr)
|
|
j = 0
|
|
while (j < length(refs)) {
|
|
s = instr[refs[j]]
|
|
if (is_number(s) && s >= 0 && s < nr_slots) {
|
|
last_ref[s] = i
|
|
}
|
|
j = j + 1
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
// Extend for backward jumps (loops)
|
|
label_map = {}
|
|
i = 0
|
|
while (i < n) {
|
|
instr = instructions[i]
|
|
if (is_text(instr) && !starts_with(instr, "_nop_")) {
|
|
label_map[instr] = i
|
|
}
|
|
i = i + 1
|
|
}
|
|
changed = true
|
|
while (changed) {
|
|
changed = false
|
|
i = 0
|
|
while (i < n) {
|
|
instr = instructions[i]
|
|
if (is_array(instr)) {
|
|
target = null
|
|
op = instr[0]
|
|
if (op == "jump") {
|
|
target = instr[1]
|
|
} else if (is_cond_jump(op)) {
|
|
target = instr[2]
|
|
}
|
|
if (target != null && is_text(target)) {
|
|
tpos = label_map[target]
|
|
if (tpos != null && tpos < i) {
|
|
s = 0
|
|
while (s < nr_slots) {
|
|
if (last_ref[s] >= 0 && last_ref[s] >= tpos && last_ref[s] < i) {
|
|
last_ref[s] = i
|
|
changed = true
|
|
}
|
|
s = s + 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
|
|
return last_ref
|
|
}
|
|
|
|
var insert_stone_text = function(func, log) {
|
|
var instructions = func.instructions
|
|
var nr_slots = func.nr_slots
|
|
var dpc = func.disruption_pc
|
|
var events = null
|
|
var slot_types = null
|
|
var result = null
|
|
var i = 0
|
|
var n = 0
|
|
var instr = null
|
|
var op = null
|
|
var esc = null
|
|
var slot = 0
|
|
var nc = 0
|
|
var shift = 0
|
|
var last_ref = null
|
|
|
|
if (instructions == null || length(instructions) == 0) {
|
|
return null
|
|
}
|
|
|
|
if (log != null && log.events != null) {
|
|
events = log.events
|
|
}
|
|
|
|
// Build liveness info (in separate function to stay under slot limit)
|
|
last_ref = build_slot_liveness(instructions, nr_slots)
|
|
|
|
// Walk instructions, tracking types, inserting stone_text
|
|
n = length(instructions)
|
|
slot_types = array(nr_slots, T_UNKNOWN)
|
|
result = []
|
|
i = 0
|
|
while (i < n) {
|
|
instr = instructions[i]
|
|
if (is_array(instr)) {
|
|
op = instr[0]
|
|
esc = escape_slot_index[op]
|
|
if (esc != null) {
|
|
slot = instr[esc]
|
|
if (is_number(slot) && slot_is(slot_types, slot, T_TEXT)) {
|
|
result[] = ["stone_text", slot]
|
|
nc = nc + 1
|
|
if (is_number(dpc) && i < dpc) shift = shift + 1
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "insert", pass: "insert_stone_text",
|
|
rule: "escape_stone", at: i, slot: slot, op: op
|
|
}
|
|
}
|
|
}
|
|
} else if (op == "move") {
|
|
// Stone source before move only if source is provably text
|
|
// AND source slot is still live after this instruction
|
|
slot = instr[2]
|
|
if (is_number(slot) && slot_is(slot_types, slot, T_TEXT) && last_ref[slot] > i) {
|
|
result[] = ["stone_text", slot]
|
|
nc = nc + 1
|
|
if (is_number(dpc) && i < dpc) shift = shift + 1
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "insert", pass: "insert_stone_text",
|
|
rule: "move_alias_stone", at: i, slot: slot
|
|
}
|
|
}
|
|
}
|
|
}
|
|
track_types(slot_types, instr)
|
|
}
|
|
result[] = instr
|
|
i = i + 1
|
|
}
|
|
|
|
if (nc > 0) {
|
|
func.instructions = result
|
|
if (is_number(dpc) && shift > 0) {
|
|
func.disruption_pc = dpc + shift
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: eliminate_unreachable — nop code after return/disrupt
|
|
// =========================================================
|
|
var eliminate_unreachable = function(func) {
|
|
var instructions = func.instructions
|
|
var num_instr = 0
|
|
var nc = 0
|
|
var after_return = false
|
|
var i = 0
|
|
var instr = null
|
|
|
|
if (instructions == null || length(instructions) == 0) {
|
|
return null
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
if (is_text(instr)) {
|
|
if (!starts_with(instr, "_nop_")) {
|
|
after_return = false
|
|
}
|
|
} else if (is_array(instr)) {
|
|
if (after_return) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_ur_" + text(nc)
|
|
} else if (instr[0] == "return") {
|
|
after_return = true
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: eliminate_unreachable_cfg — nop blocks not reachable
|
|
// from function entry under explicit jump control-flow.
|
|
// =========================================================
|
|
var eliminate_unreachable_cfg = function(func) {
|
|
var instructions = func.instructions
|
|
var num_instr = 0
|
|
var disruption_pc = -1
|
|
var label_map = null
|
|
var reachable = null
|
|
var stack = null
|
|
var sp = 0
|
|
var idx = 0
|
|
var tgt = null
|
|
var instr = null
|
|
var op = null
|
|
var nc = 0
|
|
|
|
if (instructions == null || length(instructions) == 0) {
|
|
return null
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
if (is_number(func.disruption_pc)) {
|
|
disruption_pc = func.disruption_pc
|
|
}
|
|
label_map = {}
|
|
idx = 0
|
|
while (idx < num_instr) {
|
|
instr = instructions[idx]
|
|
if (is_text(instr) && !starts_with(instr, "_nop_")) {
|
|
label_map[instr] = idx
|
|
}
|
|
idx = idx + 1
|
|
}
|
|
|
|
reachable = array(num_instr, false)
|
|
stack = [0]
|
|
if (disruption_pc > 0 && disruption_pc < num_instr) {
|
|
stack[] = disruption_pc
|
|
}
|
|
|
|
sp = 0
|
|
while (sp < length(stack)) {
|
|
idx = stack[sp]
|
|
sp = sp + 1
|
|
|
|
if (idx < 0 || idx >= num_instr || reachable[idx]) {
|
|
continue
|
|
}
|
|
reachable[idx] = true
|
|
instr = instructions[idx]
|
|
|
|
if (!is_array(instr)) {
|
|
stack[] = idx + 1
|
|
continue
|
|
}
|
|
|
|
op = instr[0]
|
|
if (op == "jump") {
|
|
tgt = label_map[instr[1]]
|
|
if (is_number(tgt)) stack[] = tgt
|
|
continue
|
|
}
|
|
if (is_cond_jump(op)) {
|
|
tgt = label_map[instr[2]]
|
|
if (is_number(tgt)) stack[] = tgt
|
|
stack[] = idx + 1
|
|
continue
|
|
}
|
|
if (op == "return" || op == "disrupt") {
|
|
continue
|
|
}
|
|
stack[] = idx + 1
|
|
}
|
|
|
|
idx = 0
|
|
while (idx < num_instr) {
|
|
if (!reachable[idx] && is_array(instructions[idx]) && (disruption_pc < 0 || idx >= disruption_pc)) {
|
|
nc = nc + 1
|
|
instructions[idx] = "_nop_ucfg_" + text(nc)
|
|
}
|
|
idx = idx + 1
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: eliminate_dead_jumps — jump to next label → nop
|
|
// =========================================================
|
|
var eliminate_dead_jumps = function(func, log) {
|
|
var instructions = func.instructions
|
|
var num_instr = 0
|
|
var nc = 0
|
|
var i = 0
|
|
var j = 0
|
|
var instr = null
|
|
var target_label = null
|
|
var peek = null
|
|
var events = null
|
|
|
|
if (instructions == null || length(instructions) == 0) {
|
|
return null
|
|
}
|
|
|
|
if (log != null && log.events != null) {
|
|
events = log.events
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
if (is_array(instr) && instr[0] == "jump") {
|
|
target_label = instr[1]
|
|
j = i + 1
|
|
while (j < num_instr) {
|
|
peek = instructions[j]
|
|
if (is_text(peek)) {
|
|
if (starts_with(peek, "_nop_")) {
|
|
j = j + 1
|
|
continue
|
|
}
|
|
if (peek == target_label) {
|
|
nc = nc + 1
|
|
instructions[i] = "_nop_dj_" + text(nc)
|
|
if (events != null) {
|
|
events[] = {
|
|
event: "rewrite", pass: "eliminate_dead_jumps",
|
|
rule: "jump_to_next", at: i,
|
|
before: instr, after: instructions[i],
|
|
why: {label: target_label}
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
if (is_array(peek)) {
|
|
break
|
|
}
|
|
j = j + 1
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: compress_slots — linear-scan register allocation
|
|
// Reuses slots with non-overlapping live ranges to reduce
|
|
// nr_slots. Mirrors mcode_compress_regs from mach.c.
|
|
// Works across all functions for captured-slot tracking.
|
|
// =========================================================
|
|
|
|
// Which instruction positions hold slot references (special cases)
|
|
slot_idx_special = {
|
|
get: [1], put: [1],
|
|
access: [1], int: [1], function: [1], regexp: [1],
|
|
true: [1], false: [1], null: [1],
|
|
record: [1], array: [1],
|
|
invoke: [1, 2], tail_invoke: [1, 2],
|
|
goinvoke: [1],
|
|
setarg: [1, 3],
|
|
frame: [1, 2], goframe: [1, 2],
|
|
jump: [], disrupt: [],
|
|
jump_true: [1], jump_false: [1], jump_not_null: [1],
|
|
wary_true: [1], wary_false: [1], jump_null: [1], jump_empty: [1],
|
|
return: [1],
|
|
stone_text: [1]
|
|
}
|
|
|
|
get_slot_refs = function(instr) {
|
|
var special = slot_idx_special[instr[0]]
|
|
var result = null
|
|
var j = 0
|
|
var limit = 0
|
|
if (special != null) return special
|
|
result = []
|
|
limit = length(instr) - 2
|
|
j = 1
|
|
while (j < limit) {
|
|
if (is_number(instr[j])) result[] = j
|
|
j = j + 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
// DEF/USE classification: which instruction positions are definitions vs uses
|
|
slot_def_special = {
|
|
get: [1], put: [], access: [1], int: [1], function: [1], regexp: [1],
|
|
true: [1], false: [1], null: [1], record: [1], array: [1],
|
|
invoke: [2], tail_invoke: [2], goinvoke: [],
|
|
move: [1], load_field: [1], load_index: [1], load_dynamic: [1],
|
|
pop: [1], frame: [1], goframe: [1],
|
|
setarg: [], store_field: [], store_index: [], store_dynamic: [],
|
|
delete: [1],
|
|
push: [], set_var: [], stone_text: [],
|
|
jump: [], jump_true: [], jump_false: [], jump_not_null: [],
|
|
wary_true: [], wary_false: [], jump_null: [], jump_empty: [],
|
|
return: [], disrupt: []
|
|
}
|
|
|
|
slot_use_special = {
|
|
get: [], put: [1], access: [], int: [], function: [], regexp: [],
|
|
true: [], false: [], null: [], record: [], array: [],
|
|
invoke: [1], tail_invoke: [1], goinvoke: [1],
|
|
move: [2], load_field: [2], load_index: [2, 3], load_dynamic: [2, 3],
|
|
pop: [2], frame: [2], goframe: [2],
|
|
setarg: [1, 3], store_field: [1, 3], store_index: [1, 2, 3],
|
|
store_dynamic: [1, 2, 3], delete: [2],
|
|
push: [1, 2], set_var: [1], stone_text: [1],
|
|
jump: [], jump_true: [1], jump_false: [1], jump_not_null: [1],
|
|
wary_true: [1], wary_false: [1], jump_null: [1], jump_empty: [1],
|
|
return: [1], disrupt: []
|
|
}
|
|
|
|
get_slot_defs = function(instr) {
|
|
var special = slot_def_special[instr[0]]
|
|
if (special != null) return special
|
|
return [1]
|
|
}
|
|
|
|
get_slot_uses = function(instr) {
|
|
var special = slot_use_special[instr[0]]
|
|
var result = null
|
|
var j = 0
|
|
var limit = 0
|
|
if (special != null) return special
|
|
result = []
|
|
limit = length(instr) - 2
|
|
j = 2
|
|
while (j < limit) {
|
|
if (is_number(instr[j])) result[] = j
|
|
j = j + 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
var compress_one_fn = function(func, captured_slots) {
|
|
var instructions = func.instructions
|
|
var nr_slots = func.nr_slots
|
|
var nr_args = func.nr_args != null ? func.nr_args : 0
|
|
var n = 0
|
|
var pinned = 0
|
|
var first_ref = null
|
|
var last_ref = null
|
|
var i = 0
|
|
var j = 0
|
|
var k = 0
|
|
var s = 0
|
|
var instr = null
|
|
var refs = null
|
|
var op = null
|
|
var target = null
|
|
var tpos = 0
|
|
var changed = false
|
|
var label_map = null
|
|
var live_slots = null
|
|
var live_first = null
|
|
var live_last = null
|
|
var cnt = 0
|
|
var key_s = 0
|
|
var key_f = 0
|
|
var key_l = 0
|
|
var remap = null
|
|
var pool = null
|
|
var next_phys = 0
|
|
var active_phys = null
|
|
var active_last = null
|
|
var phys = 0
|
|
var mi = 0
|
|
var new_max = 0
|
|
var old_val = 0
|
|
var new_active_phys = null
|
|
var new_active_last = null
|
|
var new_pool = null
|
|
|
|
if (instructions == null || !is_number(nr_slots) || nr_slots <= 1) return null
|
|
n = length(instructions)
|
|
pinned = 1 + nr_args
|
|
|
|
// Step 1: build live ranges
|
|
first_ref = array(nr_slots, -1)
|
|
last_ref = array(nr_slots, -1)
|
|
|
|
// Pin this + args
|
|
k = 0
|
|
while (k < pinned) {
|
|
first_ref[k] = 0
|
|
last_ref[k] = n
|
|
k = k + 1
|
|
}
|
|
|
|
// Scan instructions for slot references
|
|
i = 0
|
|
while (i < n) {
|
|
instr = instructions[i]
|
|
if (is_array(instr)) {
|
|
refs = get_slot_refs(instr)
|
|
j = 0
|
|
while (j < length(refs)) {
|
|
s = instr[refs[j]]
|
|
if (is_number(s) && s >= 0 && s < nr_slots) {
|
|
if (first_ref[s] < 0) first_ref[s] = i
|
|
last_ref[s] = i
|
|
}
|
|
j = j + 1
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
// Pin captured slots (AFTER scan so last_ref isn't overwritten)
|
|
if (captured_slots != null) {
|
|
k = 0
|
|
while (k < length(captured_slots)) {
|
|
s = captured_slots[k]
|
|
if (s >= 0 && s < nr_slots) {
|
|
if (first_ref[s] < 0) first_ref[s] = 0
|
|
last_ref[s] = n
|
|
}
|
|
k = k + 1
|
|
}
|
|
}
|
|
|
|
// Step 1b: extend for backward jumps (loops)
|
|
label_map = {}
|
|
i = 0
|
|
while (i < n) {
|
|
instr = instructions[i]
|
|
if (is_text(instr) && !starts_with(instr, "_nop_")) {
|
|
label_map[instr] = i
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
changed = true
|
|
while (changed) {
|
|
changed = false
|
|
i = 0
|
|
while (i < n) {
|
|
instr = instructions[i]
|
|
if (!is_array(instr)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
op = instr[0]
|
|
target = null
|
|
if (op == "jump") {
|
|
target = instr[1]
|
|
} else if (is_cond_jump(op)) {
|
|
target = instr[2]
|
|
}
|
|
if (target == null || !is_text(target)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
tpos = label_map[target]
|
|
if (tpos == null || tpos >= i) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
// Backward jump: extend slots live into loop
|
|
s = pinned
|
|
while (s < nr_slots) {
|
|
if (first_ref[s] >= 0 && first_ref[s] < tpos && last_ref[s] >= tpos && last_ref[s] < i) {
|
|
last_ref[s] = i
|
|
changed = true
|
|
}
|
|
s = s + 1
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
|
|
// Step 2: sort live intervals by first_ref
|
|
live_slots = []
|
|
live_first = []
|
|
live_last = []
|
|
s = pinned
|
|
while (s < nr_slots) {
|
|
if (first_ref[s] >= 0) {
|
|
live_slots[] = s
|
|
live_first[] = first_ref[s]
|
|
live_last[] = last_ref[s]
|
|
}
|
|
s = s + 1
|
|
}
|
|
|
|
cnt = length(live_slots)
|
|
i = 1
|
|
while (i < cnt) {
|
|
key_s = live_slots[i]
|
|
key_f = live_first[i]
|
|
key_l = live_last[i]
|
|
j = i - 1
|
|
while (j >= 0 && (live_first[j] > key_f || (live_first[j] == key_f && live_slots[j] > key_s))) {
|
|
live_slots[j + 1] = live_slots[j]
|
|
live_first[j + 1] = live_first[j]
|
|
live_last[j + 1] = live_last[j]
|
|
j = j - 1
|
|
}
|
|
live_slots[j + 1] = key_s
|
|
live_first[j + 1] = key_f
|
|
live_last[j + 1] = key_l
|
|
i = i + 1
|
|
}
|
|
|
|
// Linear-scan allocation
|
|
remap = array(nr_slots)
|
|
s = 0
|
|
while (s < nr_slots) {
|
|
remap[s] = s
|
|
s = s + 1
|
|
}
|
|
|
|
pool = []
|
|
next_phys = pinned
|
|
active_phys = []
|
|
active_last = []
|
|
|
|
i = 0
|
|
while (i < cnt) {
|
|
// Expire intervals whose last < live_first[i]
|
|
new_active_phys = []
|
|
new_active_last = []
|
|
j = 0
|
|
while (j < length(active_phys)) {
|
|
if (active_last[j] < live_first[i]) {
|
|
pool[] = active_phys[j]
|
|
} else {
|
|
new_active_phys[] = active_phys[j]
|
|
new_active_last[] = active_last[j]
|
|
}
|
|
j = j + 1
|
|
}
|
|
active_phys = new_active_phys
|
|
active_last = new_active_last
|
|
|
|
// Pick lowest available physical register
|
|
if (length(pool) > 0) {
|
|
mi = 0
|
|
j = 1
|
|
while (j < length(pool)) {
|
|
if (pool[j] < pool[mi]) mi = j
|
|
j = j + 1
|
|
}
|
|
phys = pool[mi]
|
|
new_pool = []
|
|
j = 0
|
|
while (j < length(pool)) {
|
|
if (j != mi) new_pool[] = pool[j]
|
|
j = j + 1
|
|
}
|
|
pool = new_pool
|
|
} else {
|
|
phys = next_phys
|
|
next_phys = next_phys + 1
|
|
}
|
|
|
|
remap[live_slots[i]] = phys
|
|
active_phys[] = phys
|
|
active_last[] = live_last[i]
|
|
i = i + 1
|
|
}
|
|
|
|
// Compute new nr_slots
|
|
new_max = pinned
|
|
s = 0
|
|
while (s < nr_slots) {
|
|
if (first_ref[s] >= 0 && remap[s] >= new_max) {
|
|
new_max = remap[s] + 1
|
|
}
|
|
s = s + 1
|
|
}
|
|
|
|
if (new_max >= nr_slots) return null
|
|
|
|
// Step 3: apply remap to instructions
|
|
i = 0
|
|
while (i < n) {
|
|
instr = instructions[i]
|
|
if (is_array(instr)) {
|
|
refs = get_slot_refs(instr)
|
|
j = 0
|
|
while (j < length(refs)) {
|
|
old_val = instr[refs[j]]
|
|
if (is_number(old_val) && old_val >= 0 && old_val < nr_slots) {
|
|
instr[refs[j]] = remap[old_val]
|
|
}
|
|
j = j + 1
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
func.nr_slots = new_max
|
|
return remap
|
|
}
|
|
|
|
var compress_slots = function(ir) {
|
|
if (ir == null || ir.main == null) return null
|
|
var functions = ir.functions != null ? ir.functions : []
|
|
var func_count = length(functions)
|
|
var parent_of = null
|
|
var captured = null
|
|
var remaps = null
|
|
var remap_sizes = null
|
|
var instrs = null
|
|
var instr = null
|
|
var child_idx = 0
|
|
var parent_slot = 0
|
|
var level = 0
|
|
var ancestor = 0
|
|
var caps = null
|
|
var found = false
|
|
var anc_remap = null
|
|
var old_slot = 0
|
|
var max_close = null
|
|
var needed = 0
|
|
var fi = 0
|
|
var i = 0
|
|
var j = 0
|
|
var k = 0
|
|
|
|
// Build parent_of: parent_of[i] = parent index, func_count = main
|
|
parent_of = array(func_count, -1)
|
|
|
|
// Scan main for function instructions
|
|
if (ir.main != null && ir.main.instructions != null) {
|
|
instrs = ir.main.instructions
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
instr = instrs[i]
|
|
if (is_array(instr) && instr[0] == "function") {
|
|
child_idx = instr[2]
|
|
if (child_idx >= 0 && child_idx < func_count) {
|
|
parent_of[child_idx] = func_count
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
|
|
// Scan each function for function instructions
|
|
fi = 0
|
|
while (fi < func_count) {
|
|
instrs = functions[fi].instructions
|
|
if (instrs != null) {
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
instr = instrs[i]
|
|
if (is_array(instr) && instr[0] == "function") {
|
|
child_idx = instr[2]
|
|
if (child_idx >= 0 && child_idx < func_count) {
|
|
parent_of[child_idx] = fi
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
fi = fi + 1
|
|
}
|
|
|
|
// Build captured slots per function
|
|
captured = array(func_count + 1)
|
|
i = 0
|
|
while (i < func_count + 1) {
|
|
captured[i] = []
|
|
i = i + 1
|
|
}
|
|
|
|
fi = 0
|
|
while (fi < func_count) {
|
|
instrs = functions[fi].instructions
|
|
if (instrs != null) {
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
instr = instrs[i]
|
|
if (is_array(instr) && (instr[0] == "get" || instr[0] == "put")) {
|
|
parent_slot = instr[2]
|
|
level = instr[3]
|
|
ancestor = fi
|
|
j = 0
|
|
while (j < level && ancestor >= 0) {
|
|
ancestor = parent_of[ancestor]
|
|
j = j + 1
|
|
}
|
|
if (ancestor >= 0) {
|
|
caps = captured[ancestor]
|
|
found = false
|
|
k = 0
|
|
while (k < length(caps)) {
|
|
if (caps[k] == parent_slot) {
|
|
found = true
|
|
k = length(caps)
|
|
}
|
|
k = k + 1
|
|
}
|
|
if (!found) caps[] = parent_slot
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
fi = fi + 1
|
|
}
|
|
|
|
// Compress each function and save remap tables
|
|
remaps = array(func_count + 1)
|
|
remap_sizes = array(func_count + 1, 0)
|
|
|
|
fi = 0
|
|
while (fi < func_count) {
|
|
remap_sizes[fi] = functions[fi].nr_slots
|
|
remaps[fi] = compress_one_fn(functions[fi], captured[fi])
|
|
fi = fi + 1
|
|
}
|
|
|
|
if (ir.main != null) {
|
|
remap_sizes[func_count] = ir.main.nr_slots
|
|
remaps[func_count] = compress_one_fn(ir.main, captured[func_count])
|
|
}
|
|
|
|
// Fix get/put parent_slot references using ancestor remap tables
|
|
// and track the max close slot per ancestor for nr_close_slots update
|
|
max_close = array(func_count + 1, -1)
|
|
fi = 0
|
|
while (fi < func_count) {
|
|
instrs = functions[fi].instructions
|
|
if (instrs != null) {
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
instr = instrs[i]
|
|
if (is_array(instr) && (instr[0] == "get" || instr[0] == "put")) {
|
|
level = instr[3]
|
|
ancestor = fi
|
|
j = 0
|
|
while (j < level && ancestor >= 0) {
|
|
ancestor = parent_of[ancestor]
|
|
j = j + 1
|
|
}
|
|
if (ancestor >= 0 && remaps[ancestor] != null) {
|
|
anc_remap = remaps[ancestor]
|
|
old_slot = instr[2]
|
|
if (old_slot >= 0 && old_slot < remap_sizes[ancestor]) {
|
|
instr[2] = anc_remap[old_slot]
|
|
}
|
|
}
|
|
if (ancestor >= 0 && instr[2] > max_close[ancestor]) {
|
|
max_close[ancestor] = instr[2]
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
fi = fi + 1
|
|
}
|
|
|
|
// Update nr_close_slots for functions whose close slots were remapped.
|
|
// Frame shortening keeps 1 + nr_args + nr_close_slots slots, so
|
|
// nr_close_slots must cover the highest-numbered close slot.
|
|
fi = 0
|
|
while (fi < func_count) {
|
|
if (max_close[fi] >= 0) {
|
|
needed = max_close[fi] - (functions[fi].nr_args != null ? functions[fi].nr_args : 0)
|
|
if (needed > functions[fi].nr_close_slots) {
|
|
functions[fi].nr_close_slots = needed
|
|
}
|
|
}
|
|
fi = fi + 1
|
|
}
|
|
if (max_close[func_count] >= 0 && ir.main != null) {
|
|
needed = max_close[func_count] - (ir.main.nr_args != null ? ir.main.nr_args : 0)
|
|
if (needed > ir.main.nr_close_slots) {
|
|
ir.main.nr_close_slots = needed
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pre-pass: mark closure-written slots
|
|
// Scans child functions for 'put' instructions and annotates
|
|
// the target ancestor func with closure_written[slot] = true.
|
|
// =========================================================
|
|
var mark_closure_writes = function(ir) {
|
|
var functions = ir.functions != null ? ir.functions : []
|
|
var fc = length(functions)
|
|
var parent_of = array(fc, -1)
|
|
var instrs = null
|
|
var instr = null
|
|
var fi = 0
|
|
var i = 0
|
|
var j = 0
|
|
var level = 0
|
|
var anc = 0
|
|
var slot = 0
|
|
var target = null
|
|
|
|
if (fc == 0) {
|
|
return null
|
|
}
|
|
|
|
// Build parent_of map
|
|
if (ir.main != null && ir.main.instructions != null) {
|
|
instrs = ir.main.instructions
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
instr = instrs[i]
|
|
if (is_array(instr) && instr[0] == "function") {
|
|
if (instr[2] >= 0 && instr[2] < fc) {
|
|
parent_of[instr[2]] = fc
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
fi = 0
|
|
while (fi < fc) {
|
|
instrs = functions[fi].instructions
|
|
if (instrs != null) {
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
instr = instrs[i]
|
|
if (is_array(instr) && instr[0] == "function") {
|
|
if (instr[2] >= 0 && instr[2] < fc) {
|
|
parent_of[instr[2]] = fi
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
fi = fi + 1
|
|
}
|
|
|
|
// Scan for 'put' instructions and mark ancestor slots
|
|
fi = 0
|
|
while (fi < fc) {
|
|
instrs = functions[fi].instructions
|
|
if (instrs != null) {
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
instr = instrs[i]
|
|
if (is_array(instr) && instr[0] == "put") {
|
|
slot = instr[2]
|
|
level = instr[3]
|
|
anc = fi
|
|
j = 0
|
|
while (j < level && anc >= 0) {
|
|
anc = parent_of[anc]
|
|
j = j + 1
|
|
}
|
|
if (anc >= 0) {
|
|
if (anc == fc) {
|
|
target = ir.main
|
|
} else {
|
|
target = functions[anc]
|
|
}
|
|
if (target != null) {
|
|
if (target.closure_written == null) {
|
|
target.closure_written = {}
|
|
}
|
|
target.closure_written[text(slot)] = true
|
|
}
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
}
|
|
fi = fi + 1
|
|
}
|
|
ir._parent_of = parent_of
|
|
ir._parent_fc = fc
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Resolve closure slot types from parent write_types.
|
|
// For each `get` in func, walk the parent chain and look up
|
|
// the ancestor's inferred write type for that closure slot.
|
|
// =========================================================
|
|
var resolve_closure_types = function(func, fi, ir) {
|
|
var parent_of = ir._parent_of
|
|
var fc = ir._parent_fc
|
|
var instructions = func.instructions
|
|
var num_instr = 0
|
|
var closure_types = null
|
|
var i = 0
|
|
var instr = null
|
|
var slot = 0
|
|
var depth = 0
|
|
var anc = 0
|
|
var j = 0
|
|
var target = null
|
|
var typ = null
|
|
var key = null
|
|
|
|
if (instructions == null || parent_of == null) {
|
|
return null
|
|
}
|
|
|
|
num_instr = length(instructions)
|
|
closure_types = {}
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
if (is_array(instr) && instr[0] == "get") {
|
|
slot = instr[2]
|
|
depth = instr[3]
|
|
key = text(slot) + "_" + text(depth)
|
|
if (closure_types[key] == null) {
|
|
anc = fi
|
|
j = 0
|
|
while (j < depth && anc >= 0) {
|
|
anc = parent_of[anc]
|
|
j = j + 1
|
|
}
|
|
if (anc >= 0) {
|
|
if (anc == fc) {
|
|
target = ir.main
|
|
} else {
|
|
target = ir.functions[anc]
|
|
}
|
|
if (target != null && target._write_types != null) {
|
|
typ = target._write_types[slot]
|
|
if (typ != null) {
|
|
closure_types[key] = typ
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
func._closure_slot_types = closure_types
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: diagnose_function — emit diagnostics from type info
|
|
// Runs after dead code elimination on surviving instructions.
|
|
// =========================================================
|
|
var diagnose_function = function(func, ctx, ir) {
|
|
var param_types = ctx.param_types
|
|
var write_types = ctx.write_types
|
|
var instructions = func.instructions
|
|
var nr_args = func.nr_args != null ? func.nr_args : 0
|
|
var num_instr = 0
|
|
var base_types = null
|
|
var cur_types = null
|
|
var i = 0
|
|
var j = 0
|
|
var instr = null
|
|
var op = null
|
|
var n = 0
|
|
var line = 0
|
|
var col = 0
|
|
var known = null
|
|
var filename = ir.filename != null ? ir.filename : "<unknown>"
|
|
var frame_callee = {}
|
|
var frame_argc = {}
|
|
var callee_slot = null
|
|
var obj_type = null
|
|
var key_type = null
|
|
var module_slots = {}
|
|
var slot_arity = {}
|
|
var ms_i = 0
|
|
var ms = null
|
|
var exp_info = null
|
|
var f_slot_key = null
|
|
var cs = null
|
|
var argc = null
|
|
var known_arity = null
|
|
var load_field_null = false
|
|
|
|
// Build module_slots map from ir._module_summaries
|
|
if (ir._module_summaries != null) {
|
|
ms_i = 0
|
|
while (ms_i < length(ir._module_summaries)) {
|
|
ms = ir._module_summaries[ms_i]
|
|
module_slots[text(ms.slot)] = ms.summary
|
|
ms_i = ms_i + 1
|
|
}
|
|
}
|
|
|
|
if (instructions == null || length(instructions) == 0) return null
|
|
|
|
num_instr = length(instructions)
|
|
|
|
// Pre-compute base types from params + write-invariant types
|
|
base_types = array(func.nr_slots)
|
|
j = 1
|
|
while (j <= nr_args) {
|
|
if (param_types != null && param_types[j] != null) {
|
|
base_types[j] = param_types[j]
|
|
}
|
|
j = j + 1
|
|
}
|
|
if (write_types != null) {
|
|
j = 0
|
|
while (j < length(write_types)) {
|
|
if (write_types[j] != null) {
|
|
base_types[j] = write_types[j]
|
|
}
|
|
j = j + 1
|
|
}
|
|
}
|
|
|
|
cur_types = array(base_types)
|
|
|
|
var emit = function(severity, line, col, message) {
|
|
ir._diagnostics[] = {
|
|
severity: severity,
|
|
file: filename,
|
|
line: line,
|
|
col: col,
|
|
message: message
|
|
}
|
|
}
|
|
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
|
|
if (is_text(instr)) {
|
|
// Label — reset types to base
|
|
if (!starts_with(instr, "_nop_")) {
|
|
cur_types = array(base_types)
|
|
}
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
if (!is_array(instr)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
op = instr[0]
|
|
n = length(instr)
|
|
line = instr[n - 2]
|
|
col = instr[n - 1]
|
|
|
|
// Track frame/invoke correlation
|
|
if (op == "frame" || op == "goframe") {
|
|
frame_callee[text(instr[1])] = instr[2]
|
|
if (n > 4) {
|
|
frame_argc[text(instr[1])] = instr[3]
|
|
}
|
|
}
|
|
|
|
// --- Error checks (proven to always disrupt) ---
|
|
|
|
if (op == "frame" || op == "goframe") {
|
|
callee_slot = instr[2]
|
|
known = cur_types[callee_slot]
|
|
if (known == T_NULL) {
|
|
emit("error", line, col, "invoking null — will always disrupt")
|
|
} else if (known != null && known != T_UNKNOWN && known != T_FUNCTION && known != T_RECORD) {
|
|
emit("error", line, col, `invoking ${known} — will always disrupt`)
|
|
}
|
|
}
|
|
|
|
if (op == "invoke" || op == "tail_invoke") {
|
|
f_slot_key = text(instr[1])
|
|
cs = frame_callee[f_slot_key]
|
|
argc = frame_argc[f_slot_key]
|
|
if (cs != null && argc != null) {
|
|
known_arity = slot_arity[text(cs)]
|
|
if (known_arity != null) {
|
|
if (argc > known_arity) {
|
|
emit("error", line, col, `function expects ${text(known_arity)} args, called with ${text(argc)}`)
|
|
} else if (argc < known_arity) {
|
|
emit("warning", line, col, `function expects ${text(known_arity)} args, called with ${text(argc)}`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (op == "store_field") {
|
|
obj_type = cur_types[instr[1]]
|
|
if (obj_type == T_TEXT) {
|
|
emit("error", line, col, "storing property on text — text is immutable")
|
|
} else if (obj_type == T_ARRAY) {
|
|
emit("error", line, col, "storing named property on array — use index or push")
|
|
}
|
|
}
|
|
|
|
if (op == "store_index") {
|
|
obj_type = cur_types[instr[1]]
|
|
if (obj_type == T_TEXT) {
|
|
emit("error", line, col, "storing index on text — text is immutable")
|
|
} else if (obj_type == T_RECORD) {
|
|
emit("error", line, col, "storing numeric index on record — use text key")
|
|
}
|
|
}
|
|
|
|
if (op == "store_dynamic") {
|
|
obj_type = cur_types[instr[1]]
|
|
if (obj_type == T_TEXT) {
|
|
emit("error", line, col, "storing on text — text is immutable")
|
|
}
|
|
}
|
|
|
|
if (op == "push") {
|
|
obj_type = cur_types[instr[1]]
|
|
if (obj_type != null && obj_type != T_UNKNOWN && obj_type != T_ARRAY) {
|
|
emit("error", line, col, `push on ${obj_type} — only arrays support push`)
|
|
}
|
|
}
|
|
|
|
// Note: arithmetic (add/subtract/etc), bitwise, and concat ops are NOT
|
|
// checked here because the mcode generator emits type-dispatch guards
|
|
// before these instructions. The guards ensure correct types at runtime.
|
|
|
|
// --- Warning checks (likely bug) ---
|
|
|
|
load_field_null = false
|
|
if (op == "load_field") {
|
|
obj_type = cur_types[instr[2]]
|
|
if (obj_type == T_ARRAY) {
|
|
emit("warning", line, col, "named property access on array — always returns null")
|
|
load_field_null = true
|
|
} else if (obj_type == T_TEXT) {
|
|
emit("warning", line, col, "named property access on text — always returns null")
|
|
load_field_null = true
|
|
}
|
|
// Cross-module: check if obj is a module with known exports
|
|
ms = module_slots[text(instr[2])]
|
|
if (ms != null && ms.exports != null && is_text(instr[3])) {
|
|
exp_info = ms.exports[instr[3]]
|
|
if (exp_info == null) {
|
|
emit("warning", line, col, `module does not export '${instr[3]}'`)
|
|
} else if (exp_info.type == "function") {
|
|
cur_types[instr[1]] = T_FUNCTION
|
|
slot_arity[text(instr[1])] = exp_info.arity
|
|
}
|
|
}
|
|
}
|
|
|
|
if (op == "load_dynamic") {
|
|
obj_type = cur_types[instr[2]]
|
|
key_type = cur_types[instr[3]]
|
|
if (obj_type == T_ARRAY && key_type == T_TEXT) {
|
|
emit("warning", line, col, "text key on array — always returns null")
|
|
}
|
|
if (obj_type == T_TEXT && key_type != null && key_type != T_UNKNOWN && key_type != T_INT) {
|
|
emit("warning", line, col, `${key_type} key on text — requires integer index`)
|
|
}
|
|
if (obj_type == T_RECORD && key_type != null && key_type != T_UNKNOWN && key_type != T_TEXT) {
|
|
emit("warning", line, col, `${key_type} key on record — requires text key`)
|
|
}
|
|
}
|
|
|
|
// Update types for this instruction
|
|
track_types(cur_types, instr)
|
|
|
|
// Override: load_field on array/text always returns null
|
|
if (load_field_null) {
|
|
cur_types[instr[1]] = T_NULL
|
|
}
|
|
|
|
i = i + 1
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
// =========================================================
|
|
// Sensory function IR synthesis for callback inlining
|
|
// =========================================================
|
|
var sensory_opcodes = {
|
|
is_array: "is_array", is_function: "is_func", is_object: "is_record",
|
|
is_stone: "is_stone", is_integer: "is_int", is_text: "is_text",
|
|
is_number: "is_num", is_logical: "is_bool", is_null: "is_null",
|
|
is_blob: "is_blob", is_data: "is_data",
|
|
is_true: "is_true", is_false: "is_false", is_fit: "is_fit",
|
|
is_character: "is_char", is_digit: "is_digit", is_letter: "is_letter",
|
|
is_lower: "is_lower", is_upper: "is_upper", is_whitespace: "is_ws",
|
|
is_actor: "is_actor", length: "length"
|
|
}
|
|
|
|
var make_sensory_ir = function(name) {
|
|
var opcode = sensory_opcodes[name]
|
|
if (opcode == null) return null
|
|
return {
|
|
name: name, nr_args: 1, nr_close_slots: 0, nr_slots: 3,
|
|
instructions: [[opcode, 2, 1, 0, 0], ["return", 2, 0, 0]]
|
|
}
|
|
}
|
|
|
|
// =========================================================
|
|
// Inline eligibility check
|
|
// =========================================================
|
|
var prefer_inline_set = {
|
|
filter: true, every: true, some: true, arrfor: true,
|
|
reduce: true, array: true
|
|
}
|
|
|
|
// Structural eligibility: closures, get/put, disruption, nested functions
|
|
var can_inline_structural = function(callee_func) {
|
|
var instrs = null
|
|
var i = 0
|
|
var instr = null
|
|
if (callee_func.nr_close_slots > 0) return false
|
|
instrs = callee_func.instructions
|
|
if (instrs == null) return false
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
instr = instrs[i]
|
|
if (is_array(instr)) {
|
|
if (instr[0] == "get" || instr[0] == "put") {
|
|
return false
|
|
}
|
|
// Reject if function creates child functions (closures may capture
|
|
// from the inlined frame, breaking get/put slot references)
|
|
if (instr[0] == "function") {
|
|
return false
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
if (callee_func.disruption_pc != null && callee_func.disruption_pc > 0) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Size eligibility: instruction count check
|
|
var can_inline_size = function(callee_func, is_prefer) {
|
|
var instrs = callee_func.instructions
|
|
var count = 0
|
|
var i = 0
|
|
var limit = 0
|
|
if (instrs == null) return false
|
|
i = 0
|
|
while (i < length(instrs)) {
|
|
if (is_array(instrs[i])) count = count + 1
|
|
i = i + 1
|
|
}
|
|
limit = is_prefer ? 200 : 40
|
|
return count <= limit
|
|
}
|
|
|
|
var can_inline = function(callee_func, is_prefer) {
|
|
if (!can_inline_structural(callee_func)) return false
|
|
return can_inline_size(callee_func, is_prefer)
|
|
}
|
|
|
|
// =========================================================
|
|
// Pass: inline_calls — inline same-module + sensory functions
|
|
// =========================================================
|
|
var inline_counter = 0
|
|
|
|
var inline_calls = function(func, ir, log) {
|
|
var instructions = func.instructions
|
|
var num_instr = 0
|
|
var i = 0
|
|
var j = 0
|
|
var k = 0
|
|
var instr = null
|
|
var op = null
|
|
var changed = false
|
|
var inline_count = 0
|
|
var max_inlines = 20
|
|
var slot_to_func_idx = {}
|
|
var slot_to_intrinsic = {}
|
|
var callee_slot = 0
|
|
var frame_slot = 0
|
|
var argc = 0
|
|
var result_slot = 0
|
|
var call_start = 0
|
|
var call_end = 0
|
|
var arg_slots = null
|
|
var callee_func = null
|
|
var is_prefer = false
|
|
var base = 0
|
|
var remap = null
|
|
var cinstr = null
|
|
var cop = null
|
|
var new_instr = null
|
|
var refs = null
|
|
var label_prefix = null
|
|
var cont_label = null
|
|
var spliced = null
|
|
var before = null
|
|
var after = null
|
|
var inlined_body = null
|
|
var fi = null
|
|
var intrinsic_name = null
|
|
var is_single_use = false
|
|
var ref_count = 0
|
|
var ri = 0
|
|
|
|
if (instructions == null) return false
|
|
num_instr = length(instructions)
|
|
if (num_instr == 0) return false
|
|
|
|
// Build resolution maps
|
|
i = 0
|
|
while (i < num_instr) {
|
|
instr = instructions[i]
|
|
if (is_array(instr)) {
|
|
op = instr[0]
|
|
if (op == "function") {
|
|
slot_to_func_idx[text(instr[1])] = instr[2]
|
|
} else if (op == "access" && is_object(instr[2]) && instr[2].make == "intrinsic") {
|
|
slot_to_intrinsic[text(instr[1])] = instr[2].name
|
|
}
|
|
}
|
|
i = i + 1
|
|
}
|
|
|
|
// Scan for frame/setarg/invoke sequences and inline
|
|
i = 0
|
|
while (i < length(instructions)) {
|
|
instr = instructions[i]
|
|
if (!is_array(instr) || instr[0] != "frame") {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (inline_count >= max_inlines) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
frame_slot = instr[1]
|
|
callee_slot = instr[2]
|
|
argc = instr[3]
|
|
call_start = i
|
|
|
|
// Collect setarg and find invoke
|
|
arg_slots = array(argc + 1, -1)
|
|
j = i + 1
|
|
call_end = -1
|
|
while (j < length(instructions)) {
|
|
instr = instructions[j]
|
|
if (!is_array(instr)) {
|
|
j = j + 1
|
|
continue
|
|
}
|
|
op = instr[0]
|
|
if (op == "setarg" && instr[1] == frame_slot) {
|
|
arg_slots[instr[2]] = instr[3]
|
|
} else if ((op == "invoke" || op == "tail_invoke") && instr[1] == frame_slot) {
|
|
result_slot = instr[2]
|
|
call_end = j
|
|
j = j + 1
|
|
break
|
|
} else if (op == "frame" || op == "goframe") {
|
|
// Another frame before invoke — abort this pattern
|
|
break
|
|
}
|
|
j = j + 1
|
|
}
|
|
|
|
if (call_end < 0) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
// Resolve callee
|
|
callee_func = null
|
|
is_prefer = false
|
|
|
|
fi = slot_to_func_idx[text(callee_slot)]
|
|
if (fi != null && ir.functions != null && fi >= 0 && fi < length(ir.functions)) {
|
|
callee_func = ir.functions[fi]
|
|
}
|
|
|
|
if (callee_func == null) {
|
|
intrinsic_name = slot_to_intrinsic[text(callee_slot)]
|
|
if (intrinsic_name != null) {
|
|
if (sensory_opcodes[intrinsic_name] != null) {
|
|
callee_func = make_sensory_ir(intrinsic_name)
|
|
}
|
|
if (callee_func != null) {
|
|
is_prefer = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if (callee_func == null) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
// Check if callee is a single-use function literal — skip size limit
|
|
is_single_use = false
|
|
if (fi != null) {
|
|
ref_count = 0
|
|
ri = 0
|
|
while (ri < length(instructions)) {
|
|
if (is_array(instructions[ri])) {
|
|
// Count frame instructions that use this slot as callee (position 2)
|
|
if (instructions[ri][0] == "frame" && instructions[ri][2] == callee_slot) {
|
|
ref_count = ref_count + 1
|
|
}
|
|
// Also count setarg where slot is passed as value (position 3)
|
|
if (instructions[ri][0] == "setarg" && instructions[ri][3] == callee_slot) {
|
|
ref_count = ref_count + 1
|
|
}
|
|
}
|
|
ri = ri + 1
|
|
}
|
|
if (ref_count <= 1) is_single_use = true
|
|
}
|
|
|
|
// Check eligibility
|
|
if (!can_inline_structural(callee_func)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
if (!is_single_use && !can_inline_size(callee_func, is_prefer)) {
|
|
i = i + 1
|
|
continue
|
|
}
|
|
|
|
// Slot remapping
|
|
base = func.nr_slots
|
|
func.nr_slots = func.nr_slots + callee_func.nr_slots
|
|
remap = array(callee_func.nr_slots, -1)
|
|
|
|
// Slot 0 (this) → arg_slots[0] if provided, else allocate a null slot
|
|
if (length(arg_slots) > 0 && arg_slots[0] >= 0) {
|
|
remap[0] = arg_slots[0]
|
|
} else {
|
|
remap[0] = base
|
|
}
|
|
|
|
// Params 1..nr_args → corresponding arg_slots
|
|
j = 1
|
|
while (j <= callee_func.nr_args) {
|
|
if (j < length(arg_slots) && arg_slots[j] >= 0) {
|
|
remap[j] = arg_slots[j]
|
|
} else {
|
|
remap[j] = base + j
|
|
}
|
|
j = j + 1
|
|
}
|
|
|
|
// Temporaries → fresh slots
|
|
j = callee_func.nr_args + 1
|
|
while (j < callee_func.nr_slots) {
|
|
remap[j] = base + j
|
|
j = j + 1
|
|
}
|
|
|
|
// Generate unique label prefix
|
|
inline_counter = inline_counter + 1
|
|
label_prefix = "_inl" + text(inline_counter) + "_"
|
|
cont_label = label_prefix + "cont"
|
|
|
|
// Build inlined body with remapping
|
|
// Unmapped params (e.g. caller passes 1 arg to a 2-param function)
|
|
// must be explicitly nulled. compress_slots may merge the fresh
|
|
// slot (base+j) with a previously-live slot that holds a non-null
|
|
// value, so the default-param jump_not_null preamble would skip
|
|
// the default assignment and use a stale value instead.
|
|
inlined_body = []
|
|
j = 0
|
|
while (j <= callee_func.nr_args) {
|
|
if (!(j < length(arg_slots) && arg_slots[j] >= 0)) {
|
|
inlined_body[] = ["null", remap[j], 0, 0]
|
|
}
|
|
j = j + 1
|
|
}
|
|
k = 0
|
|
while (k < length(callee_func.instructions)) {
|
|
cinstr = callee_func.instructions[k]
|
|
|
|
// Labels (strings that aren't nop markers)
|
|
if (is_text(cinstr)) {
|
|
if (starts_with(cinstr, "_nop_")) {
|
|
inlined_body[] = cinstr
|
|
} else {
|
|
inlined_body[] = label_prefix + cinstr
|
|
}
|
|
k = k + 1
|
|
continue
|
|
}
|
|
|
|
if (!is_array(cinstr)) {
|
|
inlined_body[] = cinstr
|
|
k = k + 1
|
|
continue
|
|
}
|
|
|
|
cop = cinstr[0]
|
|
|
|
// Handle return → move + jump to continuation
|
|
if (cop == "return") {
|
|
new_instr = ["move", result_slot, remap[cinstr[1]], cinstr[2], cinstr[3]]
|
|
inlined_body[] = new_instr
|
|
inlined_body[] = ["jump", cont_label, cinstr[2], cinstr[3]]
|
|
k = k + 1
|
|
continue
|
|
}
|
|
|
|
// Clone and remap the instruction
|
|
new_instr = array(cinstr)
|
|
refs = get_slot_refs(cinstr)
|
|
j = 0
|
|
while (j < length(refs)) {
|
|
if (new_instr[refs[j]] >= 0 && new_instr[refs[j]] < length(remap)) {
|
|
new_instr[refs[j]] = remap[new_instr[refs[j]]]
|
|
}
|
|
j = j + 1
|
|
}
|
|
|
|
// Remap labels in jump instructions
|
|
if (cop == "jump" && is_text(cinstr[1]) && !starts_with(cinstr[1], "_nop_")) {
|
|
new_instr[1] = label_prefix + cinstr[1]
|
|
} else if (is_cond_jump(cop)
|
|
&& is_text(cinstr[2]) && !starts_with(cinstr[2], "_nop_")) {
|
|
new_instr[2] = label_prefix + cinstr[2]
|
|
}
|
|
|
|
// Skip function instructions (don't inline nested function definitions)
|
|
if (cop == "function") {
|
|
// Keep the instruction but don't remap func_id — it still refers to ir.functions
|
|
// Only remap slot position 1 (the destination slot)
|
|
new_instr = array(cinstr)
|
|
if (cinstr[1] >= 0 && cinstr[1] < length(remap)) {
|
|
new_instr[1] = remap[cinstr[1]]
|
|
}
|
|
}
|
|
|
|
inlined_body[] = new_instr
|
|
k = k + 1
|
|
}
|
|
|
|
// Add continuation label
|
|
inlined_body[] = cont_label
|
|
|
|
// Splice: replace instructions[call_start..call_end] with inlined_body
|
|
before = array(instructions, 0, call_start)
|
|
after = array(instructions, call_end + 1, length(instructions))
|
|
spliced = array(before, inlined_body)
|
|
instructions = array(spliced, after)
|
|
func.instructions = instructions
|
|
|
|
changed = true
|
|
inline_count = inline_count + 1
|
|
|
|
// Continue scanning from after the inlined body
|
|
i = call_start + length(inlined_body)
|
|
}
|
|
|
|
return changed
|
|
}
|
|
|
|
// =========================================================
|
|
// Compose all passes
|
|
// =========================================================
|
|
var optimize_function = function(func, log) {
|
|
var param_types = null
|
|
var write_types = null
|
|
var slot_types = null
|
|
var run_cycle = function(suffix) {
|
|
var name = null
|
|
if (param_types == null) {
|
|
name = "infer_param_types" + suffix
|
|
run_pass(func, name, function() {
|
|
param_types = infer_param_types(func)
|
|
return param_types
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
}
|
|
|
|
name = "infer_slot_write_types" + suffix
|
|
run_pass(func, name, function() {
|
|
write_types = infer_slot_write_types(func, param_types)
|
|
return write_types
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
|
|
name = "eliminate_type_checks" + suffix
|
|
run_pass(func, name, function() {
|
|
slot_types = eliminate_type_checks(func, param_types, write_types, log)
|
|
return slot_types
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
|
|
if (log != null && log.type_deltas != null && slot_types != null) {
|
|
log.type_deltas[] = {
|
|
fn: func.name,
|
|
cycle: suffix == "" ? 1 : 2,
|
|
param_types: param_types,
|
|
slot_types: slot_types
|
|
}
|
|
}
|
|
|
|
name = "simplify_algebra" + suffix
|
|
run_pass(func, name, function() {
|
|
return simplify_algebra(func, log)
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
|
|
name = "simplify_booleans" + suffix
|
|
run_pass(func, name, function() {
|
|
return simplify_booleans(func, log)
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
|
|
name = "eliminate_moves" + suffix
|
|
run_pass(func, name, function() {
|
|
return eliminate_moves(func, log)
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
|
|
name = "eliminate_unreachable" + suffix
|
|
run_pass(func, name, function() {
|
|
return eliminate_unreachable(func)
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
|
|
name = "eliminate_dead_jumps" + suffix
|
|
run_pass(func, name, function() {
|
|
return eliminate_dead_jumps(func, log)
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
|
|
name = "eliminate_unreachable_cfg" + suffix
|
|
run_pass(func, name, function() {
|
|
return eliminate_unreachable_cfg(func)
|
|
})
|
|
if (verify_fn) verify_fn(func, "after " + name)
|
|
return null
|
|
}
|
|
|
|
if (func.instructions == null || length(func.instructions) == 0) {
|
|
return null
|
|
}
|
|
|
|
run_cycle("")
|
|
run_cycle("_2")
|
|
func._write_types = write_types
|
|
if (ir._warn) {
|
|
diagnose_function(func, {param_types: param_types, write_types: write_types}, ir)
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Pre-pass: mark slots written by child closures via 'put' instructions.
|
|
// Without this, infer_slot_write_types would assume those slots keep their
|
|
// initial type (e.g. T_NULL from 'var x = null'), causing the type-check
|
|
// eliminator to mis-optimize comparisons on closure-written variables.
|
|
mark_closure_writes(ir)
|
|
|
|
if (ir._warn) {
|
|
ir._diagnostics = []
|
|
}
|
|
|
|
// Phase 1: Optimize all functions (bottom-up, existing behavior)
|
|
if (ir.main != null) {
|
|
optimize_function(ir.main, log)
|
|
insert_stone_text(ir.main, log)
|
|
}
|
|
|
|
var fi = 0
|
|
if (ir.functions != null) {
|
|
fi = 0
|
|
while (fi < length(ir.functions)) {
|
|
resolve_closure_types(ir.functions[fi], fi, ir)
|
|
optimize_function(ir.functions[fi], log)
|
|
insert_stone_text(ir.functions[fi], log)
|
|
fi = fi + 1
|
|
}
|
|
}
|
|
|
|
// Phase 2: Inline pass
|
|
if (ir._no_inline) {
|
|
return ir
|
|
}
|
|
var changed_main = false
|
|
var changed_fns = null
|
|
if (ir.main != null) {
|
|
changed_main = inline_calls(ir.main, ir, log)
|
|
}
|
|
if (ir.functions != null) {
|
|
changed_fns = array(length(ir.functions), false)
|
|
fi = 0
|
|
while (fi < length(ir.functions)) {
|
|
changed_fns[fi] = inline_calls(ir.functions[fi], ir, log)
|
|
fi = fi + 1
|
|
}
|
|
}
|
|
|
|
// Phase 3: Re-optimize inlined functions
|
|
if (changed_main) {
|
|
optimize_function(ir.main, log)
|
|
insert_stone_text(ir.main, log)
|
|
}
|
|
if (ir.functions != null) {
|
|
fi = 0
|
|
while (fi < length(ir.functions)) {
|
|
if (changed_fns != null && changed_fns[fi]) {
|
|
optimize_function(ir.functions[fi], log)
|
|
insert_stone_text(ir.functions[fi], log)
|
|
}
|
|
fi = fi + 1
|
|
}
|
|
}
|
|
|
|
// Phase 4: Cascade — second inline round (callbacks inside inlined bodies)
|
|
if (changed_main) {
|
|
changed_main = inline_calls(ir.main, ir, log)
|
|
if (changed_main) {
|
|
optimize_function(ir.main, log)
|
|
insert_stone_text(ir.main, log)
|
|
}
|
|
}
|
|
if (ir.functions != null) {
|
|
fi = 0
|
|
while (fi < length(ir.functions)) {
|
|
if (changed_fns != null && changed_fns[fi]) {
|
|
changed_fns[fi] = inline_calls(ir.functions[fi], ir, log)
|
|
if (changed_fns[fi]) {
|
|
optimize_function(ir.functions[fi], log)
|
|
insert_stone_text(ir.functions[fi], log)
|
|
}
|
|
}
|
|
fi = fi + 1
|
|
}
|
|
}
|
|
|
|
// Phase 5: Compress slots across all functions (must run after per-function passes)
|
|
compress_slots(ir)
|
|
|
|
// Expose DEF/USE functions via log if requested
|
|
if (log != null) {
|
|
if (log.request_def_use) {
|
|
log.get_slot_defs = get_slot_defs
|
|
log.get_slot_uses = get_slot_uses
|
|
}
|
|
}
|
|
|
|
return ir
|
|
}
|
|
|
|
return streamline
|