comprehensive testing for regression analysis

This commit is contained in:
2026-02-12 18:15:03 -06:00
parent 4aedb8b0c5
commit 89e34ba71d
13 changed files with 25599 additions and 19153 deletions

264
diff.ce Normal file
View File

@@ -0,0 +1,264 @@
// diff.ce — differential testing: run tests optimized vs unoptimized, compare results
//
// Usage:
// cell diff - diff all test files in current package
// cell diff suite - diff a specific test file (tests/suite.cm)
// cell diff tests/foo - diff a specific test file by path
var shop = use('internal/shop')
var pkg = use('package')
var fd = use('fd')
var time = use('time')
var _args = args == null ? [] : args
var analyze = use('os').analyze
var run_ast_fn = use('os').run_ast_fn
var run_ast_noopt_fn = use('os').run_ast_noopt_fn
if (!run_ast_noopt_fn) {
log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)")
$stop()
return
}
// Parse arguments: diff [test_path]
var target_test = null
if (length(_args) > 0) {
target_test = _args[0]
}
function is_valid_package(dir) {
var _dir = dir == null ? '.' : dir
return fd.is_file(_dir + '/cell.toml')
}
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
$stop()
return
}
// Collect test files
function collect_tests(specific_test) {
var files = pkg.list_files(null)
var test_files = []
var i = 0
var f = null
var test_name = null
var match_name = null
var match_base = null
for (i = 0; i < length(files); i++) {
f = files[i]
if (starts_with(f, "tests/") && ends_with(f, ".cm")) {
if (specific_test) {
test_name = text(f, 0, -3)
match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (test_name != match_base) continue
}
push(test_files, f)
}
}
return test_files
}
// Deep comparison of two values
function values_equal(a, b) {
var i = 0
var ka = null
var kb = null
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
i = 0
while (i < length(a)) {
if (!values_equal(a[i], b[i])) return false
i = i + 1
}
return true
}
if (is_object(a) && is_object(b)) {
ka = array(a)
kb = array(b)
if (length(ka) != length(kb)) return false
i = 0
while (i < length(ka)) {
if (!values_equal(a[ka[i]], b[ka[i]])) return false
i = i + 1
}
return true
}
return false
}
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
if (is_array(val)) return `[array length=${text(length(val))}]`
if (is_object(val)) return `{record keys=${text(length(array(val)))}}`
return "<unknown>"
}
// Run a single test file through both paths
function diff_test_file(file_path) {
var mod_path = text(file_path, 0, -3)
var src_path = fd.realpath('.') + '/' + file_path
var src = null
var ast = null
var mod_opt = null
var mod_noopt = null
var results = {file: file_path, tests: [], passed: 0, failed: 0, errors: []}
var use_pkg = fd.realpath('.')
var opt_error = null
var noopt_error = null
var keys = null
var i = 0
var k = null
var opt_result = null
var noopt_result = null
var opt_err = null
var noopt_err = null
var _run_one_opt = null
var _run_one_noopt = null
// Build env for module loading
var make_env = function() {
return {
use: function(path) {
return shop.use(path, use_pkg)
}
}
}
// Read and parse
var _read = function() {
src = text(fd.slurp(src_path))
ast = analyze(src, src_path)
} disruption {
push(results.errors, `failed to parse ${file_path}`)
return results
}
_read()
if (length(results.errors) > 0) return results
// Run optimized
var _run_opt = function() {
mod_opt = run_ast_fn(mod_path, ast, make_env())
} disruption {
opt_error = "disrupted"
}
_run_opt()
// Run unoptimized
var _run_noopt = function() {
mod_noopt = run_ast_noopt_fn(mod_path, ast, make_env())
} disruption {
noopt_error = "disrupted"
}
_run_noopt()
// Compare module-level behavior
if (opt_error != noopt_error) {
push(results.errors, `module load mismatch: opt=${opt_error != null ? opt_error : "ok"} noopt=${noopt_error != null ? noopt_error : "ok"}`)
results.failed = results.failed + 1
return results
}
if (opt_error != null) {
// Both disrupted during load — that's consistent
results.passed = results.passed + 1
push(results.tests, {name: "<module>", status: "passed"})
return results
}
// If module returns a record of functions, test each one
if (is_object(mod_opt) && is_object(mod_noopt)) {
keys = array(mod_opt)
while (i < length(keys)) {
k = keys[i]
if (is_function(mod_opt[k]) && is_function(mod_noopt[k])) {
opt_result = null
noopt_result = null
opt_err = null
noopt_err = null
_run_one_opt = function() {
opt_result = mod_opt[k]()
} disruption {
opt_err = "disrupted"
}
_run_one_opt()
_run_one_noopt = function() {
noopt_result = mod_noopt[k]()
} disruption {
noopt_err = "disrupted"
}
_run_one_noopt()
if (opt_err != noopt_err) {
push(results.tests, {name: k, status: "failed"})
push(results.errors, `${k}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
results.failed = results.failed + 1
} else if (!values_equal(opt_result, noopt_result)) {
push(results.tests, {name: k, status: "failed"})
push(results.errors, `${k}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
results.failed = results.failed + 1
} else {
push(results.tests, {name: k, status: "passed"})
results.passed = results.passed + 1
}
}
i = i + 1
}
} else {
// Compare direct return values
if (!values_equal(mod_opt, mod_noopt)) {
push(results.tests, {name: "<return>", status: "failed"})
push(results.errors, `return value mismatch: opt=${describe(mod_opt)} noopt=${describe(mod_noopt)}`)
results.failed = results.failed + 1
} else {
push(results.tests, {name: "<return>", status: "passed"})
results.passed = results.passed + 1
}
}
return results
}
// Main
var test_files = collect_tests(target_test)
log.console(`Differential testing: ${text(length(test_files))} file(s)`)
var total_passed = 0
var total_failed = 0
var i = 0
var result = null
var j = 0
while (i < length(test_files)) {
result = diff_test_file(test_files[i])
log.console(` ${result.file}: ${text(result.passed)} passed, ${text(result.failed)} failed`)
j = 0
while (j < length(result.errors)) {
log.console(` MISMATCH: ${result.errors[j]}`)
j = j + 1
}
total_passed = total_passed + result.passed
total_failed = total_failed + result.failed
i = i + 1
}
log.console(`----------------------------------------`)
log.console(`Diff: ${text(total_passed)} passed, ${text(total_failed)} failed, ${text(total_passed + total_failed)} total`)
if (total_failed > 0) {
log.console(`DIFFERENTIAL FAILURES DETECTED`)
}
$stop()

278
fuzz.ce Normal file
View File

@@ -0,0 +1,278 @@
// fuzz.ce — fuzzer driver: generates random programs, runs differential, saves failures
//
// Usage:
// cell fuzz - run 100 iterations with a random seed
// cell fuzz 500 - run 500 iterations with a random seed
// cell fuzz --seed 42 - run 100 iterations starting at seed 42
// cell fuzz 500 --seed 42 - run 500 iterations starting at seed 42
//
// Each iteration generates a random self-checking program, compiles it,
// runs it through both optimized and unoptimized paths, and compares results.
// Failures are saved to tests/fuzz_failures/ for reproduction.
var fd = use('fd')
var time = use('time')
var json = use('json')
var os_ref = use('os')
var analyze = os_ref.analyze
var run_ast_fn = os_ref.run_ast_fn
var run_ast_noopt_fn = os_ref.run_ast_noopt_fn
var fuzzgen = use('fuzzgen')
var _args = args == null ? [] : args
// Parse arguments: fuzz [iterations] [--seed N]
var iterations = 100
var start_seed = null
var i = 0
var n = null
var run_err = null
var _run_one = null
while (i < length(_args)) {
if (_args[i] == '--seed' && i + 1 < length(_args)) {
start_seed = number(_args[i + 1])
i = i + 2
} else {
n = number(_args[i])
if (n != null && n > 0) iterations = n
i = i + 1
}
}
if (start_seed == null) {
start_seed = floor(time.number() * 1000) % 1000000
}
if (!run_ast_noopt_fn) {
log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)")
$stop()
return
}
// Ensure failures directory exists
var failures_dir = "tests/fuzz_failures"
function ensure_dir(path) {
if (fd.is_dir(path)) return
var parts = array(path, '/')
var current = ''
var j = 0
while (j < length(parts)) {
if (parts[j] != '') {
current = current + parts[j] + '/'
if (!fd.is_dir(current)) {
fd.mkdir(current)
}
}
j = j + 1
}
}
// Deep comparison
function values_equal(a, b) {
var j = 0
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
j = 0
while (j < length(a)) {
if (!values_equal(a[j], b[j])) return false
j = j + 1
}
return true
}
return false
}
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
return "<other>"
}
// Run a single fuzz iteration
function run_fuzz(seed_val) {
var src = fuzzgen.generate(seed_val)
var name = "fuzz_" + text(seed_val)
var ast = null
var mod_opt = null
var mod_noopt = null
var opt_err = null
var noopt_err = null
var errors = []
var keys = null
var k = 0
var key = null
var ret = null
var _run = null
var run_err = null
var keys2 = null
var k2 = 0
var key2 = null
var opt_result = null
var noopt_result = null
var opt_fn_err = null
var noopt_fn_err = null
var _run_opt = null
var _run_noopt = null
// Parse
var _parse = function() {
ast = analyze(src, name + ".cm")
} disruption {
push(errors, "parse error")
}
_parse()
if (length(errors) > 0) return {seed: seed_val, errors: errors, src: src}
// Run optimized
var _opt = function() {
mod_opt = run_ast_fn(name, ast, {use: function(p) { return use(p) }})
} disruption {
opt_err = "disrupted"
}
_opt()
// Run unoptimized
var _noopt = function() {
mod_noopt = run_ast_noopt_fn(name + "_noopt", ast, {use: function(p) { return use(p) }})
} disruption {
noopt_err = "disrupted"
}
_noopt()
// Check module-level behavior
if (opt_err != noopt_err) {
push(errors, `module load: opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
return {seed: seed_val, errors: errors, src: src}
}
if (opt_err != null) {
// Both failed to load — consistent
return {seed: seed_val, errors: errors, src: src}
}
// Run self-checks (optimized module)
if (is_object(mod_opt)) {
keys = array(mod_opt)
k = 0
while (k < length(keys)) {
key = keys[k]
if (is_function(mod_opt[key])) {
ret = null
run_err = null
_run = function() {
ret = mod_opt[key]()
} disruption {
run_err = "disrupted"
}
_run()
if (is_text(ret)) {
push(errors, `self-check ${key}: ${ret}`)
}
if (run_err != null) {
push(errors, `self-check ${key}: unexpected disruption`)
}
}
k = k + 1
}
}
// Differential check on each function
if (is_object(mod_opt) && is_object(mod_noopt)) {
keys2 = array(mod_opt)
k2 = 0
while (k2 < length(keys2)) {
key2 = keys2[k2]
if (is_function(mod_opt[key2]) && is_function(mod_noopt[key2])) {
opt_result = null
noopt_result = null
opt_fn_err = null
noopt_fn_err = null
_run_opt = function() {
opt_result = mod_opt[key2]()
} disruption {
opt_fn_err = "disrupted"
}
_run_opt()
_run_noopt = function() {
noopt_result = mod_noopt[key2]()
} disruption {
noopt_fn_err = "disrupted"
}
_run_noopt()
if (opt_fn_err != noopt_fn_err) {
push(errors, `diff ${key2}: opt=${opt_fn_err != null ? opt_fn_err : "ok"} noopt=${noopt_fn_err != null ? noopt_fn_err : "ok"}`)
} else if (!values_equal(opt_result, noopt_result)) {
push(errors, `diff ${key2}: opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
}
}
k2 = k2 + 1
}
}
return {seed: seed_val, errors: errors, src: src}
}
// Main loop
log.console(`Fuzzing: ${text(iterations)} iterations, starting seed=${text(start_seed)}`)
var total_pass = 0
var total_fail = 0
var result = null
var j = 0
var current_seed = 0
var fail_path = null
i = 0
while (i < iterations) {
current_seed = start_seed + i
run_err = null
_run_one = function() {
result = run_fuzz(current_seed)
} disruption {
run_err = "generator crashed"
}
_run_one()
if (run_err != null) {
result = {seed: current_seed, errors: [run_err], src: "// generator crashed"}
}
if (length(result.errors) > 0) {
total_fail = total_fail + 1
log.console(` FAIL seed=${text(current_seed)}: ${result.errors[0]}`)
// Save failure source for reproduction
ensure_dir(failures_dir)
fail_path = failures_dir + "/seed_" + text(current_seed) + ".cm"
fd.slurpwrite(fail_path, stone(blob(result.src)))
log.console(` saved to ${fail_path}`)
} else {
total_pass = total_pass + 1
}
// Progress report every 100 iterations
if ((i + 1) % 100 == 0) {
log.console(` progress: ${text(i + 1)}/${text(iterations)} (${text(total_pass)} passed, ${text(total_fail)} failed)`)
}
i = i + 1
}
log.console(`----------------------------------------`)
log.console(`Fuzz: ${text(total_pass)} passed, ${text(total_fail)} failed, ${text(iterations)} total`)
if (total_fail > 0) {
log.console(`Failures saved to ${failures_dir}/`)
}
$stop()

