Merge branch 'optimize_mcode'

This commit is contained in:
2026-02-21 19:42:19 -06:00
34 changed files with 53477 additions and 121900 deletions

86
bench_arith.ce Normal file
View File

@@ -0,0 +1,86 @@
// bench_arith.ce — arithmetic and number crunching benchmark
// Tests: integer add/mul, float ops, loop counter overhead, conditionals
var time = use('time')
def iterations = 2000000
// 1. Integer sum in tight loop
function bench_int_sum() {
var i = 0
var s = 0
for (i = 0; i < iterations; i++) {
s = s + i
}
return s
}
// 2. Integer multiply + mod (sieve-like)
function bench_int_mul_mod() {
var i = 0
var s = 0
for (i = 1; i < iterations; i++) {
s = s + (i * 7 % 1000)
}
return s
}
// 3. Float math — accumulate with division
function bench_float_arith() {
var i = 0
var s = 0.5
for (i = 1; i < iterations; i++) {
s = s + 1.0 / i
}
return s
}
// 4. Nested loop with branch (fizzbuzz-like counter)
function bench_branch() {
var i = 0
var fizz = 0
var buzz = 0
var fizzbuzz = 0
for (i = 1; i <= iterations; i++) {
if (i % 15 == 0) {
fizzbuzz = fizzbuzz + 1
} else if (i % 3 == 0) {
fizz = fizz + 1
} else if (i % 5 == 0) {
buzz = buzz + 1
}
}
return fizz + buzz + fizzbuzz
}
// 5. Nested loop (small inner)
function bench_nested() {
var i = 0
var j = 0
var s = 0
def outer = 5000
def inner = 5000
for (i = 0; i < outer; i++) {
for (j = 0; j < inner; j++) {
s = s + 1
}
}
return s
}
// Run each and print timing
function run(name, fn) {
var start = time.number()
var result = fn()
var elapsed = time.number() - start
var ms = whole(elapsed * 100000) / 100
log.console(` ${name}: ${ms} ms (result: ${result})`)
}
log.console("=== Arithmetic Benchmark ===")
log.console(` iterations: ${iterations}`)
run("int_sum ", bench_int_sum)
run("int_mul_mod ", bench_int_mul_mod)
run("float_arith ", bench_float_arith)
run("branch ", bench_branch)
run("nested_loop ", bench_nested)

67
bench_arith.js Normal file
View File

@@ -0,0 +1,67 @@
// bench_arith.js — arithmetic and number crunching benchmark (QuickJS)
const iterations = 2000000;
function bench_int_sum() {
let s = 0;
for (let i = 0; i < iterations; i++) {
s = s + i;
}
return s;
}
function bench_int_mul_mod() {
let s = 0;
for (let i = 1; i < iterations; i++) {
s = s + (i * 7 % 1000);
}
return s;
}
function bench_float_arith() {
let s = 0.5;
for (let i = 1; i < iterations; i++) {
s = s + 1.0 / i;
}
return s;
}
function bench_branch() {
let fizz = 0, buzz = 0, fizzbuzz = 0;
for (let i = 1; i <= iterations; i++) {
if (i % 15 === 0) {
fizzbuzz = fizzbuzz + 1;
} else if (i % 3 === 0) {
fizz = fizz + 1;
} else if (i % 5 === 0) {
buzz = buzz + 1;
}
}
return fizz + buzz + fizzbuzz;
}
function bench_nested() {
let s = 0;
const outer = 5000, inner = 5000;
for (let i = 0; i < outer; i++) {
for (let j = 0; j < inner; j++) {
s = s + 1;
}
}
return s;
}
function run(name, fn) {
const start = performance.now();
const result = fn();
const elapsed = performance.now() - start;
console.log(` ${name}: ${elapsed.toFixed(2)} ms (result: ${result})`);
}
console.log("=== Arithmetic Benchmark ===");
console.log(` iterations: ${iterations}`);
run("int_sum ", bench_int_sum);
run("int_mul_mod ", bench_int_mul_mod);
run("float_arith ", bench_float_arith);
run("branch ", bench_branch);
run("nested_loop ", bench_nested);

68
bench_arith.lua Normal file
View File

@@ -0,0 +1,68 @@
-- bench_arith.lua — arithmetic and number crunching benchmark (Lua)
local iterations = 2000000
local clock = os.clock
local function bench_int_sum()
local s = 0
for i = 0, iterations - 1 do
s = s + i
end
return s
end
local function bench_int_mul_mod()
local s = 0
for i = 1, iterations - 1 do
s = s + (i * 7 % 1000)
end
return s
end
local function bench_float_arith()
local s = 0.5
for i = 1, iterations - 1 do
s = s + 1.0 / i
end
return s
end
local function bench_branch()
local fizz, buzz, fizzbuzz = 0, 0, 0
for i = 1, iterations do
if i % 15 == 0 then
fizzbuzz = fizzbuzz + 1
elseif i % 3 == 0 then
fizz = fizz + 1
elseif i % 5 == 0 then
buzz = buzz + 1
end
end
return fizz + buzz + fizzbuzz
end
local function bench_nested()
local s = 0
local outer, inner = 5000, 5000
for i = 0, outer - 1 do
for j = 0, inner - 1 do
s = s + 1
end
end
return s
end
local function run(name, fn)
local start = clock()
local result = fn()
local elapsed = (clock() - start) * 1000
print(string.format(" %s: %.2f ms (result: %s)", name, elapsed, tostring(result)))
end
print("=== Arithmetic Benchmark ===")
print(string.format(" iterations: %d", iterations))
run("int_sum ", bench_int_sum)
run("int_mul_mod ", bench_int_mul_mod)
run("float_arith ", bench_float_arith)
run("branch ", bench_branch)
run("nested_loop ", bench_nested)

113
bench_array.ce Normal file
View File

@@ -0,0 +1,113 @@
// bench_array.ce — array operation benchmark
// Tests: sequential access, push/build, index write, sum reduction, sort
var time = use('time')
def size = 100000
// 1. Build array with push
function bench_push() {
var a = []
var i = 0
for (i = 0; i < size; i++) {
a[] = i
}
return length(a)
}
// 2. Index write into preallocated array
function bench_index_write() {
var a = array(size, 0)
var i = 0
for (i = 0; i < size; i++) {
a[i] = i
}
return a[size - 1]
}
// 3. Sequential read and sum
function bench_seq_read() {
var a = array(size, 0)
var i = 0
for (i = 0; i < size; i++) {
a[i] = i
}
var s = 0
for (i = 0; i < size; i++) {
s = s + a[i]
}
return s
}
// 4. Reverse array in-place
function bench_reverse() {
var a = array(size, 0)
var i = 0
for (i = 0; i < size; i++) {
a[i] = i
}
var lo = 0
var hi = size - 1
var tmp = 0
while (lo < hi) {
tmp = a[lo]
a[lo] = a[hi]
a[hi] = tmp
lo = lo + 1
hi = hi - 1
}
return a[0]
}
// 5. Nested array access (matrix-like, 300x300)
function bench_matrix() {
def n = 300
var mat = array(n, null)
var i = 0
var j = 0
for (i = 0; i < n; i++) {
mat[i] = array(n, 0)
for (j = 0; j < n; j++) {
mat[i][j] = i * n + j
}
}
// sum diagonal
var s = 0
for (i = 0; i < n; i++) {
s = s + mat[i][i]
}
return s
}
// 6. filter-like: count evens
function bench_filter_count() {
var a = array(size, 0)
var i = 0
for (i = 0; i < size; i++) {
a[i] = i
}
var count = 0
for (i = 0; i < size; i++) {
if (a[i] % 2 == 0) {
count = count + 1
}
}
return count
}
function run(name, fn) {
var start = time.number()
var result = fn()
var elapsed = time.number() - start
var ms = whole(elapsed * 100000) / 100
log.console(` ${name}: ${ms} ms (result: ${result})`)
}
log.console("=== Array Benchmark ===")
log.console(` size: ${size}`)
run("push ", bench_push)
run("index_write ", bench_index_write)
run("seq_read_sum ", bench_seq_read)
run("reverse ", bench_reverse)
run("matrix_300 ", bench_matrix)
run("filter_count ", bench_filter_count)

93
bench_array.js Normal file
View File

@@ -0,0 +1,93 @@
// bench_array.js — array operation benchmark (QuickJS)
const size = 100000;
function bench_push() {
let a = [];
for (let i = 0; i < size; i++) {
a.push(i);
}
return a.length;
}
function bench_index_write() {
let a = new Array(size).fill(0);
for (let i = 0; i < size; i++) {
a[i] = i;
}
return a[size - 1];
}
function bench_seq_read() {
let a = new Array(size).fill(0);
for (let i = 0; i < size; i++) {
a[i] = i;
}
let s = 0;
for (let i = 0; i < size; i++) {
s = s + a[i];
}
return s;
}
function bench_reverse() {
let a = new Array(size).fill(0);
for (let i = 0; i < size; i++) {
a[i] = i;
}
let lo = 0, hi = size - 1, tmp;
while (lo < hi) {
tmp = a[lo];
a[lo] = a[hi];
a[hi] = tmp;
lo = lo + 1;
hi = hi - 1;
}
return a[0];
}
function bench_matrix() {
const n = 300;
let mat = new Array(n);
for (let i = 0; i < n; i++) {
mat[i] = new Array(n).fill(0);
for (let j = 0; j < n; j++) {
mat[i][j] = i * n + j;
}
}
let s = 0;
for (let i = 0; i < n; i++) {
s = s + mat[i][i];
}
return s;
}
function bench_filter_count() {
let a = new Array(size).fill(0);
for (let i = 0; i < size; i++) {
a[i] = i;
}
let count = 0;
for (let i = 0; i < size; i++) {
if (a[i] % 2 === 0) {
count = count + 1;
}
}
return count;
}
function run(name, fn) {
const start = performance.now();
const result = fn();
const elapsed = performance.now() - start;
console.log(` ${name}: ${elapsed.toFixed(2)} ms (result: ${result})`);
}
console.log("=== Array Benchmark ===");
console.log(` size: ${size}`);
run("push ", bench_push);
run("index_write ", bench_index_write);
run("seq_read_sum ", bench_seq_read);
run("reverse ", bench_reverse);
run("matrix_300 ", bench_matrix);
run("filter_count ", bench_filter_count);

93
bench_array.lua Normal file
View File

