Merge branch 'optimize_mcode'
This commit is contained in:
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
|
||||
|
||||
|
||||
351
mcode.cm
351
mcode.cm
@@ -88,6 +88,7 @@ var mcode = function(ast) {
|
||||
var s_cur_col = 0
|
||||
var s_filename = null
|
||||
var s_has_disruption = false
|
||||
var s_slot_types = {}
|
||||
|
||||
// Shared closure vars for binop helpers (avoids >4 param functions)
|
||||
var _bp_dest = 0
|
||||
@@ -116,7 +117,8 @@ var mcode = function(ast) {
|
||||
intrinsic_cache: s_intrinsic_cache,
|
||||
cur_line: s_cur_line,
|
||||
cur_col: s_cur_col,
|
||||
has_disruption: s_has_disruption
|
||||
has_disruption: s_has_disruption,
|
||||
slot_types: s_slot_types
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +140,7 @@ var mcode = function(ast) {
|
||||
s_cur_line = saved.cur_line
|
||||
s_cur_col = saved.cur_col
|
||||
s_has_disruption = saved.has_disruption
|
||||
s_slot_types = saved.slot_types
|
||||
}
|
||||
|
||||
// Slot allocation
|
||||
@@ -330,20 +333,48 @@ var mcode = function(ast) {
|
||||
return node.kind == "null"
|
||||
}
|
||||
|
||||
// Slot-type tracking helpers
|
||||
var slot_is_num = function(slot) {
|
||||
var t = s_slot_types[text(slot)]
|
||||
return t == "num" || t == "int"
|
||||
}
|
||||
|
||||
var slot_is_text = function(slot) {
|
||||
return s_slot_types[text(slot)] == "text"
|
||||
}
|
||||
|
||||
var mark_slot = function(slot, typ) {
|
||||
s_slot_types[text(slot)] = typ
|
||||
}
|
||||
|
||||
var propagate_slot = function(dest, src) {
|
||||
s_slot_types[text(dest)] = s_slot_types[text(src)]
|
||||
}
|
||||
|
||||
// emit_add_decomposed: emit type-dispatched add (text → concat, num → add)
|
||||
// reads _bp_dest, _bp_left, _bp_right, _bp_ln, _bp_rn from closure
|
||||
var emit_add_decomposed = function() {
|
||||
if (is_known_text(_bp_ln) && is_known_text(_bp_rn)) {
|
||||
var left_is_num = is_known_number(_bp_ln) || slot_is_num(_bp_left)
|
||||
var left_is_text = is_known_text(_bp_ln) || slot_is_text(_bp_left)
|
||||
var right_is_num = is_known_number(_bp_rn) || slot_is_num(_bp_right)
|
||||
var right_is_text = is_known_text(_bp_rn) || slot_is_text(_bp_right)
|
||||
|
||||
// Both known text → concat
|
||||
if (left_is_text && right_is_text) {
|
||||
emit_3("concat", _bp_dest, _bp_left, _bp_right)
|
||||
mark_slot(_bp_dest, "text")
|
||||
return null
|
||||
}
|
||||
if (is_known_number(_bp_ln) && is_known_number(_bp_rn)) {
|
||||
// Both known number → add
|
||||
if (left_is_num && right_is_num) {
|
||||
emit_3("add", _bp_dest, _bp_left, _bp_right)
|
||||
mark_slot(_bp_dest, "num")
|
||||
return null
|
||||
}
|
||||
// If either operand is a known number, concat is impossible
|
||||
if (is_known_number(_bp_ln) || is_known_number(_bp_rn)) {
|
||||
// One known number, other unknown → emit_numeric_binop (guard on unknown side)
|
||||
if (left_is_num || right_is_num) {
|
||||
emit_numeric_binop("add")
|
||||
mark_slot(_bp_dest, "num")
|
||||
return null
|
||||
}
|
||||
// Unknown types: emit full dispatch
|
||||
@@ -380,8 +411,10 @@ var mcode = function(ast) {
|
||||
// emit_numeric_binop: emit type-guarded numeric binary op
|
||||
// reads _bp_dest, _bp_left, _bp_right, _bp_ln, _bp_rn from closure
|
||||
var emit_numeric_binop = function(op_str) {
|
||||
if (is_known_number(_bp_ln) && is_known_number(_bp_rn)) {
|
||||
if ((is_known_number(_bp_ln) || slot_is_num(_bp_left))
|
||||
&& (is_known_number(_bp_rn) || slot_is_num(_bp_right))) {
|
||||
emit_3(op_str, _bp_dest, _bp_left, _bp_right)
|
||||
mark_slot(_bp_dest, "num")
|
||||
return null
|
||||
}
|
||||
var t0 = alloc_slot()
|
||||
@@ -399,238 +432,33 @@ var mcode = function(ast) {
|
||||
emit_log_error("cannot apply '" + _bp_op_sym + "': operands must be numbers")
|
||||
emit_0("disrupt")
|
||||
emit_label(done)
|
||||
mark_slot(_bp_dest, "num")
|
||||
return null
|
||||
}
|
||||
|
||||
// emit_eq_decomposed: identical -> int -> float -> text -> null -> bool -> mismatch(false)
|
||||
// reads _bp_dest, _bp_left, _bp_right from closure
|
||||
// emit_eq_decomposed: VM eq handles all types (int fast path, text memcmp, identity, mixed→false)
|
||||
var emit_eq_decomposed = function() {
|
||||
var dest = _bp_dest
|
||||
var left = _bp_left
|
||||
var right = _bp_right
|
||||
var t0 = 0
|
||||
var t1 = 0
|
||||
var done = gen_label("eq_done")
|
||||
var not_int = gen_label("eq_ni")
|
||||
var not_num = gen_label("eq_nn")
|
||||
var not_text = gen_label("eq_nt")
|
||||
var not_null = gen_label("eq_nnl")
|
||||
var not_bool = gen_label("eq_nb")
|
||||
|
||||
// Identical check
|
||||
emit_3("is_identical", dest, left, right)
|
||||
emit_jump_cond("jump_true", dest, done)
|
||||
|
||||
// Int path
|
||||
t0 = alloc_slot()
|
||||
emit_2("is_int", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_int)
|
||||
t1 = alloc_slot()
|
||||
emit_2("is_int", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_int)
|
||||
emit_3("eq_int", dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
// Float path
|
||||
emit_label(not_int)
|
||||
emit_2("is_num", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_num)
|
||||
emit_2("is_num", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_num)
|
||||
emit_3("eq_float", dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
// Text path
|
||||
emit_label(not_num)
|
||||
emit_2("is_text", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_text)
|
||||
emit_2("is_text", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_text)
|
||||
emit_3("eq_text", dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
// Null path
|
||||
emit_label(not_text)
|
||||
emit_2("is_null", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_null)
|
||||
emit_2("is_null", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_null)
|
||||
emit_1("true", dest)
|
||||
emit_jump(done)
|
||||
|
||||
// Bool path
|
||||
emit_label(not_null)
|
||||
emit_2("is_bool", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_bool)
|
||||
emit_2("is_bool", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_bool)
|
||||
emit_3("eq_bool", dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
// Mismatch -> false
|
||||
emit_label(not_bool)
|
||||
emit_1("false", dest)
|
||||
emit_label(done)
|
||||
emit_3("eq", _bp_dest, _bp_left, _bp_right)
|
||||
return null
|
||||
}
|
||||
|
||||
// emit_ne_decomposed: identical -> int -> float -> text -> null -> bool -> mismatch(true)
|
||||
// reads _bp_dest, _bp_left, _bp_right from closure
|
||||
// emit_ne_decomposed: VM ne handles all types (int fast path, text memcmp, identity, mixed→true)
|
||||
var emit_ne_decomposed = function() {
|
||||
var dest = _bp_dest
|
||||
var left = _bp_left
|
||||
var right = _bp_right
|
||||
var t0 = 0
|
||||
var t1 = 0
|
||||
var done = gen_label("ne_done")
|
||||
var not_ident = gen_label("ne_nid")
|
||||
var not_int = gen_label("ne_ni")
|
||||
var not_num = gen_label("ne_nn")
|
||||
var not_text = gen_label("ne_nt")
|
||||
var not_null = gen_label("ne_nnl")
|
||||
var not_bool = gen_label("ne_nb")
|
||||
|
||||
// Identical -> false
|
||||
emit_3("is_identical", dest, left, right)
|
||||
emit_jump_cond("jump_true", dest, not_ident)
|
||||
// If jump_true doesn't fire, dest already holds false, continue to checks
|
||||
emit_jump(not_int)
|
||||
|
||||
emit_label(not_ident)
|
||||
emit_1("false", dest)
|
||||
emit_jump(done)
|
||||
|
||||
// Int path
|
||||
emit_label(not_int)
|
||||
t0 = alloc_slot()
|
||||
emit_2("is_int", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_num)
|
||||
t1 = alloc_slot()
|
||||
emit_2("is_int", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_num)
|
||||
emit_3("ne_int", dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
// Float path
|
||||
emit_label(not_num)
|
||||
emit_2("is_num", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_text)
|
||||
emit_2("is_num", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_text)
|
||||
emit_3("ne_float", dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
// Text path
|
||||
emit_label(not_text)
|
||||
emit_2("is_text", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_null)
|
||||
emit_2("is_text", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_null)
|
||||
emit_3("ne_text", dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
// Null path
|
||||
emit_label(not_null)
|
||||
emit_2("is_null", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_bool)
|
||||
emit_2("is_null", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_bool)
|
||||
emit_1("false", dest)
|
||||
emit_jump(done)
|
||||
|
||||
// Bool path
|
||||
var mismatch = gen_label("ne_mis")
|
||||
emit_label(not_bool)
|
||||
emit_2("is_bool", t0, left)
|
||||
emit_jump_cond("jump_false", t0, mismatch)
|
||||
emit_2("is_bool", t1, right)
|
||||
emit_jump_cond("jump_false", t1, mismatch)
|
||||
emit_3("ne_bool", dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
// Mismatch -> true (ne of different types is true)
|
||||
emit_label(mismatch)
|
||||
emit_1("true", dest)
|
||||
emit_label(done)
|
||||
emit_3("ne", _bp_dest, _bp_left, _bp_right)
|
||||
return null
|
||||
}
|
||||
|
||||
// emit_relational: int -> float -> text -> disrupt
|
||||
// reads _bp_dest, _bp_left, _bp_right, _bp_ln, _bp_rn from closure
|
||||
var emit_relational = function(int_op, float_op, text_op) {
|
||||
var dest = _bp_dest
|
||||
var left = _bp_left
|
||||
var right = _bp_right
|
||||
var t0 = 0
|
||||
var t1 = 0
|
||||
var left_is_int = is_known_int(_bp_ln)
|
||||
var left_is_num = is_known_number(_bp_ln)
|
||||
var left_is_text = is_known_text(_bp_ln)
|
||||
var right_is_int = is_known_int(_bp_rn)
|
||||
var right_is_num = is_known_number(_bp_rn)
|
||||
var right_is_text = is_known_text(_bp_rn)
|
||||
var not_int = null
|
||||
var not_num = null
|
||||
var done = null
|
||||
var err = null
|
||||
|
||||
// Both known int
|
||||
if (left_is_int && right_is_int) {
|
||||
emit_3(int_op, dest, left, right)
|
||||
return null
|
||||
}
|
||||
// Both known number
|
||||
if (left_is_num && right_is_num) {
|
||||
emit_3(float_op, dest, left, right)
|
||||
return null
|
||||
}
|
||||
// Both known text
|
||||
if (left_is_text && right_is_text) {
|
||||
emit_3(text_op, dest, left, right)
|
||||
return null
|
||||
}
|
||||
|
||||
not_int = gen_label("rel_ni")
|
||||
not_num = gen_label("rel_nn")
|
||||
done = gen_label("rel_done")
|
||||
err = gen_label("rel_err")
|
||||
|
||||
t0 = alloc_slot()
|
||||
emit_2("is_int", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_int)
|
||||
t1 = alloc_slot()
|
||||
emit_2("is_int", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_int)
|
||||
emit_3(int_op, dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
emit_label(not_int)
|
||||
emit_2("is_num", t0, left)
|
||||
emit_jump_cond("jump_false", t0, not_num)
|
||||
emit_2("is_num", t1, right)
|
||||
emit_jump_cond("jump_false", t1, not_num)
|
||||
emit_3(float_op, dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
emit_label(not_num)
|
||||
emit_2("is_text", t0, left)
|
||||
emit_jump_cond("jump_false", t0, err)
|
||||
emit_2("is_text", t1, right)
|
||||
emit_jump_cond("jump_false", t1, err)
|
||||
emit_3(text_op, dest, left, right)
|
||||
emit_jump(done)
|
||||
|
||||
emit_label(err)
|
||||
emit_log_error("cannot compare with '" + _bp_op_sym + "': operands must be same type")
|
||||
emit_0("disrupt")
|
||||
emit_label(done)
|
||||
// emit_relational: VM lt/le/gt/ge handle numbers and text, disrupt on mismatch
|
||||
var emit_relational = function(op_str) {
|
||||
emit_3(op_str, _bp_dest, _bp_left, _bp_right)
|
||||
return null
|
||||
}
|
||||
|
||||
// emit_neg_decomposed: emit type-guarded negate
|
||||
var emit_neg_decomposed = function(dest, src, src_node) {
|
||||
if (is_known_number(src_node)) {
|
||||
if (is_known_number(src_node) || slot_is_num(src)) {
|
||||
emit_2("negate", dest, src)
|
||||
mark_slot(dest, "num")
|
||||
return null
|
||||
}
|
||||
var t0 = alloc_slot()
|
||||
@@ -645,19 +473,13 @@ var mcode = function(ast) {
|
||||
emit_log_error("cannot negate: operand must be a number")
|
||||
emit_0("disrupt")
|
||||
emit_label(done)
|
||||
mark_slot(dest, "num")
|
||||
return null
|
||||
}
|
||||
|
||||
// Central router: maps op string to decomposition helper
|
||||
// Sets _bp_* closure vars then calls helper with reduced args
|
||||
var relational_ops = {
|
||||
lt: ["lt_int", "lt_float", "lt_text"],
|
||||
le: ["le_int", "le_float", "le_text"],
|
||||
gt: ["gt_int", "gt_float", "gt_text"],
|
||||
ge: ["ge_int", "ge_float", "ge_text"]
|
||||
}
|
||||
var emit_binop = function(op_str, dest, left, right) {
|
||||
var rel = null
|
||||
_bp_dest = dest
|
||||
_bp_left = left
|
||||
_bp_right = right
|
||||
@@ -668,18 +490,15 @@ var mcode = function(ast) {
|
||||
emit_eq_decomposed()
|
||||
} else if (op_str == "ne") {
|
||||
emit_ne_decomposed()
|
||||
} else if (op_str == "lt" || op_str == "le" || op_str == "gt" || op_str == "ge") {
|
||||
emit_relational(op_str)
|
||||
} else if (op_str == "subtract" || op_str == "multiply" ||
|
||||
op_str == "divide" || op_str == "modulo" || op_str == "remainder" ||
|
||||
op_str == "pow") {
|
||||
emit_numeric_binop(op_str)
|
||||
} else {
|
||||
rel = relational_ops[op_str]
|
||||
if (rel != null) {
|
||||
emit_relational(rel[0], rel[1], rel[2])
|
||||
} else if (op_str == "subtract" || op_str == "multiply" ||
|
||||
op_str == "divide" || op_str == "modulo" || op_str == "remainder" ||
|
||||
op_str == "pow") {
|
||||
emit_numeric_binop(op_str)
|
||||
} else {
|
||||
// Passthrough for bitwise, in, etc.
|
||||
emit_3(op_str, dest, left, right)
|
||||
}
|
||||
// Passthrough for bitwise, in, etc.
|
||||
emit_3(op_str, dest, left, right)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -716,9 +535,6 @@ var mcode = function(ast) {
|
||||
var argc = length(args)
|
||||
var frame_slot = alloc_slot()
|
||||
emit_3("frame", frame_slot, func_slot, argc)
|
||||
var null_slot = alloc_slot()
|
||||
emit_1("null", null_slot)
|
||||
emit_3("setarg", frame_slot, 0, null_slot)
|
||||
var arg_idx = 1
|
||||
var _i = 0
|
||||
while (_i < argc) {
|
||||
@@ -1060,20 +876,20 @@ var mcode = function(ast) {
|
||||
emit_1("null", null_s)
|
||||
emit_label(loop_label)
|
||||
if (forward) {
|
||||
emit_3("lt_int", check, i, len)
|
||||
emit_3("lt", check, i, len)
|
||||
} else {
|
||||
emit_3("ge_int", check, i, zero)
|
||||
emit_3("ge", check, i, zero)
|
||||
}
|
||||
emit_jump_cond("jump_false", check, done_label)
|
||||
emit_3("load_index", item, arr_slot, i)
|
||||
emit_3("eq_int", arity_is_zero, fn_arity, zero)
|
||||
emit_3("eq", arity_is_zero, fn_arity, zero)
|
||||
emit_jump_cond("jump_false", arity_is_zero, call_one_label)
|
||||
emit_3("frame", f, fn_slot, 0)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
emit_2("invoke", f, acc)
|
||||
emit_jump(call_done_label)
|
||||
emit_label(call_one_label)
|
||||
emit_3("eq_int", arity_is_one, fn_arity, one)
|
||||
emit_3("eq", arity_is_one, fn_arity, one)
|
||||
emit_jump_cond("jump_false", arity_is_one, call_two_label)
|
||||
emit_3("frame", f, fn_slot, 1)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
@@ -1121,17 +937,17 @@ var mcode = function(ast) {
|
||||
emit_1("null", null_s)
|
||||
emit_2("length", fn_arity, fn_slot)
|
||||
emit_label(loop_label)
|
||||
emit_3("lt_int", check, i, len)
|
||||
emit_3("lt", check, i, len)
|
||||
emit_jump_cond("jump_false", check, done_label)
|
||||
emit_3("load_index", item, arr_slot, i)
|
||||
emit_3("eq_int", arity_is_zero, fn_arity, zero)
|
||||
emit_3("eq", arity_is_zero, fn_arity, zero)
|
||||
emit_jump_cond("jump_false", arity_is_zero, call_one_label)
|
||||
emit_3("frame", f, fn_slot, 0)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
emit_2("invoke", f, discard)
|
||||
emit_jump(call_done_label)
|
||||
emit_label(call_one_label)
|
||||
emit_3("eq_int", arity_is_one, fn_arity, one)
|
||||
emit_3("eq", arity_is_one, fn_arity, one)
|
||||
emit_jump_cond("jump_false", arity_is_one, call_two_label)
|
||||
emit_3("frame", f, fn_slot, 1)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
@@ -1178,10 +994,10 @@ var mcode = function(ast) {
|
||||
emit_1("null", null_s)
|
||||
emit_2("length", fn_arity, fn_slot)
|
||||
emit_label(loop_label)
|
||||
emit_3("lt_int", check, i, len)
|
||||
emit_3("lt", check, i, len)
|
||||
emit_jump_cond("jump_false", check, ret_true)
|
||||
emit_3("load_index", item, arr_slot, i)
|
||||
emit_3("eq_int", arity_is_zero, fn_arity, zero)
|
||||
emit_3("eq", arity_is_zero, fn_arity, zero)
|
||||
emit_jump_cond("jump_false", arity_is_zero, call_one_label)
|
||||
emit_3("frame", f, fn_slot, 0)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
@@ -1231,10 +1047,10 @@ var mcode = function(ast) {
|
||||
emit_1("null", null_s)
|
||||
emit_2("length", fn_arity, fn_slot)
|
||||
emit_label(loop_label)
|
||||
emit_3("lt_int", check, i, len)
|
||||
emit_3("lt", check, i, len)
|
||||
emit_jump_cond("jump_false", check, ret_false)
|
||||
emit_3("load_index", item, arr_slot, i)
|
||||
emit_3("eq_int", arity_is_zero, fn_arity, zero)
|
||||
emit_3("eq", arity_is_zero, fn_arity, zero)
|
||||
emit_jump_cond("jump_false", arity_is_zero, call_one_label)
|
||||
emit_3("frame", f, fn_slot, 0)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
@@ -1287,17 +1103,17 @@ var mcode = function(ast) {
|
||||
emit_1("null", null_s)
|
||||
emit_2("length", fn_arity, fn_slot)
|
||||
emit_label(loop_label)
|
||||
emit_3("lt_int", check, i, len)
|
||||
emit_3("lt", check, i, len)
|
||||
emit_jump_cond("jump_false", check, done_label)
|
||||
emit_3("load_index", item, arr_slot, i)
|
||||
emit_3("eq_int", arity_is_zero, fn_arity, zero)
|
||||
emit_3("eq", arity_is_zero, fn_arity, zero)
|
||||
emit_jump_cond("jump_false", arity_is_zero, call_one_label)
|
||||
emit_3("frame", f, fn_slot, 0)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
emit_2("invoke", f, val)
|
||||
emit_jump(call_done_label)
|
||||
emit_label(call_one_label)
|
||||
emit_3("eq_int", arity_is_one, fn_arity, one)
|
||||
emit_3("eq", arity_is_one, fn_arity, one)
|
||||
emit_jump_cond("jump_false", arity_is_one, call_two_label)
|
||||
emit_3("frame", f, fn_slot, 1)
|
||||
emit_3("setarg", f, 0, null_s)
|
||||
@@ -1352,7 +1168,7 @@ var mcode = function(ast) {
|
||||
if (nargs == 2) {
|
||||
null_label = gen_label("reduce_null")
|
||||
d1 = gen_label("reduce_d1")
|
||||
emit_3("lt_int", check, zero, len)
|
||||
emit_3("lt", check, zero, len)
|
||||
emit_jump_cond("jump_false", check, null_label)
|
||||
emit_3("load_index", acc, arr_slot, zero)
|
||||
emit_2("move", i, one)
|
||||
@@ -1371,7 +1187,7 @@ var mcode = function(ast) {
|
||||
emit_2("is_null", check, init_slot)
|
||||
emit_jump_cond("jump_false", check, has_init)
|
||||
// No initial, forward
|
||||
emit_3("lt_int", check, zero, len)
|
||||
emit_3("lt", check, zero, len)
|
||||
emit_jump_cond("jump_false", check, null_label)
|
||||
emit_3("load_index", acc, arr_slot, zero)
|
||||
emit_2("move", i, one)
|
||||
@@ -1403,7 +1219,7 @@ var mcode = function(ast) {
|
||||
emit_2("is_null", check, init_slot)
|
||||
emit_jump_cond("jump_false", check, has_init)
|
||||
// No initial
|
||||
emit_3("lt_int", check, zero, len)
|
||||
emit_3("lt", check, zero, len)
|
||||
emit_jump_cond("jump_false", check, null_label)
|
||||
emit_jump_cond("jump_true", rev_slot, no_init_rev)
|
||||
// No initial, forward
|
||||
@@ -1514,8 +1330,10 @@ var mcode = function(ast) {
|
||||
// Standard binary ops
|
||||
left_slot = gen_expr(left, -1)
|
||||
right_slot = gen_expr(right, -1)
|
||||
// Use target slot for ops without multi-type dispatch (add has text+num paths)
|
||||
dest = (target >= 0 && kind != "+") ? target : alloc_slot()
|
||||
// Use target slot for ops without multi-type dispatch (add has text+num paths).
|
||||
// Exception: allow + to write directly to target when target == left_slot
|
||||
// (self-assign pattern like s = s + x) since concat/add reads before writing.
|
||||
dest = (target >= 0 && (kind != "+" || target == left_slot)) ? target : alloc_slot()
|
||||
op = binop_map[kind]
|
||||
if (op == null) {
|
||||
op = "add"
|
||||
@@ -1581,6 +1399,7 @@ var mcode = function(ast) {
|
||||
local = find_var(name)
|
||||
if (local >= 0) {
|
||||
emit_2("move", local, dest)
|
||||
propagate_slot(local, dest)
|
||||
}
|
||||
} else if (level > 0) {
|
||||
_lv = level - 1
|
||||
@@ -1680,9 +1499,11 @@ var mcode = function(ast) {
|
||||
if (level == 0 || level == -1) {
|
||||
slot = find_var(name)
|
||||
if (slot >= 0) {
|
||||
mark_slot(slot, null)
|
||||
val_slot = gen_expr(right, slot)
|
||||
if (val_slot != slot) {
|
||||
emit_2("move", slot, val_slot)
|
||||
propagate_slot(slot, val_slot)
|
||||
}
|
||||
return val_slot
|
||||
}
|
||||
@@ -1807,6 +1628,7 @@ var mcode = function(ast) {
|
||||
if (kind == "number") {
|
||||
slot = target >= 0 ? target : alloc_slot()
|
||||
emit_const_num(slot, expr.number)
|
||||
mark_slot(slot, is_integer(expr.number) ? "int" : "num")
|
||||
return slot
|
||||
}
|
||||
if (kind == "text") {
|
||||
@@ -1816,6 +1638,7 @@ var mcode = function(ast) {
|
||||
val = ""
|
||||
}
|
||||
emit_const_str(slot, val)
|
||||
mark_slot(slot, "text")
|
||||
return slot
|
||||
}
|
||||
// Template literal
|
||||
@@ -1852,6 +1675,7 @@ var mcode = function(ast) {
|
||||
// Call format(fmt_str, array)
|
||||
result_slot = target >= 0 ? target : alloc_slot()
|
||||
emit_call(result_slot, fmt_func_slot, [fmt_str_slot, arr_slot])
|
||||
mark_slot(result_slot, "text")
|
||||
return result_slot
|
||||
}
|
||||
if (kind == "regexp") {
|
||||
@@ -1870,16 +1694,19 @@ var mcode = function(ast) {
|
||||
if (kind == "true") {
|
||||
slot = target >= 0 ? target : alloc_slot()
|
||||
emit_const_bool(slot, true)
|
||||
mark_slot(slot, "bool")
|
||||
return slot
|
||||
}
|
||||
if (kind == "false") {
|
||||
slot = target >= 0 ? target : alloc_slot()
|
||||
emit_const_bool(slot, false)
|
||||
mark_slot(slot, "bool")
|
||||
return slot
|
||||
}
|
||||
if (kind == "null") {
|
||||
slot = target >= 0 ? target : alloc_slot()
|
||||
emit_const_null(slot)
|
||||
mark_slot(slot, null)
|
||||
return slot
|
||||
}
|
||||
if (kind == "this") {
|
||||
@@ -2774,6 +2601,7 @@ var mcode = function(ast) {
|
||||
s_instructions = []
|
||||
s_vars = []
|
||||
s_intrinsic_cache = []
|
||||
s_slot_types = {}
|
||||
s_loop_break = null
|
||||
s_loop_continue = null
|
||||
s_label_map = {}
|
||||
@@ -2972,6 +2800,7 @@ var mcode = function(ast) {
|
||||
s_max_slot = 1
|
||||
s_label_counter = 0
|
||||
s_func_counter = 0
|
||||
s_slot_types = {}
|
||||
s_loop_break = null
|
||||
s_loop_continue = null
|
||||
s_label_map = {}
|
||||
|
||||
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()
|
||||
276
source/mach.c
276
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) */
|
||||
@@ -372,26 +345,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",
|
||||
@@ -1080,10 +1034,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 +1092,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 +1375,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),
|
||||
@@ -2062,6 +2005,7 @@ vm_dispatch:
|
||||
}
|
||||
target = next;
|
||||
}
|
||||
stone_mutable_text(target->slots[c]);
|
||||
frame->slots[a] = target->slots[c];
|
||||
VM_BREAK();
|
||||
}
|
||||
@@ -2171,6 +2115,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 +2283,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 */
|
||||
@@ -2622,7 +2532,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 +2549,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 +2562,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 +2618,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 +2643,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 +2900,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 +2970,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 +2990,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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
572
streamline.cm
572
streamline.cm
@@ -41,14 +41,8 @@ var streamline = function(ir, log) {
|
||||
max: true, min: true, pow: true
|
||||
}
|
||||
var bool_result_ops = {
|
||||
eq_int: true, ne_int: true, lt_int: true, gt_int: true,
|
||||
le_int: true, ge_int: true,
|
||||
eq_float: true, ne_float: true, lt_float: true, gt_float: true,
|
||||
le_float: true, ge_float: true,
|
||||
eq_text: true, ne_text: true, lt_text: true, gt_text: true,
|
||||
le_text: true, ge_text: true,
|
||||
eq_bool: true, ne_bool: true,
|
||||
eq_tol: true, ne_tol: true,
|
||||
eq: true, ne: true, lt: true, gt: true, le: true, ge: true,
|
||||
eq_tol: true, ne_tol: true, in: true,
|
||||
not: true, and: true, or: true,
|
||||
is_int: true, is_text: true, is_num: true,
|
||||
is_bool: true, is_null: true, is_identical: true,
|
||||
@@ -63,22 +57,18 @@ var streamline = function(ir, log) {
|
||||
|
||||
// simplify_algebra dispatch tables
|
||||
var self_true_ops = {
|
||||
eq_int: true, eq_float: true, eq_text: true, eq_bool: true,
|
||||
is_identical: true,
|
||||
le_int: true, le_float: true, le_text: true,
|
||||
ge_int: true, ge_float: true, ge_text: true
|
||||
eq: true, is_identical: true, le: true, ge: true
|
||||
}
|
||||
var self_false_ops = {
|
||||
ne_int: true, ne_float: true, ne_text: true, ne_bool: true,
|
||||
lt_int: true, lt_float: true, lt_text: true,
|
||||
gt_int: true, gt_float: true, gt_text: true
|
||||
ne: true, lt: true, gt: true
|
||||
}
|
||||
var no_clear_ops = {
|
||||
int: true, access: true, true: true, false: true, move: true, null: true,
|
||||
jump: true, jump_true: true, jump_false: true, jump_not_null: true,
|
||||
return: true, disrupt: true,
|
||||
store_field: true, store_index: true, store_dynamic: true,
|
||||
push: true, setarg: true, invoke: true, tail_invoke: true
|
||||
push: true, setarg: true, invoke: true, tail_invoke: true,
|
||||
stone_text: true
|
||||
}
|
||||
|
||||
// --- Logging support ---
|
||||
@@ -141,6 +131,13 @@ var streamline = function(ir, log) {
|
||||
}
|
||||
|
||||
// track_types reuses write_rules table; move handled specially
|
||||
// Ops safe to narrow from T_NUM to T_INT when both operands are T_INT.
|
||||
// Excludes divide (int/int can produce float) and pow (int**neg produces float).
|
||||
var int_narrowable_ops = {
|
||||
add: true, subtract: true, multiply: true,
|
||||
remainder: true, modulo: true, max: true, min: true
|
||||
}
|
||||
|
||||
var track_types = function(slot_types, instr) {
|
||||
var op = instr[0]
|
||||
var rule = null
|
||||
@@ -157,6 +154,13 @@ var streamline = function(ir, log) {
|
||||
if (typ == null) {
|
||||
typ = access_value_type(instr[2])
|
||||
}
|
||||
// Narrow T_NUM to T_INT when both operands are T_INT
|
||||
if (typ == T_NUM && instr[3] != null && int_narrowable_ops[op] == true) {
|
||||
if (slot_is(slot_types, instr[2], T_INT)
|
||||
&& slot_is(slot_types, instr[3], T_INT)) {
|
||||
typ = T_INT
|
||||
}
|
||||
}
|
||||
slot_types[instr[rule[0]]] = typ
|
||||
}
|
||||
return null
|
||||
@@ -350,13 +354,6 @@ var streamline = function(ir, log) {
|
||||
load_index: [1, T_UNKNOWN], load_dynamic: [1, T_UNKNOWN],
|
||||
pop: [1, T_UNKNOWN], get: [1, T_UNKNOWN],
|
||||
invoke: [2, T_UNKNOWN], tail_invoke: [2, T_UNKNOWN],
|
||||
eq_int: [1, T_BOOL], ne_int: [1, T_BOOL], lt_int: [1, T_BOOL],
|
||||
gt_int: [1, T_BOOL], le_int: [1, T_BOOL], ge_int: [1, T_BOOL],
|
||||
eq_float: [1, T_BOOL], ne_float: [1, T_BOOL], lt_float: [1, T_BOOL],
|
||||
gt_float: [1, T_BOOL], le_float: [1, T_BOOL], ge_float: [1, T_BOOL],
|
||||
eq_text: [1, T_BOOL], ne_text: [1, T_BOOL], lt_text: [1, T_BOOL],
|
||||
gt_text: [1, T_BOOL], le_text: [1, T_BOOL], ge_text: [1, T_BOOL],
|
||||
eq_bool: [1, T_BOOL], ne_bool: [1, T_BOOL],
|
||||
eq_tol: [1, T_BOOL], ne_tol: [1, T_BOOL],
|
||||
not: [1, T_BOOL], and: [1, T_BOOL], or: [1, T_BOOL],
|
||||
is_int: [1, T_BOOL], is_text: [1, T_BOOL], is_num: [1, T_BOOL],
|
||||
@@ -373,6 +370,34 @@ var streamline = function(ir, log) {
|
||||
max: T_NUM, min: T_NUM, remainder: T_NUM, modulo: T_NUM
|
||||
}
|
||||
|
||||
var narrow_arith_type = function(write_types, param_types, instr, typ) {
|
||||
var s2 = null
|
||||
var s3 = null
|
||||
var t2 = null
|
||||
var t3 = null
|
||||
if (typ != T_NUM || instr[3] == null || int_narrowable_ops[instr[0]] != true) {
|
||||
return typ
|
||||
}
|
||||
s2 = instr[2]
|
||||
s3 = instr[3]
|
||||
if (is_number(s2)) {
|
||||
t2 = write_types[s2]
|
||||
if (t2 == null && param_types != null && s2 < length(param_types)) {
|
||||
t2 = param_types[s2]
|
||||
}
|
||||
}
|
||||
if (is_number(s3)) {
|
||||
t3 = write_types[s3]
|
||||
if (t3 == null && param_types != null && s3 < length(param_types)) {
|
||||
t3 = param_types[s3]
|
||||
}
|
||||
}
|
||||
if (t2 == T_INT && t3 == T_INT) {
|
||||
return T_INT
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
var infer_slot_write_types = function(func, param_types) {
|
||||
var instructions = func.instructions
|
||||
var nr_args = func.nr_args != null ? func.nr_args : 0
|
||||
@@ -457,6 +482,19 @@ var streamline = function(ir, log) {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
if (op == "get" && func._closure_slot_types != null) {
|
||||
slot = instr[1]
|
||||
typ = T_UNKNOWN
|
||||
src_typ = func._closure_slot_types[text(instr[2]) + "_" + text(instr[3])]
|
||||
if (src_typ != null) {
|
||||
typ = src_typ
|
||||
}
|
||||
if (slot > 0 && slot > nr_args) {
|
||||
merge_backward(write_types, slot, typ)
|
||||
}
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
rule = write_rules[op]
|
||||
if (rule != null) {
|
||||
@@ -465,6 +503,7 @@ var streamline = function(ir, log) {
|
||||
if (typ == null) {
|
||||
typ = access_value_type(instr[2])
|
||||
}
|
||||
typ = narrow_arith_type(write_types, param_types, instr, typ)
|
||||
if (slot > 0 && slot > nr_args) {
|
||||
merge_backward(write_types, slot, typ)
|
||||
}
|
||||
@@ -1058,7 +1097,9 @@ var streamline = function(ir, log) {
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Pass: eliminate_moves — move a, a → nop
|
||||
// Pass: eliminate_moves — copy propagation + self-move nop
|
||||
// Tracks move chains within basic blocks, substitutes read
|
||||
// operands to use the original source, and nops self-moves.
|
||||
// =========================================================
|
||||
var eliminate_moves = function(func, log) {
|
||||
var instructions = func.instructions
|
||||
@@ -1067,6 +1108,19 @@ var streamline = function(ir, log) {
|
||||
var i = 0
|
||||
var instr = null
|
||||
var events = null
|
||||
var copies = null
|
||||
var key = null
|
||||
var actual = null
|
||||
var dest = 0
|
||||
var src = 0
|
||||
var wr = null
|
||||
var write_pos = null
|
||||
var op = null
|
||||
var j = 0
|
||||
var k = 0
|
||||
var keys = null
|
||||
var special = null
|
||||
var limit = 0
|
||||
|
||||
if (instructions == null || length(instructions) == 0) {
|
||||
return null
|
||||
@@ -1076,24 +1130,321 @@ var streamline = function(ir, log) {
|
||||
events = log.events
|
||||
}
|
||||
|
||||
copies = {}
|
||||
num_instr = length(instructions)
|
||||
i = 0
|
||||
while (i < num_instr) {
|
||||
instr = instructions[i]
|
||||
if (is_array(instr) && instr[0] == "move" && instr[1] == instr[2]) {
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_mv_" + text(nc)
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite", pass: "eliminate_moves",
|
||||
rule: "self_move", at: i,
|
||||
before: instr, after: instructions[i]
|
||||
|
||||
// Labels: clear copies at join points
|
||||
if (is_text(instr)) {
|
||||
if (!starts_with(instr, "_nop_")) {
|
||||
copies = {}
|
||||
}
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (!is_array(instr)) {
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
op = instr[0]
|
||||
|
||||
// Control flow without reads: clear copies
|
||||
if (op == "jump" || op == "disrupt") {
|
||||
copies = {}
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Control flow with a read at position 1: substitute then clear
|
||||
if (op == "return" || op == "jump_true" || op == "jump_false" || op == "jump_not_null") {
|
||||
actual = copies[text(instr[1])]
|
||||
if (actual != null) {
|
||||
instr[1] = actual
|
||||
}
|
||||
copies = {}
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// Move: copy propagation
|
||||
if (op == "move") {
|
||||
dest = instr[1]
|
||||
src = instr[2]
|
||||
|
||||
// Follow transitive chain for src
|
||||
actual = copies[text(src)]
|
||||
if (actual == null) {
|
||||
actual = src
|
||||
}
|
||||
|
||||
// Rewrite the move's src operand
|
||||
instr[2] = actual
|
||||
|
||||
// Kill stale entries for dest
|
||||
key = text(dest)
|
||||
copies[key] = null
|
||||
keys = array(copies)
|
||||
k = 0
|
||||
while (k < length(keys)) {
|
||||
if (copies[keys[k]] == dest) {
|
||||
copies[keys[k]] = null
|
||||
}
|
||||
k = k + 1
|
||||
}
|
||||
|
||||
// Record the new copy
|
||||
copies[text(dest)] = actual
|
||||
|
||||
// Self-move after substitution → nop
|
||||
if (dest == actual) {
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_mv_" + text(nc)
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "rewrite", pass: "eliminate_moves",
|
||||
rule: "self_move", at: i,
|
||||
before: ["move", dest, src], after: instructions[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = i + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// General instruction: substitute reads, then kill write
|
||||
wr = write_rules[op]
|
||||
write_pos = null
|
||||
if (wr != null) {
|
||||
write_pos = wr[0]
|
||||
}
|
||||
|
||||
// Substitute read operands
|
||||
special = slot_idx_special[op]
|
||||
if (special != null) {
|
||||
j = 0
|
||||
while (j < length(special)) {
|
||||
k = special[j]
|
||||
if (k != write_pos && is_number(instr[k])) {
|
||||
actual = copies[text(instr[k])]
|
||||
if (actual != null) {
|
||||
instr[k] = actual
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
} else {
|
||||
limit = length(instr) - 2
|
||||
j = 1
|
||||
while (j < limit) {
|
||||
if (j != write_pos && is_number(instr[j])) {
|
||||
actual = copies[text(instr[j])]
|
||||
if (actual != null) {
|
||||
instr[j] = actual
|
||||
}
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
}
|
||||
|
||||
// Kill write destination
|
||||
if (write_pos != null && is_number(instr[write_pos])) {
|
||||
dest = instr[write_pos]
|
||||
key = text(dest)
|
||||
copies[key] = null
|
||||
keys = array(copies)
|
||||
k = 0
|
||||
while (k < length(keys)) {
|
||||
if (copies[keys[k]] == dest) {
|
||||
copies[keys[k]] = null
|
||||
}
|
||||
k = k + 1
|
||||
}
|
||||
}
|
||||
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Pass: insert_stone_text — freeze mutable text at escape points
|
||||
// Only inserts stone_text when the slot is provably T_TEXT.
|
||||
// Escape points: setfield, setindex, store_field, store_index,
|
||||
// store_dynamic, push, setarg, put (value leaving its slot).
|
||||
// move: stone source only if source is still live after the move.
|
||||
// =========================================================
|
||||
|
||||
// Map: escape opcode → index of the escaping slot in the instruction
|
||||
var escape_slot_index = {
|
||||
setfield: 3, setindex: 3,
|
||||
store_field: 3, store_index: 3, store_dynamic: 3,
|
||||
push: 2, setarg: 3, put: 1
|
||||
}
|
||||
|
||||
// Build last_ref liveness array for a function's instructions.
|
||||
// Returns array where last_ref[slot] = last instruction index referencing that slot.
|
||||
// Uses get_slot_refs to only visit actual slot reference positions.
|
||||
var build_slot_liveness = function(instructions, nr_slots) {
|
||||
var last_ref = array(nr_slots, -1)
|
||||
var n = length(instructions)
|
||||
var refs = null
|
||||
var i = 0
|
||||
var j = 0
|
||||
var s = 0
|
||||
var instr = null
|
||||
var label_map = null
|
||||
var changed = false
|
||||
var op = null
|
||||
var target = null
|
||||
var tpos = 0
|
||||
|
||||
// Scan instructions for slot references
|
||||
while (i < n) {
|
||||
instr = instructions[i]
|
||||
if (is_array(instr)) {
|
||||
refs = get_slot_refs(instr)
|
||||
j = 0
|
||||
while (j < length(refs)) {
|
||||
s = instr[refs[j]]
|
||||
if (is_number(s) && s >= 0 && s < nr_slots) {
|
||||
last_ref[s] = i
|
||||
}
|
||||
j = j + 1
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
// Extend for backward jumps (loops)
|
||||
label_map = {}
|
||||
i = 0
|
||||
while (i < n) {
|
||||
instr = instructions[i]
|
||||
if (is_text(instr) && !starts_with(instr, "_nop_")) {
|
||||
label_map[instr] = i
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
changed = true
|
||||
while (changed) {
|
||||
changed = false
|
||||
i = 0
|
||||
while (i < n) {
|
||||
instr = instructions[i]
|
||||
if (is_array(instr)) {
|
||||
target = null
|
||||
op = instr[0]
|
||||
if (op == "jump") {
|
||||
target = instr[1]
|
||||
} else if (op == "jump_true" || op == "jump_false" || op == "jump_not_null") {
|
||||
target = instr[2]
|
||||
}
|
||||
if (target != null && is_text(target)) {
|
||||
tpos = label_map[target]
|
||||
if (tpos != null && tpos < i) {
|
||||
s = 0
|
||||
while (s < nr_slots) {
|
||||
if (last_ref[s] >= 0 && last_ref[s] >= tpos && last_ref[s] < i) {
|
||||
last_ref[s] = i
|
||||
changed = true
|
||||
}
|
||||
s = s + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
return last_ref
|
||||
}
|
||||
|
||||
var insert_stone_text = function(func, log) {
|
||||
var instructions = func.instructions
|
||||
var nr_slots = func.nr_slots
|
||||
var dpc = func.disruption_pc
|
||||
var events = null
|
||||
var slot_types = null
|
||||
var result = null
|
||||
var i = 0
|
||||
var n = 0
|
||||
var instr = null
|
||||
var op = null
|
||||
var esc = null
|
||||
var slot = 0
|
||||
var nc = 0
|
||||
var shift = 0
|
||||
var last_ref = null
|
||||
|
||||
if (instructions == null || length(instructions) == 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (log != null && log.events != null) {
|
||||
events = log.events
|
||||
}
|
||||
|
||||
// Build liveness info (in separate function to stay under slot limit)
|
||||
last_ref = build_slot_liveness(instructions, nr_slots)
|
||||
|
||||
// Walk instructions, tracking types, inserting stone_text
|
||||
n = length(instructions)
|
||||
slot_types = array(nr_slots, T_UNKNOWN)
|
||||
result = []
|
||||
i = 0
|
||||
while (i < n) {
|
||||
instr = instructions[i]
|
||||
if (is_array(instr)) {
|
||||
op = instr[0]
|
||||
esc = escape_slot_index[op]
|
||||
if (esc != null) {
|
||||
slot = instr[esc]
|
||||
if (is_number(slot) && slot_is(slot_types, slot, T_TEXT)) {
|
||||
result[] = ["stone_text", slot]
|
||||
nc = nc + 1
|
||||
if (is_number(dpc) && i < dpc) shift = shift + 1
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "insert", pass: "insert_stone_text",
|
||||
rule: "escape_stone", at: i, slot: slot, op: op
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (op == "move") {
|
||||
// Stone source before move only if source is provably text
|
||||
// AND source slot is still live after this instruction
|
||||
slot = instr[2]
|
||||
if (is_number(slot) && slot_is(slot_types, slot, T_TEXT) && last_ref[slot] > i) {
|
||||
result[] = ["stone_text", slot]
|
||||
nc = nc + 1
|
||||
if (is_number(dpc) && i < dpc) shift = shift + 1
|
||||
if (events != null) {
|
||||
events[] = {
|
||||
event: "insert", pass: "insert_stone_text",
|
||||
rule: "move_alias_stone", at: i, slot: slot
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
track_types(slot_types, instr)
|
||||
}
|
||||
result[] = instr
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
if (nc > 0) {
|
||||
func.instructions = result
|
||||
if (is_number(dpc) && shift > 0) {
|
||||
func.disruption_pc = dpc + shift
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1212,7 +1563,7 @@ var streamline = function(ir, log) {
|
||||
|
||||
idx = 0
|
||||
while (idx < num_instr) {
|
||||
if (!reachable[idx] && is_array(instructions[idx])) {
|
||||
if (!reachable[idx] && is_array(instructions[idx]) && (disruption_pc < 0 || idx >= disruption_pc)) {
|
||||
nc = nc + 1
|
||||
instructions[idx] = "_nop_ucfg_" + text(nc)
|
||||
}
|
||||
@@ -1254,6 +1605,10 @@ var streamline = function(ir, log) {
|
||||
while (j < num_instr) {
|
||||
peek = instructions[j]
|
||||
if (is_text(peek)) {
|
||||
if (starts_with(peek, "_nop_")) {
|
||||
j = j + 1
|
||||
continue
|
||||
}
|
||||
if (peek == target_label) {
|
||||
nc = nc + 1
|
||||
instructions[i] = "_nop_dj_" + text(nc)
|
||||
@@ -1299,7 +1654,8 @@ var streamline = function(ir, log) {
|
||||
frame: [1, 2], goframe: [1, 2],
|
||||
jump: [], disrupt: [],
|
||||
jump_true: [1], jump_false: [1], jump_not_null: [1],
|
||||
return: [1]
|
||||
return: [1],
|
||||
stone_text: [1]
|
||||
}
|
||||
|
||||
var get_slot_refs = function(instr) {
|
||||
@@ -1318,6 +1674,54 @@ var streamline = function(ir, log) {
|
||||
return result
|
||||
}
|
||||
|
||||
// DEF/USE classification: which instruction positions are definitions vs uses
|
||||
var slot_def_special = {
|
||||
get: [1], put: [], access: [1], int: [1], function: [1], regexp: [1],
|
||||
true: [1], false: [1], null: [1], record: [1], array: [1],
|
||||
invoke: [2], tail_invoke: [2], goinvoke: [],
|
||||
move: [1], load_field: [1], load_index: [1], load_dynamic: [1],
|
||||
pop: [1], frame: [1], goframe: [1],
|
||||
setarg: [], store_field: [], store_index: [], store_dynamic: [],
|
||||
push: [], set_var: [], stone_text: [],
|
||||
jump: [], jump_true: [], jump_false: [], jump_not_null: [],
|
||||
return: [], disrupt: []
|
||||
}
|
||||
|
||||
var slot_use_special = {
|
||||
get: [], put: [1], access: [], int: [], function: [], regexp: [],
|
||||
true: [], false: [], null: [], record: [], array: [],
|
||||
invoke: [1], tail_invoke: [1], goinvoke: [1],
|
||||
move: [2], load_field: [2], load_index: [2, 3], load_dynamic: [2, 3],
|
||||
pop: [2], frame: [2], goframe: [2],
|
||||
setarg: [1, 3], store_field: [1, 3], store_index: [1, 2, 3],
|
||||
store_dynamic: [1, 2, 3],
|
||||
push: [1, 2], set_var: [1], stone_text: [1],
|
||||
jump: [], jump_true: [1], jump_false: [1], jump_not_null: [1],
|
||||
return: [1], disrupt: []
|
||||
}
|
||||
|
||||
var get_slot_defs = function(instr) {
|
||||
var special = slot_def_special[instr[0]]
|
||||
if (special != null) return special
|
||||
return [1]
|
||||
}
|
||||
|
||||
var get_slot_uses = function(instr) {
|
||||
var special = slot_use_special[instr[0]]
|
||||
var result = null
|
||||
var j = 0
|
||||
var limit = 0
|
||||
if (special != null) return special
|
||||
result = []
|
||||
limit = length(instr) - 2
|
||||
j = 2
|
||||
while (j < limit) {
|
||||
if (is_number(instr[j])) result[] = j
|
||||
j = j + 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var compress_one_fn = function(func, captured_slots) {
|
||||
var instructions = func.instructions
|
||||
var nr_slots = func.nr_slots
|
||||
@@ -1853,6 +2257,71 @@ var streamline = function(ir, log) {
|
||||
}
|
||||
fi = fi + 1
|
||||
}
|
||||
ir._parent_of = parent_of
|
||||
ir._parent_fc = fc
|
||||
return null
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// Resolve closure slot types from parent write_types.
|
||||
// For each `get` in func, walk the parent chain and look up
|
||||
// the ancestor's inferred write type for that closure slot.
|
||||
// =========================================================
|
||||
var resolve_closure_types = function(func, fi, ir) {
|
||||
var parent_of = ir._parent_of
|
||||
var fc = ir._parent_fc
|
||||
var instructions = func.instructions
|
||||
var num_instr = 0
|
||||
var closure_types = null
|
||||
var i = 0
|
||||
var instr = null
|
||||
var slot = 0
|
||||
var depth = 0
|
||||
var anc = 0
|
||||
var j = 0
|
||||
var target = null
|
||||
var typ = null
|
||||
var key = null
|
||||
|
||||
if (instructions == null || parent_of == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
num_instr = length(instructions)
|
||||
closure_types = {}
|
||||
i = 0
|
||||
while (i < num_instr) {
|
||||
instr = instructions[i]
|
||||
if (is_array(instr) && instr[0] == "get") {
|
||||
slot = instr[2]
|
||||
depth = instr[3]
|
||||
key = text(slot) + "_" + text(depth)
|
||||
if (closure_types[key] == null) {
|
||||
anc = fi
|
||||
j = 0
|
||||
while (j < depth && anc >= 0) {
|
||||
anc = parent_of[anc]
|
||||
j = j + 1
|
||||
}
|
||||
if (anc >= 0) {
|
||||
if (anc == fc) {
|
||||
target = ir.main
|
||||
} else {
|
||||
target = ir.functions[anc]
|
||||
}
|
||||
if (target != null && target._write_types != null) {
|
||||
typ = target._write_types[slot]
|
||||
if (typ != null) {
|
||||
closure_types[key] = typ
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
func._closure_slot_types = closure_types
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2095,12 +2564,14 @@ var streamline = function(ir, log) {
|
||||
var slot_types = null
|
||||
var run_cycle = function(suffix) {
|
||||
var name = null
|
||||
name = "infer_param_types" + suffix
|
||||
run_pass(func, name, function() {
|
||||
param_types = infer_param_types(func)
|
||||
return param_types
|
||||
})
|
||||
if (verify_fn) verify_fn(func, "after " + name)
|
||||
if (param_types == null) {
|
||||
name = "infer_param_types" + suffix
|
||||
run_pass(func, name, function() {
|
||||
param_types = infer_param_types(func)
|
||||
return param_types
|
||||
})
|
||||
if (verify_fn) verify_fn(func, "after " + name)
|
||||
}
|
||||
|
||||
name = "infer_slot_write_types" + suffix
|
||||
run_pass(func, name, function() {
|
||||
@@ -2154,6 +2625,12 @@ var streamline = function(ir, log) {
|
||||
return eliminate_dead_jumps(func, log)
|
||||
})
|
||||
if (verify_fn) verify_fn(func, "after " + name)
|
||||
|
||||
name = "eliminate_unreachable_cfg" + suffix
|
||||
run_pass(func, name, function() {
|
||||
return eliminate_unreachable_cfg(func)
|
||||
})
|
||||
if (verify_fn) verify_fn(func, "after " + name)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -2162,6 +2639,8 @@ var streamline = function(ir, log) {
|
||||
}
|
||||
|
||||
run_cycle("")
|
||||
run_cycle("_2")
|
||||
func._write_types = write_types
|
||||
if (ir._warn) {
|
||||
diagnose_function(func, {param_types: param_types, write_types: write_types}, ir)
|
||||
}
|
||||
@@ -2181,14 +2660,17 @@ var streamline = function(ir, log) {
|
||||
// Process main function
|
||||
if (ir.main != null) {
|
||||
optimize_function(ir.main, log)
|
||||
insert_stone_text(ir.main, log)
|
||||
}
|
||||
|
||||
// Process all sub-functions
|
||||
// Process all sub-functions (resolve closure types from parent first)
|
||||
var fi = 0
|
||||
if (ir.functions != null) {
|
||||
fi = 0
|
||||
while (fi < length(ir.functions)) {
|
||||
resolve_closure_types(ir.functions[fi], fi, ir)
|
||||
optimize_function(ir.functions[fi], log)
|
||||
insert_stone_text(ir.functions[fi], log)
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
@@ -2196,6 +2678,14 @@ var streamline = function(ir, log) {
|
||||
// Compress slots across all functions (must run after per-function passes)
|
||||
compress_slots(ir)
|
||||
|
||||
// Expose DEF/USE functions via log if requested
|
||||
if (log != null) {
|
||||
if (log.request_def_use) {
|
||||
log.get_slot_defs = get_slot_defs
|
||||
log.get_slot_uses = get_slot_uses
|
||||
}
|
||||
}
|
||||
|
||||
return ir
|
||||
}
|
||||
|
||||
|
||||
39
vm_suite.ce
39
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
|
||||
// ============================================================================
|
||||
@@ -2738,6 +2761,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
|
||||
// ============================================================================
|
||||
|
||||
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