348
fuzzgen.cm Normal file
View File

@@ -0,0 +1,348 @@
// fuzzgen.cm — generates self-checking .cm programs for fuzz testing
// Each generated program returns a record of test functions that
// validate their own expected results.
// Newline constant — backtick strings don't interpret \n as escape
var NL = "\n"
// Simple seeded PRNG (xorshift32)
var _seed = 1
function seed(s) {
_seed = s != 0 ? s : 1
}
function rand() {
_seed = _seed ^ (_seed << 13)
_seed = _seed ^ (_seed >> 17)
_seed = _seed ^ (_seed << 5)
if (_seed < 0) _seed = -_seed
return _seed
}
function rand_int(lo, hi) {
return lo + (rand() % (hi - lo + 1))
}
function rand_float() {
return rand_int(-10000, 10000) / 100
}
function rand_bool() {
return rand() % 2 == 0
}
function pick(arr) {
return arr[rand() % length(arr)]
}
// Expression generators — each returns {src: "code", val: expected_value}
// depth is decremented to prevent infinite recursion
function gen_int_literal() {
var v = rand_int(-10000, 10000)
return {src: text(v), val: v}
}
function gen_float_literal() {
var v = rand_float()
return {src: text(v), val: v}
}
function gen_bool_literal() {
var v = rand_bool()
var s = "false"
if (v) s = "true"
return {src: s, val: v}
}
function gen_text_literal() {
var words = ["alpha", "beta", "gamma", "delta", "epsilon"]
var w = pick(words)
return {src: `"${w}"`, val: w}
}
function gen_null_literal() {
return {src: "null", val: null}
}
function gen_int_expr(depth) {
var a = null
var b = null
var op = null
var result = null
if (depth <= 0) return gen_int_literal()
a = gen_int_expr(depth - 1)
b = gen_int_expr(depth - 1)
// Avoid division by zero
if (b.val == 0) b = {src: "1", val: 1}
op = pick(["+", "-", "*"])
if (op == "+") {
result = a.val + b.val
} else if (op == "-") {
result = a.val - b.val
} else {
result = a.val * b.val
}
// Guard against overflow beyond safe integer range
if (result > 9007199254740991 || result < -9007199254740991) {
return gen_int_literal()
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
function gen_float_expr(depth) {
var a = null
var b = null
var op = null
var result = null
if (depth <= 0) return gen_float_literal()
a = gen_float_expr(depth - 1)
b = gen_float_expr(depth - 1)
if (b.val == 0) b = {src: "1.0", val: 1.0}
op = pick(["+", "-", "*"])
if (op == "+") {
result = a.val + b.val
} else if (op == "-") {
result = a.val - b.val
} else {
result = a.val * b.val
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
function gen_text_expr(depth) {
var a = null
var b = null
if (depth <= 0) return gen_text_literal()
a = gen_text_literal()
b = gen_text_literal()
return {src: `(${a.src} + ${b.src})`, val: a.val + b.val}
}
function gen_comparison_expr(depth) {
var a = null
var b = null
var op = null
var result = null
a = gen_int_expr(depth > 0 ? depth - 1 : 0)
b = gen_int_expr(depth > 0 ? depth - 1 : 0)
op = pick(["==", "!=", "<", ">", "<=", ">="])
if (op == "==") {
result = a.val == b.val
} else if (op == "!=") {
result = a.val != b.val
} else if (op == "<") {
result = a.val < b.val
} else if (op == ">") {
result = a.val > b.val
} else if (op == "<=") {
result = a.val <= b.val
} else {
result = a.val >= b.val
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
// Generate an if-else expression test
function gen_if_else_test() {
var cond = gen_comparison_expr(1)
var then_val = gen_int_literal()
var else_val = gen_int_literal()
var expected = cond.val ? then_val.val : else_val.val
var body = "var result = null" + NL
body = body + " if (" + cond.src + ") {" + NL
body = body + " result = " + then_val.src + NL
body = body + " } else {" + NL
body = body + " result = " + else_val.src + NL
body = body + " }" + NL
body = body + " if (result != " + text(expected) + ") return \"if_else: expected " + text(expected) + " got \" + text(result)"
return body
}
// Generate a loop accumulator test
function gen_loop_test() {
var count = rand_int(1, 50)
var step = rand_int(1, 10)
var expected = 0
var i = 0
while (i < count) {
expected = expected + step
i = i + 1
}
var body = "var acc = 0" + NL
body = body + " var i = 0" + NL
body = body + " while (i < " + text(count) + ") {" + NL
body = body + " acc = acc + " + text(step) + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (acc != " + text(expected) + ") return \"loop: expected " + text(expected) + " got \" + text(acc)"
return body
}
// Generate a closure test
function gen_closure_test() {
var init_val = rand_int(1, 100)
var inc = rand_int(1, 10)
var calls = rand_int(1, 10)
var expected = init_val + (inc * calls)
var body = "var counter = " + text(init_val) + NL
body = body + " var inc = function() { counter = counter + " + text(inc) + " }" + NL
body = body + " var i = 0" + NL
body = body + " while (i < " + text(calls) + ") {" + NL
body = body + " inc()" + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (counter != " + text(expected) + ") return \"closure: expected " + text(expected) + " got \" + text(counter)"
return body
}
// Generate a record property test
function gen_record_test() {
var a = gen_int_literal()
var b = gen_int_literal()
var sum = a.val + b.val
var body = "var r = {a: " + a.src + ", b: " + b.src + "}" + NL
body = body + " var result = r.a + r.b" + NL
body = body + " if (result != " + text(sum) + ") return \"record: expected " + text(sum) + " got \" + text(result)"
return body
}
// Generate an array test
function gen_array_test() {
var n = rand_int(2, 10)
var vals = []
var i = 0
var sum = 0
var v = 0
while (i < n) {
v = rand_int(-100, 100)
push(vals, v)
sum = sum + v
i = i + 1
}
var val_strs = []
i = 0
while (i < n) {
push(val_strs, text(vals[i]))
i = i + 1
}
var body = "var a = [" + text(val_strs, ", ") + "]" + NL
body = body + " var _sum = 0" + NL
body = body + " var i = 0" + NL
body = body + " while (i < length(a)) {" + NL
body = body + " _sum = _sum + a[i]" + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (_sum != " + text(sum) + ") return \"array_sum: expected " + text(sum) + " got \" + text(_sum)"
return body
}
// Generate a nested function / higher-order test
function gen_higher_order_test() {
var mul = rand_int(2, 10)
var input = rand_int(1, 100)
var expected = input * mul
var body = "var make_mul = function(m) {" + NL
body = body + " return function(x) { return x * m }" + NL
body = body + " }" + NL
body = body + " var fn = make_mul(" + text(mul) + ")" + NL
body = body + " var result = fn(" + text(input) + ")" + NL
body = body + " if (result != " + text(expected) + ") return \"higher_order: expected " + text(expected) + " got \" + text(result)"
return body
}
// Generate a disruption handling test
function gen_disrupt_test() {
var body = "var caught = false" + NL
body = body + " var _fn = function() { disrupt } disruption { caught = true }" + NL
body = body + " _fn()" + NL
body = body + " if (!caught) return \"disrupt: expected to catch disruption\""
return body
}
// Generate a text operation test
function gen_text_op_test() {
var words = ["hello", "world", "foo", "bar", "baz"]
var w1 = pick(words)
var w2 = pick(words)
var expected = w1 + w2
var body = "var a = \"" + w1 + "\"" + NL
body = body + " var b = \"" + w2 + "\"" + NL
body = body + " var c = a + b" + NL
body = body + " if (c != \"" + expected + "\") return \"text_op: expected " + expected + " got \" + c"
return body
}
// All generators
var generators = [
gen_if_else_test,
gen_loop_test,
gen_closure_test,
gen_record_test,
gen_array_test,
gen_higher_order_test,
gen_disrupt_test,
gen_text_op_test
]
// Generate a complete self-checking .cm program
function generate(s) {
seed(s)
var num_tests = rand_int(5, 15)
var src = "// Auto-generated fuzz test (seed=" + text(s) + ")\nreturn {\n"
var i = 0
var gen = null
var body = null
while (i < num_tests) {
gen = pick(generators)
body = gen()
if (i > 0) src = src + ",\n"
src = src + " fuzz_" + text(i) + ": function() {\n"
src = src + " " + body + "\n"
src = src + " }"
i = i + 1
}
src = src + "\n}\n"
return src
}
return {
generate: generate,
seed: seed
}

View File

@@ -171,13 +171,34 @@ function load_module(name, env) {
streamline_mod = load_module("streamline", boot_env)
use_cache['streamline'] = streamline_mod
// Lazy-loaded verify_ir module (loaded on first use via use_fn)
var _verify_ir_mod = null
// Run AST through mcode pipeline → register VM
function run_ast(name, ast, env) {
var compiled = mcode_mod(ast)
if (os._verify_ir) {
if (_verify_ir_mod == null) {
_verify_ir_mod = use_fn('verify_ir')
}
compiled._verify = true
compiled._verify_mod = _verify_ir_mod
}
var optimized = streamline_mod(compiled)
// Clean up verify properties before JSON encoding
if (optimized._verify) {
delete optimized._verify
delete optimized._verify_mod
}
return mach_eval_mcode(name, json.encode(optimized), env)
}
// Run AST through mcode pipeline WITHOUT optimization → register VM
function run_ast_noopt(name, ast, env) {
var compiled = mcode_mod(ast)
return mach_eval_mcode(name, json.encode(compiled), env)
}
// use() with ƿit pipeline for .cm modules
function use_fn(path) {
var file_path = null
@@ -277,13 +298,13 @@ if (args != null) {
os: os, actorsym: actorsym,
init: {program: program, arg: user_args},
core_path: core_path, shop_path: shop_path, json: json,
analyze: analyze, run_ast_fn: run_ast
analyze: analyze, run_ast_fn: run_ast, run_ast_noopt_fn: run_ast_noopt
})
} else {
// Actor spawn mode — load engine.cm with full actor env
load_engine({
os: os, actorsym: actorsym, init: init,
core_path: core_path, shop_path: shop_path, json: json, nota: nota, wota: wota,
analyze: analyze, run_ast_fn: run_ast
analyze: analyze, run_ast_fn: run_ast, run_ast_noopt_fn: run_ast_noopt
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Hidden vars (os, actorsym, init, core_path, shop_path, analyze, run_ast_fn, json) come from env
// Hidden vars (os, actorsym, init, core_path, shop_path, analyze, run_ast_fn, run_ast_noopt_fn, json) come from env
// In actor spawn mode, also: nota, wota
var ACTORDATA = actorsym
var SYSYM = '__SYSTEM__'
@@ -214,6 +214,7 @@ os.global_shop_path = shop_path
os.$_ = $_
os.analyze = analyze
os.run_ast_fn = run_ast_fn
os.run_ast_noopt_fn = run_ast_noopt_fn
os.json = json
use_cache['core/json'] = json

File diff suppressed because it is too large Load Diff

200
ir_report.ce Normal file
View File

@@ -0,0 +1,200 @@
// ir_report.ce — optimizer flight recorder CLI
//
// Usage: ./cell --core . ir_report.ce [options] <file.cm|file.ce>
//
// Options:
// --summary Per-pass JSON summaries (default)
// --events Include rewrite events
// --types Include type deltas
// --ir-before=PASS Print canonical IR before PASS
// --ir-after=PASS Print canonical IR after PASS
// --ir-all Print canonical IR before/after every pass
// --full Everything (summary + events + types + ir-all)
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
var ir_stats = use("ir_stats")
// --- Parse arguments ---
var filename = null
var opt_events = false
var opt_types = false
var opt_ir_before = null
var opt_ir_after = null
var opt_ir_all = false
var i = 0
var arg = null
var p = null
var e = null
var td = null
while (i < length(args)) {
arg = args[i]
if (arg == "--events") {
opt_events = true
} else if (arg == "--types") {
opt_types = true
} else if (arg == "--ir-all") {
opt_ir_all = true
} else if (arg == "--full") {
opt_events = true
opt_types = true
opt_ir_all = true
} else if (arg == "--summary") {
// default, no-op
} else if (starts_with(arg, "--ir-before=")) {
opt_ir_before = text(arg, 12)
} else if (starts_with(arg, "--ir-after=")) {
opt_ir_after = text(arg, 11)
} else if (!starts_with(arg, "--")) {
filename = arg
} else {
print(`unknown option: ${arg}\n`)
print("usage: cell --core . ir_report.ce [options] <file>\n")
$stop()
}
i = i + 1
}
if (filename == null) {
print("usage: cell --core . ir_report.ce [options] <file.cm|file.ce>\n")
print(" --summary per-pass JSON summaries (default)\n")
print(" --events include rewrite events\n")
print(" --types include type deltas\n")
print(" --ir-before=PASS print canonical IR before PASS\n")
print(" --ir-after=PASS print canonical IR after PASS\n")
print(" --ir-all print canonical IR before/after every pass\n")
print(" --full everything\n")
$stop()
}
// --- Compile ---
var src = text(fd.slurp(filename))
var tok = tokenize(src, filename)
var ast = parse(tok.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
// --- Determine which passes need IR snapshots ---
var need_snapshots = opt_ir_all || opt_ir_before != null || opt_ir_after != null
// Deep copy for before snapshot if we need IR printing
var before_ir = null
if (need_snapshots) {
before_ir = json.decode(json.encode(compiled))
}
// --- Set up log ---
var log = {
passes: [],
events: null,
type_deltas: null
}
if (opt_events) {
log.events = []
}
if (opt_types) {
log.type_deltas = []
}
// --- Run optimizer ---
var optimized = streamline(compiled, log)
// --- Output ---
var emit = function(obj) {
print(json.encode(obj))
print("\n")
}
// Pass summaries (always)
i = 0
while (i < length(log.passes)) {
p = log.passes[i]
p.type = "pass"
emit(p)
i = i + 1
}
// Rewrite events
if (opt_events && log.events != null) {
i = 0
while (i < length(log.events)) {
e = log.events[i]
e.type = "event"
emit(e)
i = i + 1
}
}
// Type deltas
if (opt_types && log.type_deltas != null) {
i = 0
while (i < length(log.type_deltas)) {
td = log.type_deltas[i]
td.type = "types"
emit(td)
i = i + 1
}
}
// --- Canonical IR printing ---
var print_ir = function(ir_obj, when_label, pass_name) {
var fname = null
var fi = 0
var func = null
if (ir_obj.main != null) {
fname = ir_obj.name != null ? ir_obj.name : "<main>"
emit({
type: "ir",
when: when_label,
pass: pass_name,
fn: fname,
text: ir_stats.canonical_ir(ir_obj.main, fname, {show_nops: true})
})
}
if (ir_obj.functions != null) {
fi = 0
while (fi < length(ir_obj.functions)) {
func = ir_obj.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
emit({
type: "ir",
when: when_label,
pass: pass_name,
fn: fname,
text: ir_stats.canonical_ir(func, fname, {show_nops: true})
})
fi = fi + 1
}
}
return null
}
if (need_snapshots) {
if (opt_ir_all) {
print_ir(before_ir, "before", "all")
print_ir(optimized, "after", "all")
} else {
if (opt_ir_before != null) {
print_ir(before_ir, "before", opt_ir_before)
}
if (opt_ir_after != null) {
print_ir(optimized, "after", opt_ir_after)
}
}
}
$stop()

357
ir_stats.cm Normal file
View File

@@ -0,0 +1,357 @@
// ir_stats.cm — IR statistics, fingerprinting, and canonical printing
//
// Usage: var ir_stats = use("ir_stats")
// ir_stats.detailed_stats(func)
// ir_stats.ir_fingerprint(func)
// ir_stats.canonical_ir(func, name, opts)
// ir_stats.type_snapshot(slot_types)
// ir_stats.type_delta(before_types, after_types)
var json = use("json")
// --- Category maps ---
var load_ops = {
load_field: true, load_index: true, load_dynamic: true,
get: true
}
var store_ops = {
store_field: true, store_index: true, store_dynamic: true,
set_var: true, put: true, push: true
}
var branch_ops = {
jump: true, jump_true: true, jump_false: true, jump_not_null: true
}
var call_ops = {
invoke: true, goinvoke: true
}
var guard_ops = {
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
}
var arith_ops = {
add_int: true, sub_int: true, mul_int: true, div_int: true, mod_int: true,
add_float: true, sub_float: true, mul_float: true, div_float: true, mod_float: true,
concat: true, neg_int: true, neg_float: true,
bitnot: true, bitand: true, bitor: true, bitxor: true,
shl: true, shr: true, ushr: true
}
var move_ops = {
move: true
}
var const_ops = {
int: true, true: true, false: true, null: true
}
var nop_reasons = {
tc: "tc",
bl: "bl",
mv: "mv",
dj: "dj",
ur: "ur"
}
var category_tag = function(op) {
if (guard_ops[op] == true) { return "guard" }
if (branch_ops[op] == true) { return "branch" }
if (load_ops[op] == true) { return "load" }
if (store_ops[op] == true) { return "store" }
if (call_ops[op] == true) { return "call" }
if (arith_ops[op] == true) { return "arith" }
if (move_ops[op] == true) { return "move" }
if (const_ops[op] == true) { return "const" }
return null
}
// --- detailed_stats ---
var detailed_stats = function(func) {
var instructions = func.instructions
var stats = {
instr: 0, nop: 0,
load: 0, store: 0, branch: 0, call: 0,
guard: 0, arith: 0, move: 0, const: 0,
label: 0, other: 0
}
var i = 0
var instr = null
var op = null
var num = 0
if (instructions == null) {
return stats
}
num = length(instructions)
while (i < num) {
instr = instructions[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
stats.nop = stats.nop + 1
stats.instr = stats.instr + 1
} else {
stats.label = stats.label + 1
}
} else if (is_array(instr)) {
stats.instr = stats.instr + 1
op = instr[0]
if (op == "access" && !is_number(instr[2]) && !is_logical(instr[2])) {
stats.load = stats.load + 1
} else if (op == "access") {
stats.const = stats.const + 1
} else if (load_ops[op] == true) {
stats.load = stats.load + 1
} else if (store_ops[op] == true) {
stats.store = stats.store + 1
} else if (branch_ops[op] == true) {
stats.branch = stats.branch + 1
} else if (call_ops[op] == true) {
stats.call = stats.call + 1
} else if (guard_ops[op] == true) {
stats.guard = stats.guard + 1
} else if (arith_ops[op] == true) {
stats.arith = stats.arith + 1
} else if (move_ops[op] == true) {
stats.move = stats.move + 1
} else if (const_ops[op] == true) {
stats.const = stats.const + 1
} else {
stats.other = stats.other + 1
}
}
i = i + 1
}
return stats
}
// --- ir_fingerprint ---
// djb2 hash computed over the JSON-encoded instructions
var djb2 = function(s) {
var chars = array(s)
var hash = 5381
var i = 0
var num = length(chars)
while (i < num) {
hash = ((hash * 33) + number(chars[i])) % 4294967296
i = i + 1
}
return text(hash, 16)
}
var ir_fingerprint = function(func) {
return djb2(json.encode(func.instructions))
}
// --- canonical_ir ---
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var nop_reason = function(s) {
// extract reason from _nop_XX_NNN
var parts = array(s, "_")
// parts: ["", "nop", "XX", "NNN"]
if (length(parts) >= 3) {
return parts[2]
}
return "?"
}
var fmt_operand = function(v) {
if (is_null(v)) {
return "null"
}
if (is_number(v)) {
return text(v)
}
if (is_text(v)) {
return `"${v}"`
}
if (is_logical(v)) {
if (v) { return "true" }
return "false"
}
return text(v)
}
var canonical_ir = function(func, name, opts) {
var instructions = func.instructions
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var show_nops = false
var show_types = false
var slot_types = null
var lines = []
var i = 0
var instr = null
var op = null
var n = 0
var parts = null
var j = 0
var idx_str = null
var op_str = null
var operands = null
var suffix = null
var tag = null
var typ = null
var reason = null
var num = 0
if (opts != null) {
if (opts.show_nops == true) { show_nops = true }
if (opts.show_types == true) { show_types = true }
if (opts.slot_types != null) { slot_types = opts.slot_types }
}
lines[] = `fn ${name != null ? name : "<anon>"} (args=${text(nr_args)}, slots=${text(nr_slots)})`
if (instructions == null) {
return text(lines, "\n")
}
num = length(instructions)
while (i < num) {
instr = instructions[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
if (show_nops) {
reason = nop_reason(instr)
idx_str = pad_right(`@${text(i)}`, 6)
lines[] = ` ${idx_str}--- nop (${reason}) ---`
}
} else {
lines[] = ` ${instr}:`
}
i = i + 1
continue
}
if (!is_array(instr)) {
i = i + 1
continue
}
op = instr[0]
n = length(instr)
parts = []
j = 1
while (j < n - 2) {
if (is_number(instr[j]) && op != "int" && !(op == "access" && j == 2)) {
parts[] = `s${text(instr[j])}`
} else {
parts[] = fmt_operand(instr[j])
}
j = j + 1
}
operands = text(parts, ", ")
idx_str = pad_right(`@${text(i)}`, 6)
op_str = pad_right(op, 16)
suffix = ""
tag = category_tag(op)
if (show_types && slot_types != null) {
// show type for dest slot if known
if (is_number(instr[1])) {
typ = slot_types[text(instr[1])]
if (typ != null) {
suffix = `; -> ${typ}`
}
}
if (tag != null) {
suffix = suffix + ` [${tag}]`
}
} else if (tag != null) {
suffix = suffix + `; [${tag}]`
}
if (length(suffix) > 0) {
lines[] = ` ${idx_str}${op_str}${operands} ${suffix}`
} else {
lines[] = ` ${idx_str}${op_str}${operands}`
}
i = i + 1
}
return text(lines, "\n")
}
// --- type_snapshot ---
var type_snapshot = function(slot_types) {
if (slot_types == null) {
return {}
}
return stone(record(slot_types))
}
// --- type_delta ---
var type_delta = function(before_types, after_types) {
var result = {
added: {},
removed: {},
strengthened: {},
weakened: {}
}
var bt = before_types != null ? before_types : {}
var at = after_types != null ? after_types : {}
var keys = null
var i = 0
var k = null
var bv = null
var av = null
// check after for added/changed
keys = array(at)
i = 0
while (i < length(keys)) {
k = keys[i]
av = at[k]
bv = bt[k]
if (bv == null) {
result.added[k] = av
} else if (bv != av) {
if (bv == "unknown" || (bv == "num" && (av == "int" || av == "float"))) {
result.strengthened[k] = {from: bv, to: av}
} else if (av == "unknown" || (av == "num" && (bv == "int" || bv == "float"))) {
result.weakened[k] = {from: bv, to: av}
} else {
result.strengthened[k] = {from: bv, to: av}
}
}
i = i + 1
}
// check before for removed
keys = array(bt)
i = 0
while (i < length(keys)) {
k = keys[i]
if (at[k] == null) {
result.removed[k] = bt[k]
}
i = i + 1
}
return result
}
return {
detailed_stats: detailed_stats,
ir_fingerprint: ir_fingerprint,
canonical_ir: canonical_ir,
type_snapshot: type_snapshot,
type_delta: type_delta,
category_tag: category_tag
}

View File

@@ -1,7 +1,27 @@
// 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)) {
print(`[verify_ir] ${errs[i]}\n`)
i = i + 1
}
if (length(errs) > 0) {
print(`[verify_ir] ${text(length(errs))} errors after ${pass_name}\n`)
}
}
}
var streamline = function(ir) {
// Type constants
var T_UNKNOWN = "unknown"
var T_INT = "int"
@@ -44,6 +64,50 @@ var streamline = function(ir) {
is_record: T_RECORD
}
// --- 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) {
@@ -244,7 +308,7 @@ var streamline = function(ir) {
// 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) {
var eliminate_type_checks = function(func, param_types, log) {
var instructions = func.instructions
var nr_args = func.nr_args != null ? func.nr_args : 0
var has_params = false
@@ -263,9 +327,15 @@ var streamline = function(ir) {
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 null
return {}
}
if (log != null && log.events != null) {
events = log.events
}
num_instr = length(instructions)
@@ -319,6 +389,17 @@ var streamline = function(ir) {
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[text(src)], checked_type: checked_type}
}
}
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
@@ -330,6 +411,17 @@ var streamline = function(ir) {
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[text(dest)] = T_BOOL
i = i + 2
continue
@@ -338,6 +430,17 @@ var streamline = function(ir) {
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[text(dest)] = T_UNKNOWN
i = i + 2
continue
@@ -355,6 +458,17 @@ var streamline = function(ir) {
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[text(src)], checked_type: checked_type}
}
}
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
@@ -366,6 +480,17 @@ var streamline = function(ir) {
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[text(dest)] = T_BOOL
i = i + 2
continue
@@ -374,6 +499,17 @@ var streamline = function(ir) {
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[text(dest)] = T_BOOL
i = i + 2
continue
@@ -391,20 +527,58 @@ var streamline = function(ir) {
// Dynamic access reduction
if (op == "load_dynamic") {
old_op = op
if (slot_is(slot_types, instr[3], T_TEXT)) {
instr[0] = "load_field"
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "dynamic_to_field",
at: i, before: old_op, after: instr[0],
why: {slot: instr[3], known_type: slot_types[text(instr[3])]}
}
}
} else if (slot_is(slot_types, instr[3], T_INT)) {
instr[0] = "load_index"
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "dynamic_to_index",
at: i, before: old_op, after: instr[0],
why: {slot: instr[3], known_type: slot_types[text(instr[3])]}
}
}
}
slot_types[text(instr[1])] = T_UNKNOWN
i = i + 1
continue
}
if (op == "store_dynamic") {
old_op = op
if (slot_is(slot_types, instr[3], T_TEXT)) {
instr[0] = "store_field"
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "dynamic_to_field",
at: i, before: old_op, after: instr[0],
why: {slot: instr[3], known_type: slot_types[text(instr[3])]}
}
}
} else if (slot_is(slot_types, instr[3], T_INT)) {
instr[0] = "store_index"
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "dynamic_to_index",
at: i, before: old_op, after: instr[0],
why: {slot: instr[3], known_type: slot_types[text(instr[3])]}
}
}
}
i = i + 1
continue
@@ -414,7 +588,7 @@ var streamline = function(ir) {
i = i + 1
}
return null
return slot_types
}
// =========================================================
@@ -422,7 +596,7 @@ var streamline = function(ir) {
// Tracks known constant values. Rewrites identity ops to
// moves or constants. Folds same-slot comparisons.
// =========================================================
var simplify_algebra = function(func) {
var simplify_algebra = function(func, log) {
var instructions = func.instructions
var num_instr = 0
var slot_values = null
@@ -434,11 +608,17 @@ var streamline = function(ir) {
var v2 = null
var v3 = null
var sv = null
var events = null
var rule = null
if (instructions == null || length(instructions) == 0) {
return null
}
if (log != null && log.events != null) {
events = log.events
}
num_instr = length(instructions)
slot_values = {}
@@ -481,7 +661,16 @@ var streamline = function(ir) {
if (op == "add_int" || op == "sub_int") {
v3 = slot_values[text(instr[3])]
if (v3 == 0) {
rule = op == "add_int" ? "add_zero" : "sub_zero"
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: rule, at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 0}
}
}
i = i + 1
continue
}
@@ -489,6 +678,14 @@ var streamline = function(ir) {
v2 = slot_values[text(instr[2])]
if (v2 == 0) {
instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "add_zero", at: i,
before: instr, after: instructions[i],
why: {slot: instr[2], value: 0}
}
}
i = i + 1
continue
}
@@ -498,16 +695,40 @@ var streamline = function(ir) {
v2 = slot_values[text(instr[2])]
if (v3 == 1) {
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 1}
}
}
i = i + 1
continue
}
if (v2 == 1) {
instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[2], value: 1}
}
}
i = i + 1
continue
}
if (v3 == 0 || v2 == 0) {
instructions[i] = ["int", instr[1], 0, instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_zero", at: i,
before: instr, after: instructions[i],
why: {value: 0}
}
}
slot_values[text(instr[1])] = 0
i = i + 1
continue
@@ -516,6 +737,14 @@ var streamline = function(ir) {
v3 = slot_values[text(instr[3])]
if (v3 == 1) {
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "div_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 1}
}
}
i = i + 1
continue
}
@@ -526,7 +755,16 @@ var streamline = function(ir) {
if (op == "add_float" || op == "sub_float") {
v3 = slot_values[text(instr[3])]
if (v3 == 0) {
rule = op == "add_float" ? "add_zero" : "sub_zero"
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: rule, at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 0}
}
}
i = i + 1
continue
}
@@ -534,6 +772,14 @@ var streamline = function(ir) {
v2 = slot_values[text(instr[2])]
if (v2 == 0) {
instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "add_zero", at: i,
before: instr, after: instructions[i],
why: {slot: instr[2], value: 0}
}
}
i = i + 1
continue
}
@@ -543,11 +789,27 @@ var streamline = function(ir) {
v2 = slot_values[text(instr[2])]
if (v3 == 1) {
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 1}
}
}
i = i + 1
continue
}
if (v2 == 1) {
instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[2], value: 1}
}
}
i = i + 1
continue
}
@@ -555,6 +817,14 @@ var streamline = function(ir) {
v3 = slot_values[text(instr[3])]
if (v3 == 1) {
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "div_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 1}
}
}
i = i + 1
continue
}
@@ -567,6 +837,14 @@ var streamline = function(ir) {
op == "le_int" || op == "le_float" || op == "le_text" ||
op == "ge_int" || op == "ge_float" || op == "ge_text") {
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[text(instr[1])] = true
i = i + 1
continue
@@ -576,6 +854,14 @@ var streamline = function(ir) {
op == "lt_int" || op == "lt_float" || op == "lt_text" ||
op == "gt_int" || op == "gt_float" || op == "gt_text") {
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[text(instr[1])] = false
i = i + 1
continue
@@ -605,7 +891,7 @@ var streamline = function(ir) {
// =========================================================
// Pass: simplify_booleans — not+jump fusion, double-not
// =========================================================
var simplify_booleans = function(func) {
var simplify_booleans = function(func, log) {
var instructions = func.instructions
var num_instr = 0
var nc = 0
@@ -614,11 +900,16 @@ var streamline = function(ir) {
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) {
@@ -642,6 +933,14 @@ var streamline = function(ir) {
nc = nc + 1
instructions[i] = "_nop_bl_" + text(nc)
instructions[i + 1] = ["jump_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
}
@@ -651,6 +950,14 @@ var streamline = function(ir) {
nc = nc + 1
instructions[i] = "_nop_bl_" + text(nc)
instructions[i + 1] = ["jump_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
}
@@ -660,6 +967,14 @@ var streamline = function(ir) {
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
}
@@ -673,17 +988,22 @@ var streamline = function(ir) {
// =========================================================
// Pass: eliminate_moves — move a, a → nop
// =========================================================
var eliminate_moves = function(func) {
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
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) {
@@ -691,6 +1011,13 @@ var streamline = function(ir) {
if (is_array(instr) && instr[0] == "move" && instr[1] == instr[2]) {
nc = nc + 1
instructions[i] = "_nop_mv_" + text(nc)
if (events != null) {
events[] = {
event: "rewrite", pass: "eliminate_moves",
rule: "self_move", at: i,
before: instr, after: instructions[i]
}
}
}
i = i + 1
}
@@ -738,7 +1065,7 @@ var streamline = function(ir) {
// =========================================================
// Pass: eliminate_dead_jumps — jump to next label → nop
// =========================================================
var eliminate_dead_jumps = function(func) {
var eliminate_dead_jumps = function(func, log) {
var instructions = func.instructions
var num_instr = 0
var nc = 0
@@ -747,11 +1074,16 @@ var streamline = function(ir) {
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) {
@@ -765,6 +1097,14 @@ var streamline = function(ir) {
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
}
@@ -783,27 +1123,55 @@ var streamline = function(ir) {
// =========================================================
// Compose all passes
// =========================================================
var optimize_function = function(func) {
var optimize_function = function(func, log) {
var param_types = null
var slot_types = null
if (func.instructions == null || length(func.instructions) == 0) {
return null
}
param_types = infer_param_types(func)
eliminate_type_checks(func, param_types)
simplify_algebra(func)
simplify_booleans(func)
eliminate_moves(func)
run_pass(func, "infer_param_types", function() {
param_types = infer_param_types(func)
return param_types
})
if (verify_fn) verify_fn(func, "after infer_param_types")
run_pass(func, "eliminate_type_checks", function() {
slot_types = eliminate_type_checks(func, param_types, log)
return slot_types
})
if (verify_fn) verify_fn(func, "after eliminate_type_checks")
if (log != null && log.type_deltas != null && slot_types != null) {
log.type_deltas[] = {
fn: func.name,
param_types: param_types,
slot_types: slot_types
}
}
run_pass(func, "simplify_algebra", function() {
return simplify_algebra(func, log)
})
if (verify_fn) verify_fn(func, "after simplify_algebra")
run_pass(func, "simplify_booleans", function() {
return simplify_booleans(func, log)
})
if (verify_fn) verify_fn(func, "after simplify_booleans")
run_pass(func, "eliminate_moves", function() {
return eliminate_moves(func, log)
})
if (verify_fn) verify_fn(func, "after eliminate_moves")
// NOTE: eliminate_unreachable is disabled because disruption handler
// code is placed after return/disrupt without label boundaries.
// Re-enable once mcode.cm emits labels for handler entry points.
//eliminate_unreachable(func)
eliminate_dead_jumps(func)
run_pass(func, "eliminate_dead_jumps", function() {
return eliminate_dead_jumps(func, log)
})
if (verify_fn) verify_fn(func, "after eliminate_dead_jumps")
return null
}
// Process main function
if (ir.main != null) {
optimize_function(ir.main)
optimize_function(ir.main, log)
}
// Process all sub-functions
@@ -811,7 +1179,7 @@ var streamline = function(ir) {
if (ir.functions != null) {
fi = 0
while (fi < length(ir.functions)) {
optimize_function(ir.functions[fi])
optimize_function(ir.functions[fi], log)
fi = fi + 1
}
}

File diff suppressed because it is too large Load Diff

116
test.ce
View File

@@ -14,6 +14,13 @@ var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file
var all_pkgs = false
var gc_after_each_test = false
var verify_ir = false
var diff_mode = false
var os_ref = use('os')
var analyze = os_ref.analyze
var run_ast_fn = os_ref.run_ast_fn
var run_ast_noopt_fn = os_ref.run_ast_noopt_fn
// Actor test support
def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests
@@ -46,6 +53,11 @@ function get_current_package_name() {
// cell test package <name> - run all tests for named package
// cell test package <name> <test> - run specific test in named package
// cell test package all - run all tests from all packages
//
// Flags:
// -g - run GC after each test
// --verify - enable IR verification (validates mcode IR after each optimizer pass)
// --diff - enable differential testing (run each test optimized and unoptimized, compare results)
function parse_args() {
var cleaned_args = []
@@ -57,6 +69,10 @@ function parse_args() {
for (i = 0; i < length(_args); i++) {
if (_args[i] == '-g') {
gc_after_each_test = true
} else if (_args[i] == '--verify') {
verify_ir = true
} else if (_args[i] == '--diff') {
diff_mode = true
} else {
push(cleaned_args, _args[i])
}
@@ -162,6 +178,77 @@ if (!parse_args()) {
return
}
// Enable IR verification if requested
if (verify_ir) {
os_ref._verify_ir = true
log.console('IR verification enabled')
}
if (diff_mode && !run_ast_noopt_fn) {
log.console('error: --diff requires run_ast_noopt_fn (rebuild bootstrap)')
$stop()
return
}
// Diff mode: deep comparison helper
function values_equal(a, b) {
var i = 0
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
i = 0
while (i < length(a)) {
if (!values_equal(a[i], b[i])) return false
i = i + 1
}
return true
}
return false
}
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
return "<other>"
}
// Diff mode: run a test function through noopt and compare
var diff_mismatches = 0
function diff_check(test_name, file_path, opt_fn, noopt_fn) {
if (!diff_mode) return
var opt_result = null
var noopt_result = null
var opt_err = null
var noopt_err = null
var _opt = function() {
opt_result = opt_fn()
} disruption {
opt_err = "disrupted"
}
_opt()
var _noopt = function() {
noopt_result = noopt_fn()
} disruption {
noopt_err = "disrupted"
}
_noopt()
if (opt_err != noopt_err) {
log.console(` DIFF ${test_name}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
diff_mismatches = diff_mismatches + 1
} else if (!values_equal(opt_result, noopt_result)) {
log.console(` DIFF ${test_name}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
diff_mismatches = diff_mismatches + 1
}
}
function ensure_dir(path) {
if (fd.is_dir(path)) return true
@@ -320,9 +407,26 @@ function run_tests(package_name, specific_test) {
_load_file = function() {
var test_mod = null
var test_mod_noopt = null
var use_pkg = package_name ? package_name : fd.realpath('.')
var _load_noopt = null
test_mod = shop.use(mod_path, use_pkg)
// Load noopt version for diff mode
if (diff_mode) {
_load_noopt = function() {
var src_path = prefix + '/' + f
var src = text(fd.slurp(src_path))
var ast = analyze(src, src_path)
test_mod_noopt = run_ast_noopt_fn(mod_path + '_noopt', ast, {
use: function(path) { return shop.use(path, use_pkg) }
})
} disruption {
log.console(` DIFF: failed to load noopt module for ${f}`)
}
_load_noopt()
}
var tests = []
var j = 0
var t = null
@@ -406,6 +510,12 @@ function run_tests(package_name, specific_test) {
}
}
_run_one()
// Differential check: compare opt vs noopt
if (diff_mode && test_mod_noopt && is_object(test_mod_noopt) && is_function(test_mod_noopt[t.name])) {
diff_check(t.name, f, t.fn, test_mod_noopt[t.name])
}
end_time = time.number()
test_entry.duration_ns = round((end_time - start_time) * 1000000000)
@@ -635,6 +745,9 @@ function finalize_results() {
log.console(`----------------------------------------`)
log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`)
if (diff_mode) {
log.console(`Diff mismatches: ${text(diff_mismatches)}`)
}
generate_reports(totals)
$stop()
@@ -652,6 +765,9 @@ if (length(all_actor_tests) == 0) {
log.console(`----------------------------------------`)
log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`)
if (diff_mode) {
log.console(`Diff mismatches: ${text(diff_mismatches)}`)
}
} else {
$delay(check_timeouts, 1000)
}

467
verify_ir.cm Normal file
View File

@@ -0,0 +1,467 @@
// 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],
neg_int: [0, 1],
neg_float: [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],
add_int: [0, 1, 2],
sub_int: [0, 1, 2],
mul_int: [0, 1, 2],
div_int: [0, 1, 2],
mod_int: [0, 1, 2],
add_float: [0, 1, 2],
sub_float: [0, 1, 2],
mul_float: [0, 1, 2],
div_float: [0, 1, 2],
mod_float: [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],
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, neg_int: true, neg_float: 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,
add_int: true, sub_int: true, mul_int: true, div_int: true, mod_int: true,
add_float: true, sub_float: true, mul_float: true, div_float: true, mod_float: 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
}
// 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 = {
add_int: true, sub_int: true, mul_int: true, div_int: true, mod_int: true,
eq_int: true, ne_int: true, lt_int: true, gt_int: true, le_int: true, ge_int: true,
neg_int: true
}
var float_ops = {
add_float: true, sub_float: true, mul_float: true, div_float: true, mod_float: true,
eq_float: true, ne_float: true, lt_float: true, gt_float: true, le_float: true, ge_float: true,
neg_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 : "<unknown>"
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
}