@@ -0,0 +1,93 @@
-- bench_array.lua — array operation benchmark (Lua)
local size = 100000
local clock = os.clock
local function bench_push()
local a = {}
for i = 0, size - 1 do
a[#a + 1] = i
end
return #a
end
local function bench_index_write()
local a = {}
for i = 1, size do a[i] = 0 end
for i = 1, size do
a[i] = i - 1
end
return a[size]
end
local function bench_seq_read()
local a = {}
for i = 1, size do
a[i] = i - 1
end
local s = 0
for i = 1, size do
s = s + a[i]
end
return s
end
local function bench_reverse()
local a = {}
for i = 1, size do
a[i] = i - 1
end
local lo, hi = 1, size
while lo < hi do
a[lo], a[hi] = a[hi], a[lo]
lo = lo + 1
hi = hi - 1
end
return a[1]
end
local function bench_matrix()
local n = 300
local mat = {}
for i = 1, n do
mat[i] = {}
for j = 1, n do
mat[i][j] = (i - 1) * n + (j - 1)
end
end
local s = 0
for i = 1, n do
s = s + mat[i][i]
end
return s
end
local function bench_filter_count()
local a = {}
for i = 1, size do
a[i] = i - 1
end
local count = 0
for i = 1, size do
if a[i] % 2 == 0 then
count = count + 1
end
end
return count
end
local function run(name, fn)
local start = clock()
local result = fn()
local elapsed = (clock() - start) * 1000
print(string.format(" %s: %.2f ms (result: %s)", name, elapsed, tostring(result)))
end
print("=== Array Benchmark ===")
print(string.format(" size: %d", size))
run("push ", bench_push)
run("index_write ", bench_index_write)
run("seq_read_sum ", bench_seq_read)
run("reverse ", bench_reverse)
run("matrix_300 ", bench_matrix)
run("filter_count ", bench_filter_count)

21
bench_fib.ce Normal file
View File

@@ -0,0 +1,21 @@
var time = use('time')
function fib(n) {
if (n < 2) {
return n
}
return fib(n - 1) + fib(n - 2)
}
function run(name, fn) {
var start = time.number()
var result = fn()
var elapsed = time.number() - start
var ms = whole(elapsed * 100000) / 100
log.console(` ${name}: ${ms} ms (result: ${result})`)
}
log.console("=== Cell fib ===")
run("fib(25)", function() { return fib(25) })
run("fib(30)", function() { return fib(30) })
run("fib(35)", function() { return fib(35) })

118
bench_object.ce Normal file
View File

@@ -0,0 +1,118 @@
// bench_object.ce — object/record and string benchmark
// Tests: property read/write, string concat, string interpolation, method-like dispatch
var time = use('time')
def iterations = 200000
// 1. Record create + property write
function bench_record_create() {
var i = 0
var r = null
for (i = 0; i < iterations; i++) {
r = {x: i, y: i + 1, z: i + 2}
}
return r.z
}
// 2. Property read in loop
function bench_prop_read() {
var obj = {x: 10, y: 20, z: 30, w: 40}
var i = 0
var s = 0
for (i = 0; i < iterations; i++) {
s = s + obj.x + obj.y + obj.z + obj.w
}
return s
}
// 3. Dynamic property access (computed keys)
function bench_dynamic_prop() {
var obj = {a: 1, b: 2, c: 3, d: 4, e: 5}
var keys = ["a", "b", "c", "d", "e"]
var i = 0
var j = 0
var s = 0
for (i = 0; i < iterations; i++) {
for (j = 0; j < 5; j++) {
s = s + obj[keys[j]]
}
}
return s
}
// 4. String concatenation
function bench_string_concat() {
var i = 0
var s = ""
def n = 10000
for (i = 0; i < n; i++) {
s = s + "x"
}
return length(s)
}
// 5. String interpolation
function bench_interpolation() {
var i = 0
var s = ""
def n = 50000
for (i = 0; i < n; i++) {
s = `item_${i}`
}
return s
}
// 6. Prototype chain / method-like call
function make_point(x, y) {
return {
x: x,
y: y,
sum: function(self) {
return self.x + self.y
}
}
}
function bench_method_call() {
var p = make_point(3, 4)
var i = 0
var s = 0
for (i = 0; i < iterations; i++) {
s = s + p.sum(p)
}
return s
}
// 7. Function call overhead (simple recursion depth)
function fib(n) {
if (n <= 1) return n
return fib(n - 1) + fib(n - 2)
}
function bench_fncall() {
var i = 0
var s = 0
for (i = 0; i < 20; i++) {
s = s + fib(25)
}
return s
}
function run(name, fn) {
var start = time.number()
var result = fn()
var elapsed = time.number() - start
var ms = whole(elapsed * 100000) / 100
log.console(` ${name}: ${ms} ms (result: ${result})`)
}
log.console("=== Object / String / Call Benchmark ===")
log.console(` iterations: ${iterations}`)
run("record_create ", bench_record_create)
run("prop_read ", bench_prop_read)
run("dynamic_prop ", bench_dynamic_prop)
run("string_concat ", bench_string_concat)
run("interpolation ", bench_interpolation)
run("method_call ", bench_method_call)
run("fncall_fib25 ", bench_fncall)

99
bench_object.js Normal file
View File

@@ -0,0 +1,99 @@
// bench_object.js — object/string/call benchmark (QuickJS)
const iterations = 200000;
function bench_record_create() {
let r;
for (let i = 0; i < iterations; i++) {
r = {x: i, y: i + 1, z: i + 2};
}
return r.z;
}
function bench_prop_read() {
const obj = {x: 10, y: 20, z: 30, w: 40};
let s = 0;
for (let i = 0; i < iterations; i++) {
s = s + obj.x + obj.y + obj.z + obj.w;
}
return s;
}
function bench_dynamic_prop() {
const obj = {a: 1, b: 2, c: 3, d: 4, e: 5};
const keys = ["a", "b", "c", "d", "e"];
let s = 0;
for (let i = 0; i < iterations; i++) {
for (let j = 0; j < 5; j++) {
s = s + obj[keys[j]];
}
}
return s;
}
function bench_string_concat() {
let s = "";
const n = 10000;
for (let i = 0; i < n; i++) {
s = s + "x";
}
return s.length;
}
function bench_interpolation() {
let s = "";
const n = 50000;
for (let i = 0; i < n; i++) {
s = `item_${i}`;
}
return s;
}
function make_point(x, y) {
return {
x: x,
y: y,
sum: function(self) {
return self.x + self.y;
}
};
}
function bench_method_call() {
const p = make_point(3, 4);
let s = 0;
for (let i = 0; i < iterations; i++) {
s = s + p.sum(p);
}
return s;
}
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
function bench_fncall() {
let s = 0;
for (let i = 0; i < 20; i++) {
s = s + fib(25);
}
return s;
}
function run(name, fn) {
const start = performance.now();
const result = fn();
const elapsed = performance.now() - start;
console.log(` ${name}: ${elapsed.toFixed(2)} ms (result: ${result})`);
}
console.log("=== Object / String / Call Benchmark ===");
console.log(` iterations: ${iterations}`);
run("record_create ", bench_record_create);
run("prop_read ", bench_prop_read);
run("dynamic_prop ", bench_dynamic_prop);
run("string_concat ", bench_string_concat);
run("interpolation ", bench_interpolation);
run("method_call ", bench_method_call);
run("fncall_fib25 ", bench_fncall);

101
bench_object.lua Normal file
View File

@@ -0,0 +1,101 @@
-- bench_object.lua — object/string/call benchmark (Lua)
local iterations = 200000
local clock = os.clock
local function bench_record_create()
local r
for i = 0, iterations - 1 do
r = {x = i, y = i + 1, z = i + 2}
end
return r.z
end
local function bench_prop_read()
local obj = {x = 10, y = 20, z = 30, w = 40}
local s = 0
for i = 0, iterations - 1 do
s = s + obj.x + obj.y + obj.z + obj.w
end
return s
end
local function bench_dynamic_prop()
local obj = {a = 1, b = 2, c = 3, d = 4, e = 5}
local keys = {"a", "b", "c", "d", "e"}
local s = 0
for i = 0, iterations - 1 do
for j = 1, 5 do
s = s + obj[keys[j]]
end
end
return s
end
local function bench_string_concat()
local parts = {}
local n = 10000
for i = 1, n do
parts[i] = "x"
end
local s = table.concat(parts)
return #s
end
local function bench_interpolation()
local s = ""
local n = 50000
for i = 0, n - 1 do
s = string.format("item_%d", i)
end
return s
end
local function make_point(x, y)
return {
x = x,
y = y,
sum = function(self)
return self.x + self.y
end
}
end
local function bench_method_call()
local p = make_point(3, 4)
local s = 0
for i = 0, iterations - 1 do
s = s + p.sum(p)
end
return s
end
local function fib(n)
if n <= 1 then return n end
return fib(n - 1) + fib(n - 2)
end
local function bench_fncall()
local s = 0
for i = 0, 19 do
s = s + fib(25)
end
return s
end
local function run(name, fn)
local start = clock()
local result = fn()
local elapsed = (clock() - start) * 1000
print(string.format(" %s: %.2f ms (result: %s)", name, elapsed, tostring(result)))
end
print("=== Object / String / Call Benchmark ===")
print(string.format(" iterations: %d", iterations))
run("record_create ", bench_record_create)
run("prop_read ", bench_prop_read)
run("dynamic_prop ", bench_dynamic_prop)
run("string_concat ", bench_string_concat)
run("interpolation ", bench_interpolation)
run("method_call ", bench_method_call)
run("fncall_fib25 ", bench_fncall)

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

74
boot_miscompile_bad.cm Normal file
View File

@@ -0,0 +1,74 @@
// boot_miscompile_bad.cm — Documents a boot compiler miscompilation bug.
//
// BUG SUMMARY:
// The boot compiler's optimizer (likely compress_slots, eliminate_moves,
// or infer_param_types) miscompiles a specific pattern when it appears
// inside streamline.cm. The pattern: an array-loaded value used as a
// dynamic index for another array store, inside a guarded block:
//
// sv = instr[j]
// if (is_number(sv) && sv >= 0 && sv < nr_slots) {
// last_ref[sv] = i // <-- miscompiled: sv reads wrong slot
// }
//
// The bug is CONTEXT-DEPENDENT on streamline.cm's exact function/closure
// structure. A standalone module with the same pattern does NOT trigger it.
// The boot optimizer's cross-function analysis (infer_param_types, type
// propagation, etc.) makes different decisions in the full streamline.cm
// context, leading to the miscompilation.
//
// SYMPTOMS:
// - 'log' is not defined (comparison error path fires on non-comparable values)
// - array index must be a number (store_dynamic with corrupted index)
// - Error line has NO reference to 'log' — the reference comes from the
// error-reporting code path of the < operator
// - Non-deterministic: different error messages on different runs
// - NOT a GC bug: persists with --heap 4GB
// - NOT slot overflow: function has only 85 raw slots
//
// TO REPRODUCE:
// In streamline.cm, replace the build_slot_liveness function body with
// this version (raw operand scanning instead of get_slot_refs):
//
// var build_slot_liveness = function(instructions, nr_slots) {
// var last_ref = array(nr_slots, -1)
// var n = length(instructions)
// var i = 0
// var j = 0
// var limit = 0
// var sv = 0
// var instr = null
//
// while (i < n) {
// instr = instructions[i]
// if (is_array(instr)) {
// j = 1
// limit = length(instr) - 2
// while (j < limit) {
// sv = instr[j]
// if (is_number(sv) && sv >= 0 && sv < nr_slots) {
// last_ref[sv] = i
// }
// j = j + 1
// }
// }
// i = i + 1
// }
// return last_ref
// }
//
// Then: rm -rf .cell/build && ./cell --dev vm_suite
//
// WORKAROUND:
// Use get_slot_refs(instr) to iterate only over known slot-reference
// positions. This produces different IR that the boot optimizer handles
// correctly, and is also more semantically correct.
//
// FIXING:
// To find the root cause, compare the boot-compiled bytecodes of
// build_slot_liveness (in the full streamline.cm context) vs the
// source-compiled bytecodes. Use disasm.ce with --optimized to see
// what the source compiler produces. The boot-compiled bytecodes
// would need a C-level MachCode dump to inspect.
return null

456
cfg.ce Normal file
View File

@@ -0,0 +1,456 @@
// cfg.ce — control flow graph
//
// Usage:
// cell cfg --fn <N|name> <file> Text CFG for function
// cell cfg --dot --fn <N|name> <file> DOT output for graphviz
// cell cfg <file> Text CFG for all functions
var shop = use("internal/shop")
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var fmt_val = function(v) {
if (is_null(v)) return "null"
if (is_number(v)) return text(v)
if (is_text(v)) return `"${v}"`
if (is_object(v)) return text(v)
if (is_logical(v)) return v ? "true" : "false"
return text(v)
}
var is_jump_op = function(op) {
return op == "jump" || op == "jump_true" || op == "jump_false" || op == "jump_null" || op == "jump_not_null"
}
var is_conditional_jump = function(op) {
return op == "jump_true" || op == "jump_false" || op == "jump_null" || op == "jump_not_null"
}
var is_terminator = function(op) {
return op == "return" || op == "disrupt" || op == "tail_invoke" || op == "goinvoke"
}
var run = function() {
var filename = null
var fn_filter = null
var show_dot = false
var use_optimized = false
var i = 0
var compiled = null
var main_name = null
var fi = 0
var func = null
var fname = null
while (i < length(args)) {
if (args[i] == '--fn') {
i = i + 1
fn_filter = args[i]
} else if (args[i] == '--dot') {
show_dot = true
} else if (args[i] == '--optimized') {
use_optimized = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell cfg [--fn <N|name>] [--dot] [--optimized] <file>")
log.console("")
log.console(" --fn <N|name> Filter to function by index or name")
log.console(" --dot Output DOT format for graphviz")
log.console(" --optimized Use optimized IR")
return null
} else if (!starts_with(args[i], '-')) {
filename = args[i]
}
i = i + 1
}
if (!filename) {
log.console("Usage: cell cfg [--fn <N|name>] [--dot] [--optimized] <file>")
return null
}
if (use_optimized) {
compiled = shop.compile_file(filename)
} else {
compiled = shop.mcode_file(filename)
}
var fn_matches = function(index, name) {
var match = null
if (fn_filter == null) return true
if (index >= 0 && fn_filter == text(index)) return true
if (name != null) {
match = search(name, fn_filter)
if (match != null && match >= 0) return true
}
return false
}
var build_cfg = function(func) {
var instrs = func.instructions
var blocks = []
var label_to_block = {}
var pc_to_block = {}
var label_to_pc = {}
var block_start_pcs = {}
var after_terminator = false
var current_block = null
var current_label = null
var pc = 0
var ii = 0
var bi = 0
var instr = null
var op = null
var n = 0
var line_num = null
var blk = null
var last_instr_data = null
var last_op = null
var target_label = null
var target_bi = null
var edge_type = null
if (instrs == null || length(instrs) == 0) return []
// Pass 1: identify block start PCs
block_start_pcs["0"] = true
pc = 0
ii = 0
while (ii < length(instrs)) {
instr = instrs[ii]
if (is_array(instr)) {
op = instr[0]
if (after_terminator) {
block_start_pcs[text(pc)] = true
after_terminator = false
}
if (is_jump_op(op) || is_terminator(op)) {
after_terminator = true
}
pc = pc + 1
}
ii = ii + 1
}
// Pass 2: map labels to PCs and mark as block starts
pc = 0
ii = 0
while (ii < length(instrs)) {
instr = instrs[ii]
if (is_text(instr) && !starts_with(instr, "_nop_")) {
label_to_pc[instr] = pc
block_start_pcs[text(pc)] = true
} else if (is_array(instr)) {
pc = pc + 1
}
ii = ii + 1
}
// Pass 3: build basic blocks
pc = 0
ii = 0
current_label = null
while (ii < length(instrs)) {
instr = instrs[ii]
if (is_text(instr)) {
if (!starts_with(instr, "_nop_")) {
current_label = instr
}
ii = ii + 1
continue
}
if (is_array(instr)) {
if (block_start_pcs[text(pc)]) {
if (current_block != null) {
push(blocks, current_block)
}
current_block = {
id: length(blocks),
label: current_label,
start_pc: pc,
end_pc: pc,
instrs: [],
edges: [],
first_line: null,
last_line: null
}
current_label = null
}
if (current_block != null) {
push(current_block.instrs, {pc: pc, instr: instr})
current_block.end_pc = pc
n = length(instr)
line_num = instr[n - 2]
if (line_num != null) {
if (current_block.first_line == null) {
current_block.first_line = line_num
}
current_block.last_line = line_num
}
}
pc = pc + 1
}
ii = ii + 1
}
if (current_block != null) {
push(blocks, current_block)
}
// Build block index
bi = 0
while (bi < length(blocks)) {
pc_to_block[text(blocks[bi].start_pc)] = bi
if (blocks[bi].label != null) {
label_to_block[blocks[bi].label] = bi
}
bi = bi + 1
}
// Pass 4: compute edges
bi = 0
while (bi < length(blocks)) {
blk = blocks[bi]
if (length(blk.instrs) > 0) {
last_instr_data = blk.instrs[length(blk.instrs) - 1]
last_op = last_instr_data.instr[0]
n = length(last_instr_data.instr)
if (is_jump_op(last_op)) {
if (last_op == "jump") {
target_label = last_instr_data.instr[1]
} else {
target_label = last_instr_data.instr[2]
}
target_bi = label_to_block[target_label]
if (target_bi != null) {
edge_type = "jump"
if (target_bi <= bi) {
edge_type = "loop back-edge"
}
push(blk.edges, {target: target_bi, kind: edge_type})
}
if (is_conditional_jump(last_op)) {
if (bi + 1 < length(blocks)) {
push(blk.edges, {target: bi + 1, kind: "fallthrough"})
}
}
} else if (is_terminator(last_op)) {
push(blk.edges, {target: -1, kind: "EXIT (" + last_op + ")"})
} else {
if (bi + 1 < length(blocks)) {
push(blk.edges, {target: bi + 1, kind: "fallthrough"})
}
}
}
bi = bi + 1
}
return blocks
}
var print_cfg_text = function(blocks, name) {
var bi = 0
var blk = null
var header = null
var ii = 0
var idata = null
var instr = null
var op = null
var n = 0
var parts = null
var j = 0
var operands = null
var ei = 0
var edge = null
var target_label = null
log.compile(`\n=== ${name} ===`)
if (length(blocks) == 0) {
log.compile(" (empty)")
return null
}
bi = 0
while (bi < length(blocks)) {
blk = blocks[bi]
header = ` B${text(bi)}`
if (blk.label != null) {
header = header + ` "${blk.label}"`
}
header = header + ` [pc ${text(blk.start_pc)}-${text(blk.end_pc)}`
if (blk.first_line != null) {
if (blk.first_line == blk.last_line) {
header = header + `, line ${text(blk.first_line)}`
} else {
header = header + `, lines ${text(blk.first_line)}-${text(blk.last_line)}`
}
}
header = header + "]:"
log.compile(header)
ii = 0
while (ii < length(blk.instrs)) {
idata = blk.instrs[ii]
instr = idata.instr
op = instr[0]
n = length(instr)
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
j = j + 1
}
operands = text(parts, ", ")
log.compile(` ${pad_right(text(idata.pc), 6)}${pad_right(op, 15)}${operands}`)
ii = ii + 1
}
ei = 0
while (ei < length(blk.edges)) {
edge = blk.edges[ei]
if (edge.target == -1) {
log.compile(` -> ${edge.kind}`)
} else {
target_label = blocks[edge.target].label
if (target_label != null) {
log.compile(` -> B${text(edge.target)} "${target_label}" (${edge.kind})`)
} else {
log.compile(` -> B${text(edge.target)} (${edge.kind})`)
}
}
ei = ei + 1
}
log.compile("")
bi = bi + 1
}
return null
}
var print_cfg_dot = function(blocks, name) {
var safe_name = replace(replace(name, '"', '\\"'), ' ', '_')
var bi = 0
var blk = null
var label_text = null
var ii = 0
var idata = null
var instr = null
var op = null
var n = 0
var parts = null
var j = 0
var operands = null
var ei = 0
var edge = null
var style = null
log.compile(`digraph "${safe_name}" {`)
log.compile(" rankdir=TB;")
log.compile(" node [shape=record, fontname=monospace, fontsize=10];")
bi = 0
while (bi < length(blocks)) {
blk = blocks[bi]
label_text = "B" + text(bi)
if (blk.label != null) {
label_text = label_text + " (" + blk.label + ")"
}
label_text = label_text + "\\npc " + text(blk.start_pc) + "-" + text(blk.end_pc)
if (blk.first_line != null) {
label_text = label_text + "\\nline " + text(blk.first_line)
}
label_text = label_text + "|"
ii = 0
while (ii < length(blk.instrs)) {
idata = blk.instrs[ii]
instr = idata.instr
op = instr[0]
n = length(instr)
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
j = j + 1
}
operands = text(parts, ", ")
label_text = label_text + text(idata.pc) + " " + op + " " + replace(operands, '"', '\\"') + "\\l"
ii = ii + 1
}
log.compile(" B" + text(bi) + " [label=\"{" + label_text + "}\"];")
bi = bi + 1
}
// Edges
bi = 0
while (bi < length(blocks)) {
blk = blocks[bi]
ei = 0
while (ei < length(blk.edges)) {
edge = blk.edges[ei]
if (edge.target >= 0) {
style = ""
if (edge.kind == "loop back-edge") {
style = " [style=bold, color=red, label=\"loop\"]"
} else if (edge.kind == "fallthrough") {
style = " [style=dashed]"
}
log.compile(` B${text(bi)} -> B${text(edge.target)}${style};`)
}
ei = ei + 1
}
bi = bi + 1
}
log.compile("}")
return null
}
var process_function = function(func, name, index) {
var blocks = build_cfg(func)
if (show_dot) {
print_cfg_dot(blocks, name)
} else {
print_cfg_text(blocks, name)
}
return null
}
// Process functions
main_name = compiled.name != null ? compiled.name : "<main>"
if (compiled.main != null) {
if (fn_matches(-1, main_name)) {
process_function(compiled.main, main_name, -1)
}
}
if (compiled.functions != null) {
fi = 0
while (fi < length(compiled.functions)) {
func = compiled.functions[fi]
fname = func.name != null ? func.name : "<anonymous>"
if (fn_matches(fi, fname)) {
process_function(func, `[${text(fi)}] ${fname}`, fi)
}
fi = fi + 1
}
}
return null
}
run()
$stop()

