comprehensive testing for regression analysis
This commit is contained in:
264
diff.ce
Normal file
264
diff.ce
Normal 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
278
fuzz.ce
Normal 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
348
fuzzgen.cm
Normal 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
|
||||
}
|
||||
@@ -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
@@ -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
200
ir_report.ce
Normal 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
357
ir_stats.cm
Normal 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
|
||||
}
|
||||
402
streamline.cm
402
streamline.cm
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
32593
streamline.cm.mcode
32593
streamline.cm.mcode
File diff suppressed because it is too large
Load Diff
116
test.ce
116
test.ce
@@ -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
467
verify_ir.cm
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user