From 2ac446f7cfc24b7ac216a77b7101b3fc2feb13ac Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 21 Feb 2026 15:05:57 -0600 Subject: [PATCH] inline fns --- streamline.cm | 389 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 386 insertions(+), 3 deletions(-) diff --git a/streamline.cm b/streamline.cm index c1aa2704..9e6883c4 100644 --- a/streamline.cm +++ b/streamline.cm @@ -2564,6 +2564,337 @@ var streamline = function(ir, log) { return null } + // ========================================================= + // Sensory function IR synthesis for callback inlining + // ========================================================= + var sensory_opcodes = { + is_array: "is_array", is_function: "is_func", is_object: "is_record", + is_stone: "is_stone", is_integer: "is_int", is_text: "is_text", + is_number: "is_num", is_logical: "is_bool", is_null: "is_null", + is_blob: "is_blob", is_data: "is_data", + is_true: "is_true", is_false: "is_false", is_fit: "is_fit", + is_character: "is_char", is_digit: "is_digit", is_letter: "is_letter", + is_lower: "is_lower", is_upper: "is_upper", is_whitespace: "is_ws", + is_actor: "is_actor", length: "length" + } + + var make_sensory_ir = function(name) { + var opcode = sensory_opcodes[name] + if (opcode == null) return null + return { + name: name, nr_args: 1, nr_close_slots: 0, nr_slots: 3, + instructions: [[opcode, 2, 1, 0, 0], ["return", 2, 0, 0]] + } + } + + // ========================================================= + // Inline eligibility check + // ========================================================= + var prefer_inline_set = { + filter: true, every: true, some: true, arrfor: true, + reduce: true, array: true + } + + var can_inline = function(callee_func, is_prefer) { + var instrs = null + var i = 0 + var instr = null + var count = 0 + var limit = 0 + if (callee_func.nr_close_slots > 0) return false + instrs = callee_func.instructions + if (instrs == null) return false + i = 0 + while (i < length(instrs)) { + instr = instrs[i] + if (is_array(instr)) { + if (instr[0] == "get" || instr[0] == "put") { + return false + } + // Reject if function creates child functions (closures may capture + // from the inlined frame, breaking get/put slot references) + if (instr[0] == "function") { + return false + } + } + i = i + 1 + } + if (callee_func.disruption_pc != null && callee_func.disruption_pc > 0) { + return false + } + count = 0 + i = 0 + while (i < length(instrs)) { + if (is_array(instrs[i])) count = count + 1 + i = i + 1 + } + limit = is_prefer ? 200 : 40 + return count <= limit + } + + // ========================================================= + // Pass: inline_calls — inline same-module + sensory functions + // ========================================================= + var inline_counter = 0 + + var inline_calls = function(func, ir, log) { + var instructions = func.instructions + var num_instr = 0 + var i = 0 + var j = 0 + var k = 0 + var instr = null + var op = null + var changed = false + var inline_count = 0 + var max_inlines = 20 + var slot_to_func_idx = {} + var slot_to_intrinsic = {} + var callee_slot = 0 + var frame_slot = 0 + var argc = 0 + var result_slot = 0 + var call_start = 0 + var call_end = 0 + var arg_slots = null + var callee_func = null + var is_prefer = false + var base = 0 + var remap = null + var cinstr = null + var cop = null + var new_instr = null + var refs = null + var label_prefix = null + var cont_label = null + var spliced = null + var before = null + var after = null + var inlined_body = null + var fi = null + var intrinsic_name = null + + if (instructions == null) return false + num_instr = length(instructions) + if (num_instr == 0) return false + + // Build resolution maps + i = 0 + while (i < num_instr) { + instr = instructions[i] + if (is_array(instr)) { + op = instr[0] + if (op == "function") { + slot_to_func_idx[text(instr[1])] = instr[2] + } else if (op == "access" && is_object(instr[2]) && instr[2].make == "intrinsic") { + slot_to_intrinsic[text(instr[1])] = instr[2].name + } + } + i = i + 1 + } + + // Scan for frame/setarg/invoke sequences and inline + i = 0 + while (i < length(instructions)) { + instr = instructions[i] + if (!is_array(instr) || instr[0] != "frame") { + i = i + 1 + continue + } + if (inline_count >= max_inlines) { + i = i + 1 + continue + } + + frame_slot = instr[1] + callee_slot = instr[2] + argc = instr[3] + call_start = i + + // Collect setarg and find invoke + arg_slots = array(argc + 1, -1) + j = i + 1 + call_end = -1 + while (j < length(instructions)) { + instr = instructions[j] + if (!is_array(instr)) { + j = j + 1 + continue + } + op = instr[0] + if (op == "setarg" && instr[1] == frame_slot) { + arg_slots[instr[2]] = instr[3] + } else if ((op == "invoke" || op == "tail_invoke") && instr[1] == frame_slot) { + result_slot = instr[2] + call_end = j + j = j + 1 + break + } else if (op == "frame" || op == "goframe") { + // Another frame before invoke — abort this pattern + break + } + j = j + 1 + } + + if (call_end < 0) { + i = i + 1 + continue + } + + // Resolve callee + callee_func = null + is_prefer = false + + fi = slot_to_func_idx[text(callee_slot)] + if (fi != null && ir.functions != null && fi >= 0 && fi < length(ir.functions)) { + callee_func = ir.functions[fi] + } + + if (callee_func == null) { + intrinsic_name = slot_to_intrinsic[text(callee_slot)] + if (intrinsic_name != null) { + if (sensory_opcodes[intrinsic_name] != null) { + callee_func = make_sensory_ir(intrinsic_name) + } + if (callee_func != null) { + is_prefer = true + } + } + } + + if (callee_func == null) { + i = i + 1 + continue + } + + // Check eligibility + if (!can_inline(callee_func, is_prefer)) { + i = i + 1 + continue + } + + // Slot remapping + base = func.nr_slots + func.nr_slots = func.nr_slots + callee_func.nr_slots + remap = array(callee_func.nr_slots, -1) + + // Slot 0 (this) → arg_slots[0] if provided, else allocate a null slot + if (length(arg_slots) > 0 && arg_slots[0] >= 0) { + remap[0] = arg_slots[0] + } else { + remap[0] = base + } + + // Params 1..nr_args → corresponding arg_slots + j = 1 + while (j <= callee_func.nr_args) { + if (j < length(arg_slots) && arg_slots[j] >= 0) { + remap[j] = arg_slots[j] + } else { + remap[j] = base + j + } + j = j + 1 + } + + // Temporaries → fresh slots + j = callee_func.nr_args + 1 + while (j < callee_func.nr_slots) { + remap[j] = base + j + j = j + 1 + } + + // Generate unique label prefix + inline_counter = inline_counter + 1 + label_prefix = "_inl" + text(inline_counter) + "_" + cont_label = label_prefix + "cont" + + // Build inlined body with remapping + inlined_body = [] + k = 0 + while (k < length(callee_func.instructions)) { + cinstr = callee_func.instructions[k] + + // Labels (strings that aren't nop markers) + if (is_text(cinstr)) { + if (starts_with(cinstr, "_nop_")) { + inlined_body[] = cinstr + } else { + inlined_body[] = label_prefix + cinstr + } + k = k + 1 + continue + } + + if (!is_array(cinstr)) { + inlined_body[] = cinstr + k = k + 1 + continue + } + + cop = cinstr[0] + + // Handle return → move + jump to continuation + if (cop == "return") { + new_instr = ["move", result_slot, remap[cinstr[1]], cinstr[2], cinstr[3]] + inlined_body[] = new_instr + inlined_body[] = ["jump", cont_label, cinstr[2], cinstr[3]] + k = k + 1 + continue + } + + // Clone and remap the instruction + new_instr = array(cinstr) + refs = get_slot_refs(cinstr) + j = 0 + while (j < length(refs)) { + if (new_instr[refs[j]] >= 0 && new_instr[refs[j]] < length(remap)) { + new_instr[refs[j]] = remap[new_instr[refs[j]]] + } + j = j + 1 + } + + // Remap labels in jump instructions + if (cop == "jump" && is_text(cinstr[1]) && !starts_with(cinstr[1], "_nop_")) { + new_instr[1] = label_prefix + cinstr[1] + } else if ((cop == "jump_true" || cop == "jump_false" || cop == "jump_not_null") + && is_text(cinstr[2]) && !starts_with(cinstr[2], "_nop_")) { + new_instr[2] = label_prefix + cinstr[2] + } + + // Skip function instructions (don't inline nested function definitions) + if (cop == "function") { + // Keep the instruction but don't remap func_id — it still refers to ir.functions + // Only remap slot position 1 (the destination slot) + new_instr = array(cinstr) + if (cinstr[1] >= 0 && cinstr[1] < length(remap)) { + new_instr[1] = remap[cinstr[1]] + } + } + + inlined_body[] = new_instr + k = k + 1 + } + + // Add continuation label + inlined_body[] = cont_label + + // Splice: replace instructions[call_start..call_end] with inlined_body + before = array(instructions, 0, call_start) + after = array(instructions, call_end + 1, length(instructions)) + spliced = array(before, inlined_body) + instructions = array(spliced, after) + func.instructions = instructions + + changed = true + inline_count = inline_count + 1 + + // Continue scanning from after the inlined body + i = call_start + length(inlined_body) + } + + return changed + } + // ========================================================= // Compose all passes // ========================================================= @@ -2666,13 +2997,12 @@ var streamline = function(ir, log) { ir._diagnostics = [] } - // Process main function + // Phase 1: Optimize all functions (bottom-up, existing behavior) if (ir.main != null) { optimize_function(ir.main, log) insert_stone_text(ir.main, log) } - // Process all sub-functions (resolve closure types from parent first) var fi = 0 if (ir.functions != null) { fi = 0 @@ -2684,7 +3014,60 @@ var streamline = function(ir, log) { } } - // Compress slots across all functions (must run after per-function passes) + // Phase 2: Inline pass + var changed_main = false + var changed_fns = null + if (ir.main != null) { + changed_main = inline_calls(ir.main, ir, log) + } + if (ir.functions != null) { + changed_fns = array(length(ir.functions), false) + fi = 0 + while (fi < length(ir.functions)) { + changed_fns[fi] = inline_calls(ir.functions[fi], ir, log) + fi = fi + 1 + } + } + + // Phase 3: Re-optimize inlined functions + if (changed_main) { + optimize_function(ir.main, log) + insert_stone_text(ir.main, log) + } + if (ir.functions != null) { + fi = 0 + while (fi < length(ir.functions)) { + if (changed_fns != null && changed_fns[fi]) { + optimize_function(ir.functions[fi], log) + insert_stone_text(ir.functions[fi], log) + } + fi = fi + 1 + } + } + + // Phase 4: Cascade — second inline round (callbacks inside inlined bodies) + if (changed_main) { + changed_main = inline_calls(ir.main, ir, log) + if (changed_main) { + optimize_function(ir.main, log) + insert_stone_text(ir.main, log) + } + } + if (ir.functions != null) { + fi = 0 + while (fi < length(ir.functions)) { + if (changed_fns != null && changed_fns[fi]) { + changed_fns[fi] = inline_calls(ir.functions[fi], ir, log) + if (changed_fns[fi]) { + optimize_function(ir.functions[fi], log) + insert_stone_text(ir.functions[fi], log) + } + } + fi = fi + 1 + } + } + + // Phase 5: Compress slots across all functions (must run after per-function passes) compress_slots(ir) // Expose DEF/USE functions via log if requested