310
diff_ir.ce Normal file
View File

@@ -0,0 +1,310 @@
// diff_ir.ce — mcode vs streamline diff
//
// Usage:
// cell diff_ir <file> Diff all functions
// cell diff_ir --fn <N|name> <file> Diff only one function
// cell diff_ir --summary <file> Counts only
var fd = use("fd")
var shop = use("internal/shop")
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var fmt_val = function(v) {
if (is_null(v)) return "null"
if (is_number(v)) return text(v)
if (is_text(v)) return `"${v}"`
if (is_object(v)) return text(v)
if (is_logical(v)) return v ? "true" : "false"
return text(v)
}
var run = function() {
var fn_filter = null
var show_summary = false
var filename = null
var i = 0
var mcode_ir = null
var opt_ir = null
var source_text = null
var source_lines = null
var main_name = null
var fi = 0
var func = null
var opt_func = null
var fname = null
while (i < length(args)) {
if (args[i] == '--fn') {
i = i + 1
fn_filter = args[i]
} else if (args[i] == '--summary') {
show_summary = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell diff_ir [--fn <N|name>] [--summary] <file>")
log.console("")
log.console(" --fn <N|name> Filter to function by index or name")
log.console(" --summary Show counts only")
return null
} else if (!starts_with(args[i], '-')) {
filename = args[i]
}
i = i + 1
}
if (!filename) {
log.console("Usage: cell diff_ir [--fn <N|name>] [--summary] <file>")
return null
}
mcode_ir = shop.mcode_file(filename)
opt_ir = shop.compile_file(filename)
source_text = text(fd.slurp(filename))
source_lines = array(source_text, "\n")
var get_source_line = function(line_num) {
if (line_num < 1 || line_num > length(source_lines)) return null
return source_lines[line_num - 1]
}
var fn_matches = function(index, name) {
var match = null
if (fn_filter == null) return true
if (index >= 0 && fn_filter == text(index)) return true
if (name != null) {
match = search(name, fn_filter)
if (match != null && match >= 0) return true
}
return false
}
var fmt_instr = function(instr) {
var op = instr[0]
var n = length(instr)
var parts = []
var j = 1
var operands = null
var line_str = null
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
j = j + 1
}
operands = text(parts, ", ")
line_str = instr[n - 2] != null ? `:${text(instr[n - 2])}` : ""
return pad_right(`${pad_right(op, 15)}${operands}`, 45) + line_str
}
var classify = function(before, after) {
var bn = 0
var an = 0
var k = 0
if (is_text(after) && starts_with(after, "_nop_")) return "eliminated"
if (is_array(before) && is_array(after)) {
if (before[0] != after[0]) return "rewritten"
bn = length(before)
an = length(after)
if (bn != an) return "rewritten"
k = 1
while (k < bn - 2) {
if (before[k] != after[k]) return "rewritten"
k = k + 1
}
return "identical"
}
return "identical"
}
var total_eliminated = 0
var total_rewritten = 0
var total_funcs = 0
var diff_function = function(mcode_func, opt_func, name, index) {
var nr_args = mcode_func.nr_args != null ? mcode_func.nr_args : 0
var nr_slots = mcode_func.nr_slots != null ? mcode_func.nr_slots : 0
var m_instrs = mcode_func.instructions
var o_instrs = opt_func.instructions
var eliminated = 0
var rewritten = 0
var mi = 0
var oi = 0
var pc = 0
var m_instr = null
var o_instr = null
var kind = null
var last_line = null
var instr_line = null
var n = 0
var src = null
var annotation = null
if (m_instrs == null) m_instrs = []
if (o_instrs == null) o_instrs = []
// First pass: count changes
mi = 0
oi = 0
while (mi < length(m_instrs) && oi < length(o_instrs)) {
m_instr = m_instrs[mi]
o_instr = o_instrs[oi]
if (is_text(m_instr)) {
mi = mi + 1
oi = oi + 1
continue
}
if (is_text(o_instr) && starts_with(o_instr, "_nop_")) {
if (is_array(m_instr)) {
eliminated = eliminated + 1
}
mi = mi + 1
oi = oi + 1
continue
}
if (is_array(m_instr) && is_array(o_instr)) {
kind = classify(m_instr, o_instr)
if (kind == "rewritten") {
rewritten = rewritten + 1
}
}
mi = mi + 1
oi = oi + 1
}
total_eliminated = total_eliminated + eliminated
total_rewritten = total_rewritten + rewritten
total_funcs = total_funcs + 1
if (show_summary) {
if (eliminated == 0 && rewritten == 0) {
log.compile(` ${pad_right(name + ":", 40)} 0 eliminated, 0 rewritten (unchanged)`)
} else {
log.compile(` ${pad_right(name + ":", 40)} ${text(eliminated)} eliminated, ${text(rewritten)} rewritten`)
}
return null
}
if (eliminated == 0 && rewritten == 0) return null
log.compile(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`)
log.compile(` ${text(eliminated)} eliminated, ${text(rewritten)} rewritten`)
// Second pass: show diffs
mi = 0
oi = 0
pc = 0
last_line = null
while (mi < length(m_instrs) && oi < length(o_instrs)) {
m_instr = m_instrs[mi]
o_instr = o_instrs[oi]
if (is_text(m_instr) && !starts_with(m_instr, "_nop_")) {
mi = mi + 1
oi = oi + 1
continue
}
if (is_text(m_instr) && starts_with(m_instr, "_nop_")) {
mi = mi + 1
oi = oi + 1
continue
}
if (is_text(o_instr) && starts_with(o_instr, "_nop_")) {
if (is_array(m_instr)) {
n = length(m_instr)
instr_line = m_instr[n - 2]
if (instr_line != last_line && instr_line != null) {
src = get_source_line(instr_line)
if (src != null) src = trim(src)
if (last_line != null) log.compile("")
if (src != null && length(src) > 0) {
log.compile(` --- line ${text(instr_line)}: ${src} ---`)
}
last_line = instr_line
}
log.compile(` - ${pad_right(text(pc), 6)}${fmt_instr(m_instr)}`)
log.compile(` + ${pad_right(text(pc), 6)}${pad_right(o_instr, 45)} (eliminated)`)
}
mi = mi + 1
oi = oi + 1
pc = pc + 1
continue
}
if (is_array(m_instr) && is_array(o_instr)) {
kind = classify(m_instr, o_instr)
if (kind != "identical") {
n = length(m_instr)
instr_line = m_instr[n - 2]
if (instr_line != last_line && instr_line != null) {
src = get_source_line(instr_line)
if (src != null) src = trim(src)
if (last_line != null) log.compile("")
if (src != null && length(src) > 0) {
log.compile(` --- line ${text(instr_line)}: ${src} ---`)
}
last_line = instr_line
}
annotation = ""
if (kind == "rewritten") {
if (o_instr[0] == "concat" && m_instr[0] != "concat") {
annotation = "(specialized)"
} else {
annotation = "(rewritten)"
}
}
log.compile(` - ${pad_right(text(pc), 6)}${fmt_instr(m_instr)}`)
log.compile(` + ${pad_right(text(pc), 6)}${fmt_instr(o_instr)} ${annotation}`)
}
pc = pc + 1
}
mi = mi + 1
oi = oi + 1
}
return null
}
// Process functions
main_name = mcode_ir.name != null ? mcode_ir.name : "<main>"
if (mcode_ir.main != null && opt_ir.main != null) {
if (fn_matches(-1, main_name)) {
diff_function(mcode_ir.main, opt_ir.main, main_name, -1)
}
}
if (mcode_ir.functions != null && opt_ir.functions != null) {
fi = 0
while (fi < length(mcode_ir.functions) && fi < length(opt_ir.functions)) {
func = mcode_ir.functions[fi]
opt_func = opt_ir.functions[fi]
fname = func.name != null ? func.name : "<anonymous>"
if (fn_matches(fi, fname)) {
diff_function(func, opt_func, `[${text(fi)}] ${fname}`, fi)
}
fi = fi + 1
}
}
if (show_summary) {
log.compile(`\n total: ${text(total_eliminated)} eliminated, ${text(total_rewritten)} rewritten across ${text(total_funcs)} functions`)
}
return null
}
run()
$stop()

View File

@@ -30,6 +30,10 @@ Each stage has a corresponding CLI tool that lets you see its output.
| streamline | `streamline.ce --ir` | Human-readable canonical IR |
| disasm | `disasm.ce` | Source-interleaved disassembly |
| disasm | `disasm.ce --optimized` | Optimized source-interleaved disassembly |
| diff | `diff_ir.ce` | Mcode vs streamline instruction diff |
| xref | `xref.ce` | Cross-reference / call creation graph |
| cfg | `cfg.ce` | Control flow graph (basic blocks) |
| slots | `slots.ce` | Slot data flow / use-def chains |
| all | `ir_report.ce` | Structured optimizer flight recorder |
All tools take a source file as input and run the pipeline up to the relevant stage.
@@ -141,6 +145,160 @@ Function creation instructions include a cross-reference annotation showing the
3 function 5, 12 :235 ; -> [12] helper_fn
```
## diff_ir.ce
Compares mcode IR (before optimization) with streamline IR (after optimization), showing what the optimizer changed. Useful for understanding which instructions were eliminated, specialized, or rewritten.
```bash
cell diff_ir <file> # diff all functions
cell diff_ir --fn <N|name> <file> # diff only one function
cell diff_ir --summary <file> # counts only
```
| Flag | Description |
|------|-------------|
| (none) | Show all diffs with source interleaving |
| `--fn <N\|name>` | Filter to specific function by index or name |
| `--summary` | Show only eliminated/rewritten counts per function |
### Output Format
Changed instructions are shown in diff style with `-` (before) and `+` (after) lines:
```
=== [0] <anonymous> (args=1, slots=40) ===
17 eliminated, 51 rewritten
--- line 4: if (n <= 1) { ---
- 1 is_int 4, 1 :4
+ 1 is_int 3, 1 :4 (specialized)
- 3 is_int 5, 2 :4
+ 3 _nop_tc_1 (eliminated)
```
Summary mode gives a quick overview:
```
[0] <anonymous>: 17 eliminated, 51 rewritten
[1] <anonymous>: 65 eliminated, 181 rewritten
total: 86 eliminated, 250 rewritten across 4 functions
```
## xref.ce
Cross-reference / call graph tool. Shows which functions create other functions (via `function` instructions), building a creation tree.
```bash
cell xref <file> # full creation tree
cell xref --callers <N> <file> # who creates function [N]?
cell xref --callees <N> <file> # what does [N] create/call?
cell xref --dot <file> # DOT graph for graphviz
cell xref --optimized <file> # use optimized IR
```
| Flag | Description |
|------|-------------|
| (none) | Indented creation tree from main |
| `--callers <N>` | Show which functions create function [N] |
| `--callees <N>` | Show what function [N] creates (use -1 for main) |
| `--dot` | Output DOT format for graphviz |
| `--optimized` | Use optimized IR instead of raw mcode |
### Output Format
Default tree view:
```
demo_disasm.cm
[0] <anonymous>
[1] <anonymous>
[2] <anonymous>
```
Caller/callee query:
```
Callers of [0] <anonymous>:
demo_disasm.cm at line 3
```
DOT output can be piped to graphviz: `cell xref --dot file.cm | dot -Tpng -o xref.png`
## cfg.ce
Control flow graph tool. Identifies basic blocks from labels and jumps, computes edges, and detects loop back-edges.
```bash
cell cfg --fn <N|name> <file> # text CFG for function
cell cfg --dot --fn <N|name> <file> # DOT output for graphviz
cell cfg <file> # text CFG for all functions
cell cfg --optimized <file> # use optimized IR
```
| Flag | Description |
|------|-------------|
| `--fn <N\|name>` | Filter to specific function by index or name |
| `--dot` | Output DOT format for graphviz |
| `--optimized` | Use optimized IR instead of raw mcode |
### Output Format
```
=== [0] <anonymous> ===
B0 [pc 0-2, line 4]:
0 access 2, 1
1 is_int 4, 1
2 jump_false 4, "rel_ni_2"
-> B3 "rel_ni_2" (jump)
-> B1 (fallthrough)
B1 [pc 3-4, line 4]:
3 is_int 5, 2
4 jump_false 5, "rel_ni_2"
-> B3 "rel_ni_2" (jump)
-> B2 (fallthrough)
```
Each block shows its ID, PC range, source lines, instructions, and outgoing edges. Loop back-edges (target PC <= source PC) are annotated.
## slots.ce
Slot data flow analysis. Builds use-def chains for every slot in a function, showing where each slot is defined and used. Optionally captures type information from streamline.
```bash
cell slots --fn <N|name> <file> # slot summary for function
cell slots --slot <N> --fn <N|name> <file> # trace slot N
cell slots <file> # slot summary for all functions
```
| Flag | Description |
|------|-------------|
| `--fn <N\|name>` | Filter to specific function by index or name |
| `--slot <N>` | Show chronological DEF/USE trace for a specific slot |
### Output Format
Summary shows each slot with its def count, use count, inferred type, and first definition. Dead slots (defined but never used) are flagged:
```
=== [0] <anonymous> (args=1, slots=40) ===
slot defs uses type first-def
s0 0 0 - (this)
s1 0 10 - (arg 0)
s2 1 6 - pc 0: access
s10 1 0 - pc 29: invoke <- dead
```
Slot trace (`--slot N`) shows every DEF and USE in program order:
```
=== slot 3 in [0] <anonymous> ===
DEF pc 5: le_int 3, 1, 2 :4
DEF pc 11: le_float 3, 1, 2 :4
DEF pc 17: le_text 3, 1, 2 :4
USE pc 31: jump_false 3, "if_else_0" :4
```
## seed.ce
Regenerates the boot seed files in `boot/`. These are pre-compiled mcode IR (JSON) files that bootstrap the compilation pipeline on cold start.

View File

@@ -93,3 +93,13 @@ Arithmetic ops (ADD, SUB, MUL, DIV, MOD, POW) are executed inline without callin
DIV and MOD check for zero divisor (→ null). POW uses `pow()` with non-finite handling for finite inputs.
Comparison ops (EQ through GE) and bitwise ops still use `reg_vm_binop()` for their slow paths, as they handle a wider range of type combinations (string comparisons, null equality, etc.).
## String Concatenation
CONCAT has a three-tier dispatch for self-assign patterns (`concat R(A), R(A), R(C)` where dest equals the left operand):
1. **In-place append**: If `R(A)` is a mutable heap text (S bit clear) with `length + rhs_length <= cap56`, characters are appended directly. Zero allocation, zero GC.
2. **Growth allocation** (`JS_ConcatStringGrow`): Allocates a new text with 2x capacity and does **not** stone the result, leaving it mutable for subsequent appends.
3. **Exact-fit stoned** (`JS_ConcatString`): Used when dest differs from the left operand (normal non-self-assign concat).
The `stone_text` instruction (iABC, B=0, C=0) sets the S bit on a mutable heap text in `R(A)`. For non-pointer values or already-stoned text, it is a no-op. This instruction is emitted by the streamline optimizer at escape points; see [Streamline — insert_stone_text](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) and [Stone Memory — Mutable Text](stone.md#mutable-text-concatenation).

View File

@@ -101,6 +101,11 @@ Operands are register slot numbers (integers), constant values (strings, numbers
| Instruction | Operands | Description |
|-------------|----------|-------------|
| `concat` | `dest, a, b` | `dest = a ~ b` (text concatenation) |
| `stone_text` | `slot` | Stone a mutable text value (see below) |
The `stone_text` instruction is emitted by the streamline optimizer's escape analysis pass (`insert_stone_text`). It freezes a mutable text value before it escapes its defining slot — for example, before a `move`, `setarg`, `store_field`, `push`, or `put`. The instruction is only inserted when the slot is provably `T_TEXT`; non-text values never need stoning. See [Streamline Optimizer — insert_stone_text](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) for details.
At the VM level, `stone_text` is a single-operand instruction (iABC with B=0, C=0). If the slot holds a heap text without the S bit set, it sets the S bit. For all other values (integers, booleans, already-stoned text, etc.), it is a no-op.
### Comparison — Integer

View File

@@ -77,6 +77,30 @@ Messages between actors are stoned before delivery, ensuring actors never share
Literal objects and arrays that can be determined at compile time may be allocated directly in stone memory.
## Mutable Text Concatenation
String concatenation in a loop (`s = s + "x"`) is optimized to O(n) amortized by leaving concat results **unstoned** with over-allocated capacity. On the next concatenation, if the destination text is mutable (S bit clear) and has enough room, the VM appends in-place with zero allocation.
### How It Works
When the VM executes `concat dest, dest, src` (same destination and left operand — a self-assign pattern):
1. **Inline fast path**: If `dest` holds a heap text, is not stoned, and `length + src_length <= capacity` — append characters in place, update length, done. No allocation, no GC possible.
2. **Growth path** (`JS_ConcatStringGrow`): Allocate a new text with `capacity = max(new_length * 2, 16)`, copy both operands, and return the result **without stoning** it. The 2x growth factor means a loop of N concatenations does O(log N) allocations totaling O(N) character copies.
3. **Exact-fit path** (`JS_ConcatString`): When `dest != left` (not self-assign), the existing exact-fit stoned path is used. This is the normal case for expressions like `var c = a + b`.
### Safety Invariant
**An unstoned heap text is uniquely referenced by exactly one slot.** This is enforced by the `stone_text` mcode instruction, which the [streamline optimizer](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) inserts before any instruction that would create a second reference to the value (move, store, push, setarg, put). Two VM-level guards cover cases where the compiler cannot prove the type: `get` (closure reads) and `return` (inter-frame returns).
### Why Over-Allocation Is GC-Safe
- The copying collector copies based on `cap56` (the object header's capacity field), not `length`. Over-allocated capacity survives GC.
- `js_alloc_string` zero-fills the packed data region, so padding beyond `length` is always clean.
- String comparisons, hashing, and interning all use `length`, not `cap56`. Extra capacity is invisible to string operations.
## Relationship to GC
The Cheney copying collector only operates on the mutable heap. During collection, when the collector encounters a pointer to stone memory (S bit set), it skips it — stone objects are roots that never move. This means stone memory acts as a permanent root set with zero GC overhead.

View File

@@ -164,7 +164,44 @@ Removes `move a, a` instructions where the source and destination are the same s
**Nop prefix:** `_nop_mv_`
### 7. eliminate_unreachable (dead code after return)
### 7. insert_stone_text (mutable text escape analysis)
Inserts `stone_text` instructions before mutable text values escape their defining slot. This pass supports the mutable text concatenation optimization (see [Stone Memory — Mutable Text](stone.md#mutable-text-concatenation)), which leaves `concat` results unstoned with excess capacity so that subsequent `s = s + x` can append in-place.
The invariant is: **an unstoned heap text is uniquely referenced by exactly one slot.** This pass ensures that whenever a text value is copied or shared (via move, store, push, function argument, closure write, etc.), it is stoned first.
**Algorithm:**
1. **Compute liveness.** Build `first_ref[slot]` and `last_ref[slot]` arrays by scanning all instructions. Extend live ranges for backward jumps (loops): if a backward jump targets label L at position `lpos`, every slot referenced between `lpos` and the jump has its `last_ref` extended to the jump position.
2. **Forward walk with type tracking.** Walk instructions using `track_types` to maintain per-slot types. At each escape point, if the escaping slot is provably `T_TEXT`, insert `stone_text slot` before the instruction.
3. **Move special case.** For `move dest, src`: only insert `stone_text src` if the source is `T_TEXT` **and** `last_ref[src] > i` (the source slot is still live after the move, meaning both slots alias the same text). If the source is dead after the move, the value transfers uniquely — no stoning needed.
**Escape points and the slot that gets stoned:**
| Instruction | Stoned slot | Why it escapes |
|---|---|---|
| `move` | source (if still live) | Two slots alias the same value |
| `store_field` | value | Stored to object property |
| `store_index` | value | Stored to array element |
| `store_dynamic` | value | Dynamic property store |
| `push` | value | Pushed to array |
| `setarg` | value | Passed as function argument |
| `put` | source | Written to outer closure frame |
**Not handled by this pass** (handled by VM guards instead):
| Instruction | Reason |
|---|---|
| `get` (closure read) | Value arrives from outer frame; type may be T_UNKNOWN at compile time |
| `return` | Return value's type may be T_UNKNOWN; VM stones at inter-frame boundary |
These two cases use runtime `stone_mutable_text` guards in the VM because the streamline pass cannot always prove the slot type across frame boundaries.
**Nop prefix:** none (inserts instructions, does not create nops)
### 8. eliminate_unreachable (dead code after return)
Nops instructions after `return` until the next real label. Only `return` is treated as a terminal instruction; `disrupt` is not, because the disruption handler code immediately follows `disrupt` and must remain reachable.
@@ -172,13 +209,13 @@ The mcode compiler emits a label at disruption handler entry points (see `emit_l
**Nop prefix:** `_nop_ur_`
### 8. eliminate_dead_jumps (jump-to-next-label elimination)
### 9. eliminate_dead_jumps (jump-to-next-label elimination)
Removes `jump L` instructions where `L` is the immediately following label (skipping over any intervening nop strings). These are common after other passes eliminate conditional branches, leaving behind jumps that fall through naturally.
**Nop prefix:** `_nop_dj_`
### 9. diagnose_function (compile-time diagnostics)
### 10. diagnose_function (compile-time diagnostics)
Optional pass that runs when `_warn` is set on the mcode input. Performs a forward type-tracking scan and emits diagnostics for provably wrong operations. Diagnostics are collected in `ir._diagnostics` as `{severity, file, line, col, message}` records.
@@ -219,6 +256,7 @@ eliminate_type_checks → uses param_types + write_types
simplify_algebra
simplify_booleans
eliminate_moves
insert_stone_text → escape analysis for mutable text
eliminate_unreachable
eliminate_dead_jumps
diagnose_function → optional, when _warn is set
@@ -286,7 +324,9 @@ move 2, 7 // i = temp
subtract 2, 2, 6 // i = i - 1 (direct)
```
The `+` operator is excluded from target slot propagation when it would use the full text+num dispatch (i.e., when neither operand is a known number), because writing both `concat` and `add` to the variable's slot would pollute its write type. When the known-number shortcut applies, `+` uses `emit_numeric_binop` and would be safe for target propagation, but this is not currently implemented — the exclusion is by operator kind, not by dispatch path.
The `+` operator uses target slot propagation when the target slot equals the left operand (`target == left_slot`), i.e. for self-assign patterns like `s = s + x`. In this case both `concat` and `add` write to the same slot that already holds the left operand, so write-type pollution is acceptable — the value is being updated in place. For other cases (target differs from left operand), `+` still allocates a temp to avoid polluting the target slot's write type with both T_TEXT and T_NUM.
This enables the VM's in-place append fast path for string concatenation: when `concat dest, dest, src` has the same destination and left operand, the VM can append directly to a mutable text's excess capacity without allocating.
## Debugging Tools
@@ -375,7 +415,7 @@ This was implemented and tested but causes a bootstrap failure during self-hosti
### Target Slot Propagation for Add with Known Numbers
When the known-number add shortcut applies (one operand is a literal number), the generated code uses `emit_numeric_binop` which has a single write path. Target slot propagation should be safe in this case, but is currently blocked by the blanket `kind != "+"` exclusion. Refining the exclusion to check whether the shortcut will apply (by testing `is_known_number` on either operand) would enable direct writes for patterns like `i = i + 1`.
When the known-number add shortcut applies (one operand is a literal number), the generated code uses `emit_numeric_binop` which has a single write path. Target slot propagation is already enabled for the self-assign case (`i = i + 1`), but when the target differs from the left operand and neither operand is a known number, a temp is still used. Refining the exclusion to check `is_known_number` would enable direct writes for the remaining non-self-assign cases like `j = i + 1`.
### Forward Type Narrowing from Typed Operations

View File

@@ -13,14 +13,33 @@ var os = use('internal/os')
var link = use('link')
// These come from env (via core_extras in engine.cm):
// analyze, run_ast_fn, core_json, use_cache, shop_path, actor_api, runtime_env,
// content_hash, cache_path, ensure_build_dir
// analyze, run_ast_fn, core_json, use_cache, core_path, shop_path, actor_api,
// runtime_env, content_hash, cache_path, ensure_build_dir
var shop_json = core_json
var global_shop_path = shop_path
var my$_ = actor_api
var core = "core"
// Compiler fingerprint: hash of all compiler source files so that any compiler
// change invalidates the entire build cache. Folded into hash_path().
var compiler_fingerprint = (function() {
var files = [
"tokenize", "parse", "fold", "mcode", "streamline",
"qbe", "qbe_emit", "ir_stats"
]
var combined = ""
var i = 0
var path = null
while (i < length(files)) {
path = core_path + '/' + files[i] + '.cm'
if (fd.is_file(path))
combined = combined + text(fd.slurp(path))
i = i + 1
}
return content_hash(stone(blob(combined)))
})()
// Make a package name safe for use in C identifiers.
// Replaces /, ., -, @ with _ so the result is a valid C identifier fragment.
function safe_c_name(name) {
@@ -43,7 +62,7 @@ function put_into_cache(content, obj)
function hash_path(content, salt)
{
var s = salt || 'mach'
return global_shop_path + '/build/' + content_hash(stone(blob(text(content) + '\n' + s)))
return global_shop_path + '/build/' + content_hash(stone(blob(text(content) + '\n' + s + '\n' + compiler_fingerprint)))
}
var Shop = {}

851
mcode.cm

File diff suppressed because it is too large Load Diff

View File

@@ -1627,8 +1627,10 @@ var parse = function(tokens, src, filename, tokenizer) {
if (r.v != null) {
left_node.level = r.level
left_node.function_nr = r.def_function_nr
r.v.nr_uses = r.v.nr_uses + 1
if (r.level > 0) r.v.closure = 1
if (r.level > 0) {
r.v.nr_uses = r.v.nr_uses + 1
r.v.closure = 1
}
} else {
left_node.level = -1
}
@@ -1720,8 +1722,10 @@ var parse = function(tokens, src, filename, tokenizer) {
if (r.v != null) {
operand.level = r.level
operand.function_nr = r.def_function_nr
r.v.nr_uses = r.v.nr_uses + 1
if (r.level > 0) r.v.closure = 1
if (r.level > 0) {
r.v.nr_uses = r.v.nr_uses + 1
r.v.closure = 1
}
} else {
operand.level = -1
}

303
slots.ce Normal file
View File

@@ -0,0 +1,303 @@
// slots.ce — slot data flow / use-def chains
//
// Usage:
// cell slots --fn <N|name> <file> Slot summary for function
// cell slots --slot <N> --fn <N|name> <file> Trace slot N in function
// cell slots <file> Slot summary for all functions
var shop = use("internal/shop")
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var fmt_val = function(v) {
if (is_null(v)) return "null"
if (is_number(v)) return text(v)
if (is_text(v)) return `"${v}"`
if (is_object(v)) return text(v)
if (is_logical(v)) return v ? "true" : "false"
return text(v)
}
// DEF/USE functions — populated from streamline's log hooks
var sl_get_defs = null
var sl_get_uses = null
var run = function() {
var filename = null
var fn_filter = null
var slot_filter = null
var i = 0
var compiled = null
var type_info = {}
var sl_log = null
var td = null
var main_name = null
var fi = 0
var func = null
var fname = null
while (i < length(args)) {
if (args[i] == '--fn') {
i = i + 1
fn_filter = args[i]
} else if (args[i] == '--slot') {
i = i + 1
slot_filter = number(args[i])
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell slots [--fn <N|name>] [--slot <N>] <file>")
log.console("")
log.console(" --fn <N|name> Filter to function by index or name")
log.console(" --slot <N> Trace a specific slot")
return null
} else if (!starts_with(args[i], '-')) {
filename = args[i]
}
i = i + 1
}
if (!filename) {
log.console("Usage: cell slots [--fn <N|name>] [--slot <N>] <file>")
return null
}
compiled = shop.mcode_file(filename)
// Try to get type info from streamline
var get_type_info = function() {
var mcode_copy = shop.mcode_file(filename)
var streamline = use("streamline")
var ti = 0
sl_log = {
passes: [],
events: null,
type_deltas: [],
request_def_use: true
}
streamline(mcode_copy, sl_log)
if (sl_log.get_slot_defs != null) {
sl_get_defs = sl_log.get_slot_defs
sl_get_uses = sl_log.get_slot_uses
}
if (sl_log.type_deltas != null) {
ti = 0
while (ti < length(sl_log.type_deltas)) {
td = sl_log.type_deltas[ti]
if (td.fn != null) {
type_info[td.fn] = td.slot_types
}
ti = ti + 1
}
}
return null
} disruption {
// Type info is optional
}
get_type_info()
var fn_matches = function(index, name) {
var match = null
if (fn_filter == null) return true
if (index >= 0 && fn_filter == text(index)) return true
if (name != null) {
match = search(name, fn_filter)
if (match != null && match >= 0) return true
}
return false
}
var analyze_function = function(func, name, index) {
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var instrs = func.instructions
var defs = {}
var uses = {}
var first_def = {}
var first_def_op = {}
var events = []
var pc = 0
var ii = 0
var instr = null
var op = null
var n = 0
var def_positions = null
var use_positions = null
var di = 0
var ui = 0
var slot_num = null
var operand_val = null
var parts = null
var j = 0
var operands = null
var slot_types = null
var type_key = null
var ei = 0
var evt = null
var found = false
var line_str = null
var si = 0
var slot_key = null
var d_count = 0
var u_count = 0
var t = null
var first = null
var dead_marker = null
if (instrs == null) instrs = []
// Walk instructions, build def/use chains
ii = 0
while (ii < length(instrs)) {
instr = instrs[ii]
if (is_text(instr)) {
ii = ii + 1
continue
}
if (!is_array(instr)) {
ii = ii + 1
continue
}
op = instr[0]
n = length(instr)
def_positions = sl_get_defs(instr)
use_positions = sl_get_uses(instr)
di = 0
while (di < length(def_positions)) {
operand_val = instr[def_positions[di]]
if (is_number(operand_val)) {
slot_num = text(operand_val)
if (!defs[slot_num]) defs[slot_num] = 0
defs[slot_num] = defs[slot_num] + 1
if (first_def[slot_num] == null) {
first_def[slot_num] = pc
first_def_op[slot_num] = op
}
push(events, {kind: "DEF", slot: operand_val, pc: pc, instr: instr})
}
di = di + 1
}
ui = 0
while (ui < length(use_positions)) {
operand_val = instr[use_positions[ui]]
if (is_number(operand_val)) {
slot_num = text(operand_val)
if (!uses[slot_num]) uses[slot_num] = 0
uses[slot_num] = uses[slot_num] + 1
push(events, {kind: "USE", slot: operand_val, pc: pc, instr: instr})
}
ui = ui + 1
}
pc = pc + 1
ii = ii + 1
}
// Get type info for this function
type_key = func.name != null ? func.name : name
if (type_info[type_key]) {
slot_types = type_info[type_key]
}
// --slot mode: show trace
if (slot_filter != null) {
log.compile(`\n=== slot ${text(slot_filter)} in ${name} ===`)
ei = 0
found = false
while (ei < length(events)) {
evt = events[ei]
if (evt.slot == slot_filter) {
found = true
n = length(evt.instr)
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(evt.instr[j]))
j = j + 1
}
operands = text(parts, ", ")
line_str = evt.instr[n - 2] != null ? `:${text(evt.instr[n - 2])}` : ""
log.compile(` ${pad_right(evt.kind, 5)}pc ${pad_right(text(evt.pc) + ":", 6)} ${pad_right(evt.instr[0], 15)}${pad_right(operands, 30)}${line_str}`)
}
ei = ei + 1
}
if (!found) {
log.compile(" (no activity)")
}
return null
}
// Summary mode
log.compile(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`)
log.compile(` ${pad_right("slot", 8)}${pad_right("defs", 8)}${pad_right("uses", 8)}${pad_right("type", 12)}first-def`)
si = 0
while (si < nr_slots) {
slot_key = text(si)
d_count = defs[slot_key] != null ? defs[slot_key] : 0
u_count = uses[slot_key] != null ? uses[slot_key] : 0
// Skip slots with no activity unless they're args or have type info
if (d_count == 0 && u_count == 0 && si >= nr_args + 1) {
si = si + 1
continue
}
t = "-"
if (slot_types != null && slot_types[slot_key] != null) {
t = slot_types[slot_key]
}
first = ""
if (si == 0) {
first = "(this)"
} else if (si > 0 && si <= nr_args) {
first = `(arg ${text(si - 1)})`
} else if (first_def[slot_key] != null) {
first = `pc ${text(first_def[slot_key])}: ${first_def_op[slot_key]}`
}
dead_marker = ""
if (d_count > 0 && u_count == 0 && si > nr_args) {
dead_marker = " <- dead"
}
log.compile(` ${pad_right("s" + slot_key, 8)}${pad_right(text(d_count), 8)}${pad_right(text(u_count), 8)}${pad_right(t, 12)}${first}${dead_marker}`)
si = si + 1
}
return null
}
// Process functions
main_name = compiled.name != null ? compiled.name : "<main>"
if (compiled.main != null) {
if (fn_matches(-1, main_name)) {
analyze_function(compiled.main, main_name, -1)
}
}
if (compiled.functions != null) {
fi = 0
while (fi < length(compiled.functions)) {
func = compiled.functions[fi]
fname = func.name != null ? func.name : "<anonymous>"
if (fn_matches(fi, fname)) {
analyze_function(func, `[${text(fi)}] ${fname}`, fi)
}
fi = fi + 1
}
}
return null
}
run()
$stop()

View File

@@ -212,34 +212,7 @@ typedef enum MachOpcode {
/* Text */
MACH_CONCAT, /* R(A) = R(B) ++ R(C) — string concatenation */
/* Typed integer comparisons (ABC) */
MACH_EQ_INT, /* R(A) = (R(B) == R(C)) — int */
MACH_NE_INT, /* R(A) = (R(B) != R(C)) — int */
MACH_LT_INT, /* R(A) = (R(B) < R(C)) — int */
MACH_LE_INT, /* R(A) = (R(B) <= R(C)) — int */
MACH_GT_INT, /* R(A) = (R(B) > R(C)) — int */
MACH_GE_INT, /* R(A) = (R(B) >= R(C)) — int */
/* Typed float comparisons (ABC) */
MACH_EQ_FLOAT, /* R(A) = (R(B) == R(C)) — float */
MACH_NE_FLOAT, /* R(A) = (R(B) != R(C)) — float */
MACH_LT_FLOAT, /* R(A) = (R(B) < R(C)) — float */
MACH_LE_FLOAT, /* R(A) = (R(B) <= R(C)) — float */
MACH_GT_FLOAT, /* R(A) = (R(B) > R(C)) — float */
MACH_GE_FLOAT, /* R(A) = (R(B) >= R(C)) — float */
/* Typed text comparisons (ABC) */
MACH_EQ_TEXT, /* R(A) = (R(B) == R(C)) — text */
MACH_NE_TEXT, /* R(A) = (R(B) != R(C)) — text */
MACH_LT_TEXT, /* R(A) = (R(B) < R(C)) — text */
MACH_LE_TEXT, /* R(A) = (R(B) <= R(C)) — text */
MACH_GT_TEXT, /* R(A) = (R(B) > R(C)) — text */
MACH_GE_TEXT, /* R(A) = (R(B) >= R(C)) — text */
/* Typed bool comparisons (ABC) */
MACH_EQ_BOOL, /* R(A) = (R(B) == R(C)) — bool */
MACH_NE_BOOL, /* R(A) = (R(B) != R(C)) — bool */
MACH_STONE_TEXT, /* stone(R(A)) — freeze mutable text before escape */
/* Special comparisons */
MACH_IS_IDENTICAL, /* R(A) = (R(B) === R(C)) — identity check (ABC) */
@@ -296,6 +269,18 @@ typedef enum MachOpcode {
MACH_IS_STONE, /* R(A) = is_stone(R(B)) */
MACH_LENGTH, /* R(A) = length(R(B)) — array/text/blob length */
MACH_IS_PROXY, /* R(A) = is_function(R(B)) && R(B).length == 2 */
MACH_IS_BLOB, /* R(A) = is_blob(R(B)) */
MACH_IS_DATA, /* R(A) = is_data(R(B)) — plain record, not array/func/blob */
MACH_IS_TRUE, /* R(A) = (R(B) === true) */
MACH_IS_FALSE, /* R(A) = (R(B) === false) */
MACH_IS_FIT, /* R(A) = is_fit(R(B)) — safe integer */
MACH_IS_CHAR, /* R(A) = is_character(R(B)) — single char text */
MACH_IS_DIGIT, /* R(A) = is_digit(R(B)) */
MACH_IS_LETTER, /* R(A) = is_letter(R(B)) */
MACH_IS_LOWER, /* R(A) = is_lower(R(B)) */
MACH_IS_UPPER, /* R(A) = is_upper(R(B)) */
MACH_IS_WS, /* R(A) = is_whitespace(R(B)) */
MACH_IS_ACTOR, /* R(A) = is_actor(R(B)) — has actor_sym property */
MACH_OP_COUNT
} MachOpcode;
@@ -372,26 +357,7 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = {
[MACH_NOP] = "nop",
/* Mcode-derived */
[MACH_CONCAT] = "concat",
[MACH_EQ_INT] = "eq_int",
[MACH_NE_INT] = "ne_int",
[MACH_LT_INT] = "lt_int",
[MACH_LE_INT] = "le_int",
[MACH_GT_INT] = "gt_int",
[MACH_GE_INT] = "ge_int",
[MACH_EQ_FLOAT] = "eq_float",
[MACH_NE_FLOAT] = "ne_float",
[MACH_LT_FLOAT] = "lt_float",
[MACH_LE_FLOAT] = "le_float",
[MACH_GT_FLOAT] = "gt_float",
[MACH_GE_FLOAT] = "ge_float",
[MACH_EQ_TEXT] = "eq_text",
[MACH_NE_TEXT] = "ne_text",
[MACH_LT_TEXT] = "lt_text",
[MACH_LE_TEXT] = "le_text",
[MACH_GT_TEXT] = "gt_text",
[MACH_GE_TEXT] = "ge_text",
[MACH_EQ_BOOL] = "eq_bool",
[MACH_NE_BOOL] = "ne_bool",
[MACH_STONE_TEXT] = "stone_text",
[MACH_IS_IDENTICAL] = "is_identical",
[MACH_IS_INT] = "is_int",
[MACH_IS_NUM] = "is_num",
@@ -427,6 +393,18 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = {
[MACH_IS_STONE] = "is_stone",
[MACH_LENGTH] = "length",
[MACH_IS_PROXY] = "is_proxy",
[MACH_IS_BLOB] = "is_blob",
[MACH_IS_DATA] = "is_data",
[MACH_IS_TRUE] = "is_true",
[MACH_IS_FALSE] = "is_false",
[MACH_IS_FIT] = "is_fit",
[MACH_IS_CHAR] = "is_char",
[MACH_IS_DIGIT] = "is_digit",
[MACH_IS_LETTER] = "is_letter",
[MACH_IS_LOWER] = "is_lower",
[MACH_IS_UPPER] = "is_upper",
[MACH_IS_WS] = "is_ws",
[MACH_IS_ACTOR] = "is_actor",
};
/* ---- Compile-time constant pool entry ---- */
@@ -1080,10 +1058,6 @@ static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) {
}
}
/* String concat for ADD */
if (op == MACH_ADD && mist_is_text(a) && mist_is_text(b))
return JS_ConcatString(ctx, a, b);
/* Comparison ops allow mixed types — return false for mismatches */
if (op >= MACH_EQ && op <= MACH_GE) {
/* Fast path: identical values (chase pointers for forwarded objects) */
@@ -1142,7 +1116,10 @@ static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) {
default: break;
}
}
/* Different types: EQ→false, NEQ→true, others→false */
/* Different types for ordering comparisons: disrupt */
if (op >= MACH_LT && op <= MACH_GE)
return JS_RaiseDisrupt(ctx, "cannot compare: operands must be same type");
/* EQ/NEQ with different types: false/true */
if (op == MACH_NEQ) return JS_NewBool(ctx, 1);
return JS_NewBool(ctx, 0);
}
@@ -1422,17 +1399,7 @@ vm_dispatch:
DT(MACH_HASPROP), DT(MACH_REGEXP),
DT(MACH_EQ_TOL), DT(MACH_NEQ_TOL),
DT(MACH_NOP),
DT(MACH_CONCAT),
DT(MACH_EQ_INT), DT(MACH_NE_INT),
DT(MACH_LT_INT), DT(MACH_LE_INT),
DT(MACH_GT_INT), DT(MACH_GE_INT),
DT(MACH_EQ_FLOAT), DT(MACH_NE_FLOAT),
DT(MACH_LT_FLOAT), DT(MACH_LE_FLOAT),
DT(MACH_GT_FLOAT), DT(MACH_GE_FLOAT),
DT(MACH_EQ_TEXT), DT(MACH_NE_TEXT),
DT(MACH_LT_TEXT), DT(MACH_LE_TEXT),
DT(MACH_GT_TEXT), DT(MACH_GE_TEXT),
DT(MACH_EQ_BOOL), DT(MACH_NE_BOOL),
DT(MACH_CONCAT), DT(MACH_STONE_TEXT),
DT(MACH_IS_IDENTICAL),
DT(MACH_IS_INT), DT(MACH_IS_NUM),
DT(MACH_IS_TEXT), DT(MACH_IS_BOOL),
@@ -1453,6 +1420,12 @@ vm_dispatch:
DT(MACH_IS_ARRAY), DT(MACH_IS_FUNC),
DT(MACH_IS_RECORD), DT(MACH_IS_STONE),
DT(MACH_LENGTH), DT(MACH_IS_PROXY),
DT(MACH_IS_BLOB), DT(MACH_IS_DATA),
DT(MACH_IS_TRUE), DT(MACH_IS_FALSE),
DT(MACH_IS_FIT), DT(MACH_IS_CHAR),
DT(MACH_IS_DIGIT), DT(MACH_IS_LETTER),
DT(MACH_IS_LOWER), DT(MACH_IS_UPPER),
DT(MACH_IS_WS), DT(MACH_IS_ACTOR),
};
#pragma GCC diagnostic pop
#undef DT
@@ -2062,6 +2035,7 @@ vm_dispatch:
}
target = next;
}
stone_mutable_text(target->slots[c]);
frame->slots[a] = target->slots[c];
VM_BREAK();
}
@@ -2171,6 +2145,7 @@ vm_dispatch:
}
VM_CASE(MACH_RETURN):
stone_mutable_text(frame->slots[a]);
result = frame->slots[a];
if (!JS_IsPtr(frame->caller)) goto done;
{
@@ -2338,81 +2313,46 @@ vm_dispatch:
/* === New mcode-derived opcodes === */
/* Text concatenation */
/* Text concatenation — with in-place append fast path for s = s + x */
VM_CASE(MACH_CONCAT): {
JSValue res = JS_ConcatString(ctx, frame->slots[b], frame->slots[c]);
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
if (JS_IsException(res)) goto disrupt;
frame->slots[a] = res;
VM_BREAK();
}
/* Typed integer comparisons */
VM_CASE(MACH_EQ_INT):
frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) == JS_VALUE_GET_INT(frame->slots[c]));
VM_BREAK();
VM_CASE(MACH_NE_INT):
frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) != JS_VALUE_GET_INT(frame->slots[c]));
VM_BREAK();
VM_CASE(MACH_LT_INT):
frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) < JS_VALUE_GET_INT(frame->slots[c]));
VM_BREAK();
VM_CASE(MACH_LE_INT):
frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) <= JS_VALUE_GET_INT(frame->slots[c]));
VM_BREAK();
VM_CASE(MACH_GT_INT):
frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) > JS_VALUE_GET_INT(frame->slots[c]));
VM_BREAK();
VM_CASE(MACH_GE_INT):
frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) >= JS_VALUE_GET_INT(frame->slots[c]));
VM_BREAK();
/* Typed float comparisons */
VM_CASE(MACH_EQ_FLOAT): VM_CASE(MACH_NE_FLOAT):
VM_CASE(MACH_LT_FLOAT): VM_CASE(MACH_LE_FLOAT):
VM_CASE(MACH_GT_FLOAT): VM_CASE(MACH_GE_FLOAT): {
double da, db;
JS_ToFloat64(ctx, &da, frame->slots[b]);
JS_ToFloat64(ctx, &db, frame->slots[c]);
int r;
switch (op) {
case MACH_EQ_FLOAT: r = (da == db); break;
case MACH_NE_FLOAT: r = (da != db); break;
case MACH_LT_FLOAT: r = (da < db); break;
case MACH_LE_FLOAT: r = (da <= db); break;
case MACH_GT_FLOAT: r = (da > db); break;
case MACH_GE_FLOAT: r = (da >= db); break;
default: r = 0; break;
if (a == b) {
/* Self-assign pattern: slot[a] = slot[a] + slot[c] */
JSValue left = frame->slots[a];
JSValue right = frame->slots[c];
/* Inline fast path: mutable heap text with enough capacity */
if (JS_IsPtr(left)) {
JSText *s = (JSText *)chase(left);
int slen = (int)s->length;
int rlen = js_string_value_len(right);
int cap = (int)objhdr_cap56(s->hdr);
if (objhdr_type(s->hdr) == OBJ_TEXT
&& !(s->hdr & OBJHDR_S_MASK)
&& slen + rlen <= cap) {
/* Append in-place — zero allocation, no GC possible */
for (int i = 0; i < rlen; i++)
string_put(s, slen + i, js_string_value_get(right, i));
s->length = slen + rlen;
VM_BREAK();
}
}
/* Slow path: allocate with growth factor, leave unstoned */
JSValue res = JS_ConcatStringGrow(ctx, frame->slots[b], frame->slots[c]);
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
if (JS_IsException(res)) goto disrupt;
frame->slots[a] = res;
} else {
/* Different target: use existing exact-fit stoned path */
JSValue res = JS_ConcatString(ctx, frame->slots[b], frame->slots[c]);
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
if (JS_IsException(res)) goto disrupt;
frame->slots[a] = res;
}
frame->slots[a] = JS_NewBool(ctx, r);
VM_BREAK();
}
/* Typed text comparisons */
VM_CASE(MACH_EQ_TEXT): VM_CASE(MACH_NE_TEXT):
VM_CASE(MACH_LT_TEXT): VM_CASE(MACH_LE_TEXT):
VM_CASE(MACH_GT_TEXT): VM_CASE(MACH_GE_TEXT): {
int cmp = js_string_compare_value(ctx, frame->slots[b], frame->slots[c], FALSE);
int r;
switch (op) {
case MACH_EQ_TEXT: r = (cmp == 0); break;
case MACH_NE_TEXT: r = (cmp != 0); break;
case MACH_LT_TEXT: r = (cmp < 0); break;
case MACH_LE_TEXT: r = (cmp <= 0); break;
case MACH_GT_TEXT: r = (cmp > 0); break;
case MACH_GE_TEXT: r = (cmp >= 0); break;
default: r = 0; break;
}
frame->slots[a] = JS_NewBool(ctx, r);
VM_BREAK();
}
/* Typed bool comparisons */
VM_CASE(MACH_EQ_BOOL):
frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_BOOL(frame->slots[b]) == JS_VALUE_GET_BOOL(frame->slots[c]));
VM_BREAK();
VM_CASE(MACH_NE_BOOL):
frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_BOOL(frame->slots[b]) != JS_VALUE_GET_BOOL(frame->slots[c]));
/* Stone mutable text compiler-emitted at escape points */
VM_CASE(MACH_STONE_TEXT):
stone_mutable_text(frame->slots[a]);
VM_BREAK();
/* Identity check */
@@ -2476,6 +2416,123 @@ vm_dispatch:
frame->slots[a] = JS_NewBool(ctx, is_proxy);
VM_BREAK();
}
VM_CASE(MACH_IS_BLOB):
frame->slots[a] = JS_NewBool(ctx, mist_is_blob(frame->slots[b]));
VM_BREAK();
VM_CASE(MACH_IS_DATA): {
JSValue v = frame->slots[b];
int result = 0;
if (mist_is_gc_object(v) && !mist_is_array(v)
&& !mist_is_function(v) && !mist_is_blob(v))
result = 1;
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
VM_CASE(MACH_IS_TRUE):
frame->slots[a] = JS_NewBool(ctx, frame->slots[b] == JS_TRUE);
VM_BREAK();
VM_CASE(MACH_IS_FALSE):
frame->slots[a] = JS_NewBool(ctx, frame->slots[b] == JS_FALSE);
VM_BREAK();
VM_CASE(MACH_IS_FIT): {
JSValue v = frame->slots[b];
int result = 0;
if (JS_IsInt(v)) {
result = 1;
} else if (JS_IsShortFloat(v)) {
double d = JS_VALUE_GET_FLOAT64(v);
result = (isfinite(d) && trunc(d) == d && fabs(d) <= 9007199254740992.0);
}
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
VM_CASE(MACH_IS_CHAR): {
JSValue v = frame->slots[b];
int result = 0;
if (MIST_IsImmediateASCII(v))
result = (MIST_GetImmediateASCIILen(v) == 1);
else if (mist_is_text(v))
result = (js_string_value_len(v) == 1);
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
VM_CASE(MACH_IS_DIGIT): {
JSValue v = frame->slots[b];
int result = 0;
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
int ch = MIST_GetImmediateASCIIChar(v, 0);
result = (ch >= '0' && ch <= '9');
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
uint32_t ch = js_string_value_get(v, 0);
result = (ch >= '0' && ch <= '9');
}
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
VM_CASE(MACH_IS_LETTER): {
JSValue v = frame->slots[b];
int result = 0;
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
int ch = MIST_GetImmediateASCIIChar(v, 0);
result = ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'));
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
uint32_t ch = js_string_value_get(v, 0);
result = ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z'));
}
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
VM_CASE(MACH_IS_LOWER): {
JSValue v = frame->slots[b];
int result = 0;
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
int ch = MIST_GetImmediateASCIIChar(v, 0);
result = (ch >= 'a' && ch <= 'z');
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
uint32_t ch = js_string_value_get(v, 0);
result = (ch >= 'a' && ch <= 'z');
}
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
VM_CASE(MACH_IS_UPPER): {
JSValue v = frame->slots[b];
int result = 0;
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
int ch = MIST_GetImmediateASCIIChar(v, 0);
result = (ch >= 'A' && ch <= 'Z');
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
uint32_t ch = js_string_value_get(v, 0);
result = (ch >= 'A' && ch <= 'Z');
}
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
VM_CASE(MACH_IS_WS): {
JSValue v = frame->slots[b];
int result = 0;
if (MIST_IsImmediateASCII(v) && MIST_GetImmediateASCIILen(v) == 1) {
int ch = MIST_GetImmediateASCIIChar(v, 0);
result = (ch == ' ' || ch == '\t' || ch == '\n'
|| ch == '\r' || ch == '\f' || ch == '\v');
} else if (mist_is_text(v) && js_string_value_len(v) == 1) {
uint32_t ch = js_string_value_get(v, 0);
result = (ch == ' ' || ch == '\t' || ch == '\n'
|| ch == '\r' || ch == '\f' || ch == '\v');
}
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
VM_CASE(MACH_IS_ACTOR): {
JSValue v = frame->slots[b];
int result = 0;
if (mist_is_record(v) && !JS_IsNull(ctx->actor_sym)) {
result = JS_HasPropertyKey(ctx, v, ctx->actor_sym) > 0;
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
}
frame->slots[a] = JS_NewBool(ctx, result);
VM_BREAK();
}
/* Logical */
VM_CASE(MACH_NOT): {
int bval = JS_ToBool(ctx, frame->slots[b]);
@@ -2622,7 +2679,15 @@ vm_dispatch:
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
goto disrupt;
}
int nr = c + 2; /* argc + this + func overhead */
JSFunction *fn = JS_VALUE_GET_FUNCTION(func_val);
int nr;
if (fn->kind == JS_FUNC_KIND_REGISTER) {
JSCodeRegister *fn_code = JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code;
nr = fn_code->nr_slots;
if (nr < c + 2) nr = c + 2; /* safety: never smaller than argc+2 */
} else {
nr = c + 2;
}
JSFrameRegister *call_frame = alloc_frame_register(ctx, nr);
if (!call_frame) {
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
@@ -2631,6 +2696,7 @@ vm_dispatch:
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
func_val = frame->slots[b]; /* re-read after GC */
call_frame->function = func_val;
call_frame->address = JS_NewInt32(ctx, c); /* store actual argc */
frame->slots[a] = JS_MKPTR(call_frame);
VM_BREAK();
}
@@ -2643,36 +2709,19 @@ vm_dispatch:
VM_CASE(MACH_INVOKE): {
/* A=frame_slot, B=result_slot */
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
int nr = (int)objhdr_cap56(fr->header);
int c_argc = (nr >= 2) ? nr - 2 : 0;
int c_argc = JS_VALUE_GET_INT(fr->address); /* actual argc stored by FRAME */
JSValue fn_val = fr->function;
JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val);
if (!mach_check_call_arity(ctx, fn, c_argc))
goto disrupt;
if (fn->kind == JS_FUNC_KIND_REGISTER) {
/* Register function: switch frames inline (fast path) */
JSCodeRegister *fn_code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code;
JSFrameRegister *new_frame = alloc_frame_register(ctx, fn_code->nr_slots);
if (!new_frame) {
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
goto disrupt;
}
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
fn_val = fr->function;
fn = JS_VALUE_GET_FUNCTION(fn_val);
fn_code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code;
new_frame->function = fn_val;
/* Copy this + args from call frame to new frame */
int copy_count = (c_argc < fn_code->arity) ? c_argc : fn_code->arity;
new_frame->slots[0] = fr->slots[0]; /* this */
for (int i = 0; i < copy_count; i++)
new_frame->slots[1 + i] = fr->slots[1 + i];
/* Register function: FRAME already allocated nr_slots — just switch */
JSCodeRegister *fn_code = JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code;
/* Save return info */
frame->address = JS_NewInt32(ctx, (pc << 16) | b);
new_frame->caller = JS_MKPTR(frame);
frame = new_frame;
fr->caller = JS_MKPTR(frame);
frame = fr;
frame_ref.val = JS_MKPTR(frame);
code = fn_code;
env = fn->u.cell.env_record;
@@ -2716,8 +2765,7 @@ vm_dispatch:
VM_CASE(MACH_GOINVOKE): {
/* Tail call: replace current frame with callee */
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
int nr = (int)objhdr_cap56(fr->header);
int c_argc = (nr >= 2) ? nr - 2 : 0;
int c_argc = JS_VALUE_GET_INT(fr->address); /* actual argc stored by FRAME */
JSValue fn_val = fr->function;
JSFunction *fn = JS_VALUE_GET_FUNCTION(fn_val);
if (!mach_check_call_arity(ctx, fn, c_argc))
@@ -2742,25 +2790,10 @@ vm_dispatch:
env = fn->u.cell.env_record;
pc = code->entry_point;
} else {
/* SLOW PATH: callee needs more slots, must allocate */
JSFrameRegister *new_frame = alloc_frame_register(ctx, fn_code->nr_slots);
if (!new_frame) {
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
goto disrupt;
}
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->slots[a]);
fn_val = fr->function;
fn = JS_VALUE_GET_FUNCTION(fn_val);
fn_code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code;
new_frame->function = fn_val;
int copy_count = (c_argc < fn_code->arity) ? c_argc : fn_code->arity;
new_frame->slots[0] = fr->slots[0]; /* this */
for (int i = 0; i < copy_count; i++)
new_frame->slots[1 + i] = fr->slots[1 + i];
new_frame->caller = frame->caller;
/* SLOW PATH: GOFRAME already allocated nr_slots — use fr directly */
fr->caller = frame->caller;
frame->caller = JS_NULL;
frame = new_frame;
frame = fr;
frame_ref.val = JS_MKPTR(frame);
code = fn_code;
env = fn->u.cell.env_record;
@@ -3014,10 +3047,10 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
if (s.nr_slots > 255) {
cJSON *nm_chk = cJSON_GetObjectItemCaseSensitive(fobj, "name");
const char *fn_name = nm_chk ? cJSON_GetStringValue(nm_chk) : "<anonymous>";
fprintf(stderr, "ERROR: function '%s' has %d slots (max 255). "
fprintf(stderr, "FATAL: function '%s' has %d slots (max 255). "
"Ensure the streamline optimizer ran before mach compilation.\n",
fn_name, s.nr_slots);
return NULL;
abort();
}
int dis_raw = (int)cJSON_GetNumberValue(
cJSON_GetObjectItemCaseSensitive(fobj, "disruption_pc"));
@@ -3084,6 +3117,7 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
else if (strcmp(op, "move") == 0) { AB2(MACH_MOVE); }
/* Text */
else if (strcmp(op, "concat") == 0) { ABC3(MACH_CONCAT); }
else if (strcmp(op, "stone_text") == 0) { EM(MACH_ABC(MACH_STONE_TEXT, A1, 0, 0)); }
/* Generic arithmetic */
else if (strcmp(op, "add") == 0) { ABC3(MACH_ADD); }
else if (strcmp(op, "subtract") == 0) { ABC3(MACH_SUB); }
@@ -3103,30 +3137,13 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
else if (strcmp(op, "ceiling") == 0) { ABC3(MACH_CEILING); }
else if (strcmp(op, "round") == 0) { ABC3(MACH_ROUND); }
else if (strcmp(op, "trunc") == 0) { ABC3(MACH_TRUNC); }
/* Typed integer comparisons */
else if (strcmp(op, "eq_int") == 0) { ABC3(MACH_EQ_INT); }
else if (strcmp(op, "ne_int") == 0) { ABC3(MACH_NE_INT); }
else if (strcmp(op, "lt_int") == 0) { ABC3(MACH_LT_INT); }
else if (strcmp(op, "le_int") == 0) { ABC3(MACH_LE_INT); }
else if (strcmp(op, "gt_int") == 0) { ABC3(MACH_GT_INT); }
else if (strcmp(op, "ge_int") == 0) { ABC3(MACH_GE_INT); }
/* Typed float comparisons */
else if (strcmp(op, "eq_float") == 0) { ABC3(MACH_EQ_FLOAT); }
else if (strcmp(op, "ne_float") == 0) { ABC3(MACH_NE_FLOAT); }
else if (strcmp(op, "lt_float") == 0) { ABC3(MACH_LT_FLOAT); }
else if (strcmp(op, "le_float") == 0) { ABC3(MACH_LE_FLOAT); }
else if (strcmp(op, "gt_float") == 0) { ABC3(MACH_GT_FLOAT); }
else if (strcmp(op, "ge_float") == 0) { ABC3(MACH_GE_FLOAT); }
/* Typed text comparisons */
else if (strcmp(op, "eq_text") == 0) { ABC3(MACH_EQ_TEXT); }
else if (strcmp(op, "ne_text") == 0) { ABC3(MACH_NE_TEXT); }
else if (strcmp(op, "lt_text") == 0) { ABC3(MACH_LT_TEXT); }
else if (strcmp(op, "le_text") == 0) { ABC3(MACH_LE_TEXT); }
else if (strcmp(op, "gt_text") == 0) { ABC3(MACH_GT_TEXT); }
else if (strcmp(op, "ge_text") == 0) { ABC3(MACH_GE_TEXT); }
/* Typed bool comparisons */
else if (strcmp(op, "eq_bool") == 0) { ABC3(MACH_EQ_BOOL); }
else if (strcmp(op, "ne_bool") == 0) { ABC3(MACH_NE_BOOL); }
/* Generic comparisons */
else if (strcmp(op, "eq") == 0) { ABC3(MACH_EQ); }
else if (strcmp(op, "ne") == 0) { ABC3(MACH_NEQ); }
else if (strcmp(op, "lt") == 0) { ABC3(MACH_LT); }
else if (strcmp(op, "le") == 0) { ABC3(MACH_LE); }
else if (strcmp(op, "gt") == 0) { ABC3(MACH_GT); }
else if (strcmp(op, "ge") == 0) { ABC3(MACH_GE); }
/* Special comparisons */
else if (strcmp(op, "is_identical") == 0) { ABC3(MACH_IS_IDENTICAL); }
else if (strcmp(op, "eq_tol") == 0) {
@@ -3161,6 +3178,18 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
else if (strcmp(op, "is_stone") == 0) { AB2(MACH_IS_STONE); }
else if (strcmp(op, "length") == 0) { AB2(MACH_LENGTH); }
else if (strcmp(op, "is_proxy") == 0) { AB2(MACH_IS_PROXY); }
else if (strcmp(op, "is_blob") == 0) { AB2(MACH_IS_BLOB); }
else if (strcmp(op, "is_data") == 0) { AB2(MACH_IS_DATA); }
else if (strcmp(op, "is_true") == 0) { AB2(MACH_IS_TRUE); }
else if (strcmp(op, "is_false") == 0) { AB2(MACH_IS_FALSE); }
else if (strcmp(op, "is_fit") == 0) { AB2(MACH_IS_FIT); }
else if (strcmp(op, "is_char") == 0) { AB2(MACH_IS_CHAR); }
else if (strcmp(op, "is_digit") == 0) { AB2(MACH_IS_DIGIT); }
else if (strcmp(op, "is_letter") == 0) { AB2(MACH_IS_LETTER); }
else if (strcmp(op, "is_lower") == 0) { AB2(MACH_IS_LOWER); }
else if (strcmp(op, "is_upper") == 0) { AB2(MACH_IS_UPPER); }
else if (strcmp(op, "is_ws") == 0) { AB2(MACH_IS_WS); }
else if (strcmp(op, "is_actor") == 0) { AB2(MACH_IS_ACTOR); }
/* Logical */
else if (strcmp(op, "not") == 0) { AB2(MACH_NOT); }
else if (strcmp(op, "and") == 0) { ABC3(MACH_AND); }

View File

@@ -479,6 +479,17 @@ static inline void mach_resolve_forward(JSValue *slot) {
}
}
/* Stone a mutable (unstoned) heap text in-place. Used at escape points
in the VM to enforce the invariant that an unstoned text is uniquely
referenced by exactly one slot. */
static inline void stone_mutable_text(JSValue v) {
if (JS_IsPtr(v)) {
objhdr_t *oh = (objhdr_t *)JS_VALUE_GET_PTR(v);
if (objhdr_type(*oh) == OBJ_TEXT && !(*oh & OBJHDR_S_MASK))
*oh = objhdr_set_s(*oh, true);
}
}
/* Inline type checks — use these in the VM dispatch loop to avoid
function call overhead. The public API (JS_IsArray etc. in quickjs.h)
remains non-inline for external callers; those wrappers live in runtime.c. */
@@ -1213,6 +1224,7 @@ int JS_SetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key, JSValue va
void *js_realloc_rt (void *ptr, size_t size);
char *js_strdup_rt (const char *str);
JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2);
JSValue JS_ConcatStringGrow (JSContext *ctx, JSValue op1, JSValue op2);
JSText *pretext_init (JSContext *ctx, int capacity);
JSText *pretext_putc (JSContext *ctx, JSText *s, uint32_t c);
JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v);

View File

@@ -2910,6 +2910,84 @@ JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2) {
return ret_val;
}
/* Concat with over-allocated capacity and NO stoning.
Used by MACH_CONCAT self-assign (s = s + x) slow path so that
subsequent appends can reuse the excess capacity in-place. */
JSValue JS_ConcatStringGrow (JSContext *ctx, JSValue op1, JSValue op2) {
if (unlikely (!JS_IsText (op1))) {
JSGCRef op2_guard;
JS_PushGCRef (ctx, &op2_guard);
op2_guard.val = op2;
op1 = JS_ToString (ctx, op1);
op2 = op2_guard.val;
JS_PopGCRef (ctx, &op2_guard);
if (JS_IsException (op1)) return JS_EXCEPTION;
}
if (unlikely (!JS_IsText (op2))) {
JSGCRef op1_guard;
JS_PushGCRef (ctx, &op1_guard);
op1_guard.val = op1;
op2 = JS_ToString (ctx, op2);
op1 = op1_guard.val;
JS_PopGCRef (ctx, &op1_guard);
if (JS_IsException (op2)) return JS_EXCEPTION;
}
int len1 = js_string_value_len (op1);
int len2 = js_string_value_len (op2);
int new_len = len1 + len2;
/* Try immediate ASCII for short results */
if (new_len <= MIST_ASCII_MAX_LEN) {
char buf[8];
BOOL all_ascii = TRUE;
for (int i = 0; i < len1 && all_ascii; i++) {
uint32_t c = js_string_value_get (op1, i);
if (c >= 0x80) all_ascii = FALSE;
else buf[i] = (char)c;
}
for (int i = 0; i < len2 && all_ascii; i++) {
uint32_t c = js_string_value_get (op2, i);
if (c >= 0x80) all_ascii = FALSE;
else buf[len1 + i] = (char)c;
}
if (all_ascii) {
JSValue imm = MIST_TryNewImmediateASCII (buf, new_len);
if (!JS_IsNull (imm)) return imm;
}
}
/* Allocate with 2x growth factor, minimum 16 */
int capacity = new_len * 2;
if (capacity < 16) capacity = 16;
JSGCRef op1_ref, op2_ref;
JS_PushGCRef (ctx, &op1_ref);
op1_ref.val = op1;
JS_PushGCRef (ctx, &op2_ref);
op2_ref.val = op2;
JSText *p = js_alloc_string (ctx, capacity);
if (!p) {
JS_PopGCRef (ctx, &op2_ref);
JS_PopGCRef (ctx, &op1_ref);
return JS_EXCEPTION;
}
op1 = op1_ref.val;
op2 = op2_ref.val;
JS_PopGCRef (ctx, &op2_ref);
JS_PopGCRef (ctx, &op1_ref);
for (int i = 0; i < len1; i++)
string_put (p, i, js_string_value_get (op1, i));
for (int i = 0; i < len2; i++)
string_put (p, len1 + i, js_string_value_get (op2, i));
p->length = new_len;
/* Do NOT stone — leave mutable so in-place append can reuse capacity */
return JS_MKPTR (p);
}
/* WARNING: proto must be an object or JS_NULL */
JSValue JS_NewObjectProtoClass (JSContext *ctx, JSValue proto_val, JSClassID class_id) {
JSGCRef proto_ref;
@@ -11320,6 +11398,79 @@ static JSValue js_cell_is_letter (JSContext *ctx, JSValue this_val, int argc, JS
return JS_NewBool (ctx, (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'));
}
/* is_true(val) - check if value is exactly true */
static JSValue js_cell_is_true (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 1) return JS_FALSE;
return JS_NewBool (ctx, argv[0] == JS_TRUE);
}
/* is_false(val) - check if value is exactly false */
static JSValue js_cell_is_false (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 1) return JS_FALSE;
return JS_NewBool (ctx, argv[0] == JS_FALSE);
}
/* is_fit(val) - check if value is a safe integer (int32 or float with integer value <= 2^53) */
static JSValue js_cell_is_fit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 1) return JS_FALSE;
JSValue val = argv[0];
if (JS_IsInt (val)) return JS_TRUE;
if (JS_IsShortFloat (val)) {
double d = JS_VALUE_GET_FLOAT64 (val);
return JS_NewBool (ctx, isfinite (d) && trunc (d) == d && fabs (d) <= 9007199254740992.0);
}
return JS_FALSE;
}
/* is_character(val) - check if value is a single character text */
static JSValue js_cell_is_character (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 1) return JS_FALSE;
JSValue val = argv[0];
if (!JS_IsText (val)) return JS_FALSE;
return JS_NewBool (ctx, js_string_value_len (val) == 1);
}
/* is_digit(val) - check if value is a single digit character */
static JSValue js_cell_is_digit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 1) return JS_FALSE;
JSValue val = argv[0];
if (!JS_IsText (val)) return JS_FALSE;
if (js_string_value_len (val) != 1) return JS_FALSE;
uint32_t c = js_string_value_get (val, 0);
return JS_NewBool (ctx, c >= '0' && c <= '9');
}
/* is_lower(val) - check if value is a single lowercase letter */
static JSValue js_cell_is_lower (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 1) return JS_FALSE;
JSValue val = argv[0];
if (!JS_IsText (val)) return JS_FALSE;
if (js_string_value_len (val) != 1) return JS_FALSE;
uint32_t c = js_string_value_get (val, 0);
return JS_NewBool (ctx, c >= 'a' && c <= 'z');
}
/* is_upper(val) - check if value is a single uppercase letter */
static JSValue js_cell_is_upper (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 1) return JS_FALSE;
JSValue val = argv[0];
if (!JS_IsText (val)) return JS_FALSE;
if (js_string_value_len (val) != 1) return JS_FALSE;
uint32_t c = js_string_value_get (val, 0);
return JS_NewBool (ctx, c >= 'A' && c <= 'Z');
}
/* is_whitespace(val) - check if value is a single whitespace character */
static JSValue js_cell_is_whitespace (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 1) return JS_FALSE;
JSValue val = argv[0];
if (!JS_IsText (val)) return JS_FALSE;
if (js_string_value_len (val) != 1) return JS_FALSE;
uint32_t c = js_string_value_get (val, 0);
return JS_NewBool (ctx, c == ' ' || c == '\t' || c == '\n'
|| c == '\r' || c == '\f' || c == '\v');
}
/* is_proto(val, master) - check if val has master in prototype chain */
static JSValue js_cell_is_proto (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
if (argc < 2) return JS_FALSE;
@@ -11487,6 +11638,14 @@ static void JS_AddIntrinsicBaseObjects (JSContext *ctx) {
js_set_global_cfunc(ctx, "is_text", js_cell_is_text, 1);
js_set_global_cfunc(ctx, "is_proto", js_cell_is_proto, 2);
js_set_global_cfunc(ctx, "is_letter", js_cell_is_letter, 1);
js_set_global_cfunc(ctx, "is_true", js_cell_is_true, 1);
js_set_global_cfunc(ctx, "is_false", js_cell_is_false, 1);
js_set_global_cfunc(ctx, "is_fit", js_cell_is_fit, 1);
js_set_global_cfunc(ctx, "is_character", js_cell_is_character, 1);
js_set_global_cfunc(ctx, "is_digit", js_cell_is_digit, 1);
js_set_global_cfunc(ctx, "is_lower", js_cell_is_lower, 1);
js_set_global_cfunc(ctx, "is_upper", js_cell_is_upper, 1);
js_set_global_cfunc(ctx, "is_whitespace", js_cell_is_whitespace, 1);
/* Utility functions */
js_set_global_cfunc(ctx, "apply", js_cell_fn_apply, 2);

File diff suppressed because it is too large Load Diff

View File

@@ -103,6 +103,29 @@ run("string concatenation empty", function() {
if ("" + "world" != "world") fail("empty + string failed")
})
run("string concat does not mutate alias", function() {
var a = "hello world"
var b = a
a = a + " appended"
if (a != "hello world appended") fail("a wrong, got " + a)
if (b != "hello world") fail("b should still be hello world, got " + b)
})
run("string concat in loop preserves aliases", function() {
var a = "starting value"
var copies = [a]
var i = 0
while (i < 5) {
a = a + " more"
copies[] = a
i = i + 1
}
if (copies[0] != "starting value") fail("copies[0] wrong, got " + copies[0])
if (copies[1] != "starting value more") fail("copies[1] wrong, got " + copies[1])
if (copies[5] != "starting value more more more more more") fail("copies[5] wrong, got " + copies[5])
if (a != "starting value more more more more more") fail("a wrong, got " + a)
})
// ============================================================================
// TYPE MIXING SHOULD DISRUPT
// ============================================================================
@@ -958,6 +981,99 @@ run("is_proto", function() {
if (!is_proto(b, a)) fail("is_proto failed on meme")
})
run("is_data", function() {
if (!is_data({})) fail("is_data {} should be true")
if (is_data([])) fail("is_data [] should be false")
if (is_data(42)) fail("is_data number should be false")
if (is_data("hello")) fail("is_data string should be false")
if (is_data(null)) fail("is_data null should be false")
if (is_data(true)) fail("is_data bool should be false")
if (is_data(function(){})) fail("is_data function should be false")
})
run("is_true", function() {
if (!is_true(true)) fail("is_true true should be true")
if (is_true(false)) fail("is_true false should be false")
if (is_true(1)) fail("is_true 1 should be false")
if (is_true("true")) fail("is_true string should be false")
if (is_true(null)) fail("is_true null should be false")
})
run("is_false", function() {
if (!is_false(false)) fail("is_false false should be true")
if (is_false(true)) fail("is_false true should be false")
if (is_false(0)) fail("is_false 0 should be false")
if (is_false("")) fail("is_false empty string should be false")
if (is_false(null)) fail("is_false null should be false")
})
run("is_fit", function() {
if (!is_fit(0)) fail("is_fit 0 should be true")
if (!is_fit(42)) fail("is_fit 42 should be true")
if (!is_fit(-100)) fail("is_fit -100 should be true")
if (!is_fit(3.0)) fail("is_fit 3.0 should be true")
if (is_fit(3.5)) fail("is_fit 3.5 should be false")
if (is_fit("42")) fail("is_fit string should be false")
if (is_fit(null)) fail("is_fit null should be false")
})
run("is_character", function() {
if (!is_character("a")) fail("is_character a should be true")
if (!is_character("Z")) fail("is_character Z should be true")
if (!is_character("5")) fail("is_character 5 should be true")
if (!is_character(" ")) fail("is_character space should be true")
if (is_character("ab")) fail("is_character ab should be false")
if (is_character("")) fail("is_character empty should be false")
if (is_character(42)) fail("is_character number should be false")
if (is_character(null)) fail("is_character null should be false")
})
run("is_digit", function() {
if (!is_digit("0")) fail("is_digit 0 should be true")
if (!is_digit("5")) fail("is_digit 5 should be true")
if (!is_digit("9")) fail("is_digit 9 should be true")
if (is_digit("a")) fail("is_digit a should be false")
if (is_digit("55")) fail("is_digit 55 should be false")
if (is_digit(5)) fail("is_digit number should be false")
if (is_digit(null)) fail("is_digit null should be false")
})
run("is_letter", function() {
if (!is_letter("a")) fail("is_letter a should be true")
if (!is_letter("Z")) fail("is_letter Z should be true")
if (is_letter("5")) fail("is_letter 5 should be false")
if (is_letter("ab")) fail("is_letter ab should be false")
if (is_letter(42)) fail("is_letter number should be false")
})
run("is_lower", function() {
if (!is_lower("a")) fail("is_lower a should be true")
if (!is_lower("z")) fail("is_lower z should be true")
if (is_lower("A")) fail("is_lower A should be false")
if (is_lower("5")) fail("is_lower 5 should be false")
if (is_lower("ab")) fail("is_lower ab should be false")
if (is_lower(42)) fail("is_lower number should be false")
})
run("is_upper", function() {
if (!is_upper("A")) fail("is_upper A should be true")
if (!is_upper("Z")) fail("is_upper Z should be true")
if (is_upper("a")) fail("is_upper a should be false")
if (is_upper("5")) fail("is_upper 5 should be false")
if (is_upper("AB")) fail("is_upper AB should be false")
if (is_upper(42)) fail("is_upper number should be false")
})
run("is_whitespace", function() {
if (!is_whitespace(" ")) fail("is_whitespace space should be true")
if (!is_whitespace("\t")) fail("is_whitespace tab should be true")
if (!is_whitespace("\n")) fail("is_whitespace newline should be true")
if (is_whitespace("a")) fail("is_whitespace a should be false")
if (is_whitespace(" ")) fail("is_whitespace two spaces should be false")
if (is_whitespace(42)) fail("is_whitespace number should be false")
if (is_whitespace(null)) fail("is_whitespace null should be false")
})
// ============================================================================
// GLOBAL FUNCTIONS - LENGTH
// ============================================================================
@@ -2738,6 +2854,22 @@ run("modulo floats", function() {
if (result < 1.4 || result > 1.6) fail("modulo floats failed")
})
run("remainder float basic", function() {
if (remainder(5.5, 2.5) != 0.5) fail("remainder 5.5 % 2.5 failed")
})
run("modulo float basic", function() {
if (modulo(5.5, 2.5) != 0.5) fail("modulo 5.5 % 2.5 failed")
})
run("remainder float negative", function() {
if (remainder(-5.5, 2.5) != -0.5) fail("remainder -5.5 % 2.5 failed")
})
run("modulo float negative", function() {
if (modulo(-5.5, 2.5) != 2.0) fail("modulo -5.5 % 2.5 failed")
})
// ============================================================================
// MIN AND MAX FUNCTIONS
// ============================================================================
@@ -3370,6 +3502,166 @@ run("array map with exit", function() {
if (length(result) != 5) fail("array map with exit length unexpected")
})
run("inline map intrinsic is_data", function() {
var items = [{}, [], "hello", 42, true]
var result = array(items, is_data)
if (result[0] != true) fail("is_data {} should be true")
if (result[1] != false) fail("is_data [] should be false")
if (result[2] != false) fail("is_data string should be false")
if (result[3] != false) fail("is_data number should be false")
if (result[4] != false) fail("is_data bool should be false")
if (length(result) != 5) fail("result length should be 5")
})
run("inline map intrinsic is_number", function() {
var items = [1, "two", 3.14, null, true]
var result = array(items, is_number)
if (result[0] != true) fail("1 should be number")
if (result[1] != false) fail("'two' should not be number")
if (result[2] != true) fail("3.14 should be number")
if (result[3] != false) fail("null should not be number")
if (result[4] != false) fail("true should not be number")
})
run("inline map intrinsic is_text", function() {
var items = ["hello", 42, "", null]
var result = array(items, is_text)
if (result[0] != true) fail("'hello' should be text")
if (result[1] != false) fail("42 should not be text")
if (result[2] != true) fail("'' should be text")
if (result[3] != false) fail("null should not be text")
})
run("inline map intrinsic is_digit", function() {
var chars = array("a5B2 ")
var result = array(chars, is_digit)
if (result[0] != false) fail("a should not be digit")
if (result[1] != true) fail("5 should be digit")
if (result[2] != false) fail("B should not be digit")
if (result[3] != true) fail("2 should be digit")
if (result[4] != false) fail("space should not be digit")
})
run("inline map lambda", function() {
var arr = [10, 20, 30]
var result = array(arr, function(x) { return x + 1 })
if (result[0] != 11) fail("10+1 should be 11")
if (result[1] != 21) fail("20+1 should be 21")
if (result[2] != 31) fail("30+1 should be 31")
})
run("inline map empty array", function() {
var result = array([], is_number)
if (length(result) != 0) fail("map of empty should be empty")
})
// ============================================================================
// NUMERIC INTRINSIC CALLBACK INLINING
// ============================================================================
var mymap = function(arr, fn) {
var result = array(length(arr))
var i = 0
while (i < length(arr)) {
result[i] = fn(arr[i])
i = i + 1
}
return result
}
var myfold = function(arr, fn) {
var acc = arr[0]
var i = 1
while (i < length(arr)) {
acc = fn(acc, arr[i])
i = i + 1
}
return acc
}
run("inline callback abs", function() {
var result = mymap([-3, 5, -1.5, 0], function(a) { return abs(a) })
assert_eq(result[0], 3, "abs(-3)")
assert_eq(result[1], 5, "abs(5)")
assert_eq(result[2], 1.5, "abs(-1.5)")
assert_eq(result[3], 0, "abs(0)")
})
run("inline callback neg", function() {
var result = mymap([3, -5, 0, 1.5], function(a) { return neg(a) })
assert_eq(result[0], -3, "neg(3)")
assert_eq(result[1], 5, "neg(-5)")
assert_eq(result[2], 0, "neg(0)")
assert_eq(result[3], -1.5, "neg(1.5)")
})
run("inline callback sign", function() {
var result = mymap([-7, 0, 42, -0.5], function(a) { return sign(a) })
assert_eq(result[0], -1, "sign(-7)")
assert_eq(result[1], 0, "sign(0)")
assert_eq(result[2], 1, "sign(42)")
assert_eq(result[3], -1, "sign(-0.5)")
})
run("inline callback fraction", function() {
var result = mymap([3.75, -2.5, 5.0], function(a) { return fraction(a) })
assert_eq(result[0], 0.75, "fraction(3.75)")
assert_eq(result[1], -0.5, "fraction(-2.5)")
assert_eq(result[2], 0, "fraction(5.0)")
})
run("inline callback floor", function() {
var result = mymap([3.7, -2.3, 5.0, 1.9], function(a) { return floor(a) })
assert_eq(result[0], 3, "floor(3.7)")
assert_eq(result[1], -3, "floor(-2.3)")
assert_eq(result[2], 5, "floor(5.0)")
assert_eq(result[3], 1, "floor(1.9)")
})
run("inline callback ceiling", function() {
var result = mymap([3.2, -2.7, 5.0, 1.1], function(a) { return ceiling(a) })
assert_eq(result[0], 4, "ceiling(3.2)")
assert_eq(result[1], -2, "ceiling(-2.7)")
assert_eq(result[2], 5, "ceiling(5.0)")
assert_eq(result[3], 2, "ceiling(1.1)")
})
run("inline callback round", function() {
var result = mymap([3.5, -2.5, 5.0, 1.4], function(a) { return round(a) })
assert_eq(result[0], 4, "round(3.5)")
assert_eq(result[1], -3, "round(-2.5)")
assert_eq(result[2], 5, "round(5.0)")
assert_eq(result[3], 1, "round(1.4)")
})
run("inline callback trunc", function() {
var result = mymap([3.7, -2.3, 5.0, -1.9], function(a) { return trunc(a) })
assert_eq(result[0], 3, "trunc(3.7)")
assert_eq(result[1], -2, "trunc(-2.3)")
assert_eq(result[2], 5, "trunc(5.0)")
assert_eq(result[3], -1, "trunc(-1.9)")
})
run("inline callback max", function() {
var result = myfold([3, 7, 2, 9, 1], function(a, b) { return max(a, b) })
assert_eq(result, 9, "max reduce")
})
run("inline callback min", function() {
var result = myfold([3, 7, 2, 9, 1], function(a, b) { return min(a, b) })
assert_eq(result, 1, "min reduce")
})
run("inline callback modulo", function() {
var result = myfold([17, 5], function(a, b) { return modulo(a, b) })
assert_eq(result, 2, "modulo")
})
run("inline callback remainder", function() {
var result = myfold([-17, 5], function(a, b) { return remainder(a, b) })
assert_eq(result, -2, "remainder")
})
// ============================================================================
// STRING METHOD EDGE CASES
// ============================================================================
@@ -5690,6 +5982,187 @@ run("gc closure - factory pattern survives gc", function() {
assert_eq(b.say(), "hello bob", "second factory closure")
})
// ============================================================================
// INLINE LOOP EXPANSION TESTS
// ============================================================================
// --- filter inline expansion ---
run("filter inline - integer predicate", function() {
var result = filter([0, 1.25, 2, 3.5, 4, 5.75], is_integer)
assert_eq(length(result), 3, "filter integer count")
assert_eq(result[0], 0, "filter integer [0]")
assert_eq(result[1], 2, "filter integer [1]")
assert_eq(result[2], 4, "filter integer [2]")
})
run("filter inline - all pass", function() {
var result = filter([1, 2, 3], function(x) { return true })
assert_eq(length(result), 3, "filter all pass length")
})
run("filter inline - none pass", function() {
var result = filter([1, 2, 3], function(x) { return false })
assert_eq(length(result), 0, "filter none pass length")
})
run("filter inline - empty", function() {
var result = filter([], is_integer)
assert_eq(length(result), 0, "filter empty length")
})
run("filter inline - with index", function() {
var result = filter([10, 20, 30], function(e, i) { return i > 0 })
assert_eq(length(result), 2, "filter index length")
assert_eq(result[0], 20, "filter index [0]")
assert_eq(result[1], 30, "filter index [1]")
})
run("filter inline - large callback", function() {
var result = filter([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], function(x) {
var a = x * 2
var b = a + 1
var c = b * 3
var d = c - a
return d > 20
})
if (length(result) < 1) fail("filter large callback should return elements")
})
// --- find inline expansion ---
run("find inline - function forward", function() {
var idx = find([1, 2, 3], function(x) { return x == 2 })
assert_eq(idx, 1, "find fn forward")
})
run("find inline - function not found", function() {
var idx = find([1, 2, 3], function(x) { return x == 99 })
assert_eq(idx, null, "find fn not found")
})
run("find inline - value forward", function() {
var idx = find([10, 20, 30], 20)
assert_eq(idx, 1, "find value forward")
})
run("find inline - value not found", function() {
var idx = find([10, 20, 30], 99)
assert_eq(idx, null, "find value not found")
})
run("find inline - reverse", function() {
var idx = find([1, 2, 1, 2], function(x) { return x == 1 }, true)
assert_eq(idx, 2, "find reverse")
})
run("find inline - from", function() {
var idx = find([1, 2, 3, 2], function(x) { return x == 2 }, false, 2)
assert_eq(idx, 3, "find from")
})
run("find inline - empty", function() {
var idx = find([], 1)
assert_eq(idx, null, "find empty")
})
run("find inline - value reverse", function() {
var idx = find([10, 20, 30, 20], 20, true)
assert_eq(idx, 3, "find value reverse")
})
run("find inline - value with from", function() {
var idx = find([10, 20, 30, 20], 20, false, 2)
assert_eq(idx, 3, "find value with from")
})
run("find inline - with index callback", function() {
var idx = find(["a", "b", "c"], (x, i) => i == 2)
assert_eq(idx, 2, "find index callback")
})
// --- arrfor inline expansion ---
run("arrfor inline - basic sum", function() {
var sum = 0
arrfor([1, 2, 3, 4, 5], function(x) { sum = sum + x })
assert_eq(sum, 15, "arrfor basic sum")
})
run("arrfor inline - reverse", function() {
var order = []
arrfor([1, 2, 3], function(x) { order[] = x }, true)
assert_eq(order[0], 3, "arrfor reverse [0]")
assert_eq(order[1], 2, "arrfor reverse [1]")
assert_eq(order[2], 1, "arrfor reverse [2]")
})
run("arrfor inline - exit", function() {
var result = arrfor([1, 2, 3, 4, 5], function(x) {
if (x > 3) return true
return null
}, false, true)
assert_eq(result, true, "arrfor exit")
})
run("arrfor inline - no exit returns null", function() {
var result = arrfor([1, 2, 3], function(x) { })
assert_eq(result, null, "arrfor no exit null")
})
run("arrfor inline - with index", function() {
var indices = []
arrfor([10, 20, 30], (x, i) => { indices[] = i })
assert_eq(indices[0], 0, "arrfor index [0]")
assert_eq(indices[1], 1, "arrfor index [1]")
assert_eq(indices[2], 2, "arrfor index [2]")
})
run("arrfor inline - reverse with index", function() {
var items = []
arrfor(["a", "b", "c"], function(x, i) { items[] = text(i) + x }, true)
assert_eq(items[0], "2c", "arrfor rev index [0]")
assert_eq(items[1], "1b", "arrfor rev index [1]")
assert_eq(items[2], "0a", "arrfor rev index [2]")
})
// --- reduce inline expansion ---
run("reduce inline - no initial forward", function() {
var result = reduce([1, 2, 3, 4, 5, 6, 7, 8, 9], function(a, b) { return a + b })
assert_eq(result, 45, "reduce sum 1-9")
})
run("reduce inline - single element", function() {
var result = reduce([42], function(a, b) { return a + b })
assert_eq(result, 42, "reduce single")
})
run("reduce inline - empty", function() {
var result = reduce([], function(a, b) { return a + b })
assert_eq(result, null, "reduce empty")
})
run("reduce inline - with initial", function() {
var result = reduce([1, 2, 3], function(a, b) { return a + b }, 10)
assert_eq(result, 16, "reduce with initial")
})
run("reduce inline - with initial empty", function() {
var result = reduce([], function(a, b) { return a + b }, 99)
assert_eq(result, 99, "reduce initial empty")
})
run("reduce inline - reverse", function() {
var result = reduce([1, 2, 3], function(a, b) { return a - b }, 0, true)
assert_eq(result, -6, "reduce reverse")
})
run("reduce inline - intrinsic callback", function() {
var result = reduce([3, 7, 2, 9, 1], max)
assert_eq(result, 9, "reduce max")
})
// ============================================================================
// SUMMARY
// ============================================================================

249
xref.ce Normal file
View File

@@ -0,0 +1,249 @@
// xref.ce — cross-reference / call graph
//
// Usage:
// cell xref <file> Full creation tree
// cell xref --callers <N> <file> Who creates function [N]?
// cell xref --callees <N> <file> What does [N] create/call?
// cell xref --optimized <file> Use optimized IR
// cell xref --dot <file> DOT graph for graphviz
var shop = use("internal/shop")
var run = function() {
var filename = null
var use_optimized = false
var show_callers = null
var show_callees = null
var show_dot = false
var i = 0
var compiled = null
var creates = {}
var created_by = {}
var func_names = {}
var fi = 0
var func = null
var fname = null
var main_name = null
var creators = null
var c = null
var line_info = null
var children = null
var ch = null
var ch_line = null
var parent_keys = null
var ki = 0
var parent_idx = 0
var ch_list = null
var ci = 0
var printed = {}
while (i < length(args)) {
if (args[i] == '--callers') {
i = i + 1
show_callers = number(args[i])
} else if (args[i] == '--callees') {
i = i + 1
show_callees = number(args[i])
} else if (args[i] == '--dot') {
show_dot = true
} else if (args[i] == '--optimized') {
use_optimized = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell xref [--callers <N>] [--callees <N>] [--dot] [--optimized] <file>")
log.console("")
log.console(" --callers <N> Who creates function [N]?")
log.console(" --callees <N> What does [N] create/call?")
log.console(" --dot Output DOT format for graphviz")
log.console(" --optimized Use optimized IR")
return null
} else if (!starts_with(args[i], '-')) {
filename = args[i]
}
i = i + 1
}
if (!filename) {
log.console("Usage: cell xref [--callers <N>] [--callees <N>] [--dot] [--optimized] <file>")
return null
}
if (use_optimized) {
compiled = shop.compile_file(filename)
} else {
compiled = shop.mcode_file(filename)
}
main_name = compiled.name != null ? compiled.name : "<main>"
func_names["-1"] = main_name
var scan_func = function(func, parent_idx) {
var instrs = func.instructions
var j = 0
var instr = null
var n = 0
var child_idx = null
var instr_line = null
if (instrs == null) return null
while (j < length(instrs)) {
instr = instrs[j]
if (is_array(instr) && instr[0] == "function") {
n = length(instr)
child_idx = instr[2]
instr_line = instr[n - 2]
if (!creates[text(parent_idx)]) {
creates[text(parent_idx)] = []
}
push(creates[text(parent_idx)], {child: child_idx, line: instr_line})
if (!created_by[text(child_idx)]) {
created_by[text(child_idx)] = []
}
push(created_by[text(child_idx)], {parent: parent_idx, line: instr_line})
}
j = j + 1
}
return null
}
if (compiled.main != null) {
scan_func(compiled.main, -1)
}
if (compiled.functions != null) {
fi = 0
while (fi < length(compiled.functions)) {
func = compiled.functions[fi]
fname = func.name != null ? func.name : "<anonymous>"
func_names[text(fi)] = fname
scan_func(func, fi)
fi = fi + 1
}
}
var func_label = function(idx) {
var name = func_names[text(idx)]
if (idx == -1) return main_name
if (name != null) return `[${text(idx)}] ${name}`
return `[${text(idx)}]`
}
var safe_label = function(idx) {
var name = func_names[text(idx)]
if (name != null) return replace(name, '"', '\\"')
if (idx == -1) return main_name
return `func_${text(idx)}`
}
var node_id = function(idx) {
if (idx == -1) return "main"
return `f${text(idx)}`
}
// --callers mode
if (show_callers != null) {
creators = created_by[text(show_callers)]
log.compile(`\nCallers of ${func_label(show_callers)}:`)
if (creators == null || length(creators) == 0) {
log.compile(" (none - may be main or unreferenced)")
} else {
i = 0
while (i < length(creators)) {
c = creators[i]
line_info = c.line != null ? ` at line ${text(c.line)}` : ""
log.compile(` ${func_label(c.parent)}${line_info}`)
i = i + 1
}
}
return null
}
// --callees mode
if (show_callees != null) {
children = creates[text(show_callees)]
log.compile(`\nCallees of ${func_label(show_callees)}:`)
if (children == null || length(children) == 0) {
log.compile(" (none)")
} else {
i = 0
while (i < length(children)) {
ch = children[i]
ch_line = ch.line != null ? ` at line ${text(ch.line)}` : ""
log.compile(` ${func_label(ch.child)}${ch_line}`)
i = i + 1
}
}
return null
}
// --dot mode
if (show_dot) {
log.compile("digraph xref {")
log.compile(" rankdir=TB;")
log.compile(" node [shape=box, style=filled, fillcolor=lightyellow];")
log.compile(` ${node_id(-1)} [label="${safe_label(-1)}"];`)
if (compiled.functions != null) {
fi = 0
while (fi < length(compiled.functions)) {
log.compile(` ${node_id(fi)} [label="${safe_label(fi)}"];`)
fi = fi + 1
}
}
parent_keys = array(creates)
ki = 0
while (ki < length(parent_keys)) {
parent_idx = number(parent_keys[ki])
ch_list = creates[parent_keys[ki]]
ci = 0
while (ci < length(ch_list)) {
log.compile(` ${node_id(parent_idx)} -> ${node_id(ch_list[ci].child)};`)
ci = ci + 1
}
ki = ki + 1
}
log.compile("}")
return null
}
// Default: indented tree from main
var print_tree = function(idx, depth) {
var indent = ""
var d = 0
var children = null
var ci = 0
var child = null
while (d < depth) {
indent = indent + " "
d = d + 1
}
log.compile(`${indent}${func_label(idx)}`)
if (printed[text(idx)]) {
log.compile(`${indent} (already shown)`)
return null
}
printed[text(idx)] = true
children = creates[text(idx)]
if (children != null) {
ci = 0
while (ci < length(children)) {
child = children[ci]
print_tree(child.child, depth + 1)
ci = ci + 1
}
}
return null
}
log.compile("")
print_tree(-1, 0)
return null
}
run()
$stop()