Merge branch 'optimize_mcode'
This commit is contained in:
86
bench_arith.ce
Normal file
86
bench_arith.ce
Normal 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
67
bench_arith.js
Normal 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
68
bench_arith.lua
Normal 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
113
bench_array.ce
Normal 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
93
bench_array.js
Normal 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
93
bench_array.lua
Normal 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
21
bench_fib.ce
Normal 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
118
bench_object.ce
Normal 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
99
bench_object.js
Normal 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
101
bench_object.lua
Normal 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
28755
boot/fold.cm.mcode
28755
boot/fold.cm.mcode
File diff suppressed because one or more lines are too long
43234
boot/mcode.cm.mcode
43234
boot/mcode.cm.mcode
File diff suppressed because one or more lines are too long
44250
boot/parse.cm.mcode
44250
boot/parse.cm.mcode
File diff suppressed because one or more lines are too long
41246
boot/streamline.cm.mcode
41246
boot/streamline.cm.mcode
File diff suppressed because one or more lines are too long
11016
boot/tokenize.cm.mcode
11016
boot/tokenize.cm.mcode
File diff suppressed because one or more lines are too long
74
boot_miscompile_bad.cm
Normal file
74
boot_miscompile_bad.cm
Normal 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
456
cfg.ce
Normal 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
310
diff_ir.ce
Normal 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()
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
12
parse.cm
12
parse.cm
@@ -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
303
slots.ce
Normal 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()
|
||||
435
source/mach.c
435
source/mach.c
@@ -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); }
|
||||
|
||||
@@ -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);
|
||||
|
||||
159
source/runtime.c
159
source/runtime.c
@@ -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);
|
||||
|
||||
1027
streamline.cm
1027
streamline.cm
File diff suppressed because it is too large
Load Diff
473
vm_suite.ce
473
vm_suite.ce
@@ -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
249
xref.ce
Normal 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()
|
||||
Reference in New Issue
Block a user