diff --git a/docs/spec/pipeline.md b/docs/spec/pipeline.md index fe1240ba..c8fda47d 100644 --- a/docs/spec/pipeline.md +++ b/docs/spec/pipeline.md @@ -47,16 +47,22 @@ Lowers the AST to a JSON-based intermediate representation with explicit operati - **Typed load/store**: Emits `load_index` (array by integer), `load_field` (record by string), or `load_dynamic` (unknown) based on type information from fold. - **Decomposed calls**: Function calls are split into `frame` (create call frame) + `setarg` (set arguments) + `invoke` (execute call). - **Intrinsic access**: Intrinsic functions are loaded via `access` with an intrinsic marker rather than global lookup. +- **Intrinsic inlining**: Type-check intrinsics (`is_array`, `is_text`, `is_number`, `is_integer`, `is_logical`, `is_null`, `is_function`, `is_object`, `is_stone`), `length`, and `push` are emitted as direct opcodes instead of frame/setarg/invoke call sequences. See [Mcode IR](mcode.md) for instruction format details. ### Streamline (`streamline.cm`) -Optimizes the Mcode IR. Operates per-function: +Optimizes the Mcode IR through a series of independent passes. Operates per-function: -- **Redundant instruction elimination**: Removes no-op patterns and redundant moves. -- **Dead code removal**: Eliminates instructions whose results are never used. -- **Type-based narrowing**: When type information is available, narrows `load_dynamic`/`store_dynamic` to typed variants. +1. **Backward type inference**: Infers parameter types from how they are used in typed operators. Immutable `def` parameters keep their inferred type across label join points. +2. **Type-check elimination**: When a slot's type is known, eliminates `is_` + conditional jump pairs. Narrows `load_dynamic`/`store_dynamic` to typed variants. +3. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons. +4. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition. +5. **Move elimination**: Removes self-moves (`move a, a`). +6. **Dead jump elimination**: Removes jumps to the immediately following label. + +See [Streamline Optimizer](streamline.md) for detailed pass descriptions. ### QBE Emit (`qbe_emit.cm`) @@ -107,6 +113,14 @@ Generates QBE IL that can be compiled to native code. | `qbe.cm` | QBE IL operation templates | | `internal/bootstrap.cm` | Pipeline orchestrator | +## Debug Tools + +| File | Purpose | +|------|---------| +| `dump_mcode.cm` | Print raw Mcode IR before streamlining | +| `dump_stream.cm` | Print IR after streamlining with before/after stats | +| `dump_types.cm` | Print streamlined IR with type annotations | + ## Test Files | File | Tests | @@ -116,3 +130,5 @@ Generates QBE IL that can be compiled to native code. | `mcode_test.ce` | Typed load/store, decomposed calls | | `streamline_test.ce` | Optimization counts, IR before/after | | `qbe_test.ce` | End-to-end QBE IL generation | +| `test_intrinsics.cm` | Inlined intrinsic opcodes (is_array, length, push, etc.) | +| `test_backward.cm` | Backward type propagation for parameters | diff --git a/docs/spec/streamline.md b/docs/spec/streamline.md new file mode 100644 index 00000000..3d413695 --- /dev/null +++ b/docs/spec/streamline.md @@ -0,0 +1,202 @@ +--- +title: "Streamline Optimizer" +description: "Mcode IR optimization passes" +--- + +## Overview + +The streamline optimizer (`streamline.cm`) runs a series of independent passes over the Mcode IR to eliminate redundant operations. Each pass is a standalone function that can be enabled, disabled, or reordered. Passes communicate only through the instruction array they mutate in place, replacing eliminated instructions with nop strings (e.g., `_nop_tc_1`). + +The optimizer runs after `mcode.cm` generates the IR and before the result is lowered to the Mach VM or emitted as QBE IL. + +``` +Fold (AST) → Mcode (JSON IR) → Streamline → Mach VM / QBE +``` + +## Type Lattice + +The optimizer tracks a type for each slot in the register file: + +| Type | Meaning | +|------|---------| +| `unknown` | No type information | +| `int` | Integer | +| `float` | Floating-point | +| `num` | Number (subsumes int and float) | +| `text` | String | +| `bool` | Logical (true/false) | +| `null` | Null value | +| `array` | Array | +| `record` | Record (object) | +| `function` | Function | +| `blob` | Binary blob | + +Subsumption: `int` and `float` both satisfy a `num` check. + +## Passes + +### 1. infer_param_types (backward type inference) + +Scans all typed operators to determine what types their operands must be. For example, `add_int dest, a, b` implies both `a` and `b` are integers. + +When a parameter slot (1..nr_args) is consistently inferred as a single type, that type is recorded. Since parameters are immutable (`def`), the inferred type holds for the entire function and persists across label join points (loop headers, branch targets). + +Backward inference rules: + +| Operator class | Operand type inferred | +|---|---| +| `add_int`, `sub_int`, `mul_int`, `div_int`, `mod_int`, `eq_int`, comparisons, bitwise | T_INT | +| `add_float`, `sub_float`, `mul_float`, `div_float`, `mod_float`, float comparisons | T_FLOAT | +| `concat`, text comparisons | T_TEXT | +| `eq_bool`, `ne_bool`, `not`, `and`, `or` | T_BOOL | +| `store_index` (object operand) | T_ARRAY | +| `store_index` (index operand) | T_INT | +| `store_field` (object operand) | T_RECORD | +| `push` (array operand) | T_ARRAY | + +When a slot appears with conflicting type inferences (e.g., used in both `add_int` and `concat` across different type-dispatch branches), the result is `unknown`. INT + FLOAT conflicts produce `num`. + +**Nop prefix:** none (analysis only, does not modify instructions) + +### 2. eliminate_type_checks (type-check + jump elimination) + +Forward pass that tracks the known type of each slot. When a type check (`is_int`, `is_text`, `is_num`, etc.) is followed by a conditional jump, and the slot's type is already known, the check and jump can be eliminated or converted to an unconditional jump. + +Three cases: + +- **Known match** (e.g., `is_int` on a slot known to be `int`): both the check and the conditional jump are eliminated (nop'd). +- **Known mismatch** (e.g., `is_text` on a slot known to be `int`): the check is nop'd and the conditional jump is rewritten to an unconditional `jump`. +- **Unknown**: the check remains, but on fallthrough, the slot's type is narrowed to the checked type (enabling downstream eliminations). + +This pass also reduces `load_dynamic`/`store_dynamic` to `load_field`/`store_field` or `load_index`/`store_index` when the key slot's type is known. + +At label join points, all type information is reset except for parameter types seeded by the backward inference pass. + +**Nop prefix:** `_nop_tc_` + +### 3. simplify_algebra (algebraic identity + comparison folding) + +Tracks known constant values alongside types. Rewrites identity operations: + +| Pattern | Rewrite | +|---------|---------| +| `add_int dest, x, 0` | `move dest, x` | +| `add_int dest, 0, x` | `move dest, x` | +| `sub_int dest, x, 0` | `move dest, x` | +| `mul_int dest, x, 1` | `move dest, x` | +| `mul_int dest, 1, x` | `move dest, x` | +| `mul_int dest, x, 0` | `int dest, 0` | +| `div_int dest, x, 1` | `move dest, x` | +| `add_float dest, x, 0` | `move dest, x` | +| `mul_float dest, x, 1` | `move dest, x` | +| `div_float dest, x, 1` | `move dest, x` | + +Float multiplication by zero is intentionally not optimized because it is not safe with NaN and Inf values. + +Same-slot comparison folding: + +| Pattern | Rewrite | +|---------|---------| +| `eq_* dest, x, x` | `true dest` | +| `le_* dest, x, x` | `true dest` | +| `ge_* dest, x, x` | `true dest` | +| `is_identical dest, x, x` | `true dest` | +| `ne_* dest, x, x` | `false dest` | +| `lt_* dest, x, x` | `false dest` | +| `gt_* dest, x, x` | `false dest` | + +**Nop prefix:** none (rewrites in place, does not create nops) + +### 4. simplify_booleans (not + jump fusion) + +Peephole pass that eliminates unnecessary `not` instructions: + +| Pattern | Rewrite | +|---------|---------| +| `not d, x; jump_false d, L` | nop; `jump_true x, L` | +| `not d, x; jump_true d, L` | nop; `jump_false x, L` | +| `not d1, x; not d2, d1` | nop; `move d2, x` | + +This is particularly effective on `if (!cond)` patterns, which the compiler generates as `not; jump_false`. After this pass, they become a single `jump_true`. + +**Nop prefix:** `_nop_bl_` + +### 5. eliminate_moves (self-move elimination) + +Removes `move a, a` instructions where the source and destination are the same slot. These can arise from earlier passes rewriting binary operations into moves. + +**Nop prefix:** `_nop_mv_` + +### 6. eliminate_unreachable (dead code after return/disrupt) + +*Currently disabled.* Nops instructions after `return` or `disrupt` until the next real label. + +Disabled because disruption handler code is placed after the `return`/`disrupt` instruction without a label boundary. The VM dispatches to handlers via the `disruption_pc` offset, not through normal control flow. Re-enabling this pass requires the mcode compiler to emit labels at disruption handler entry points. + +**Nop prefix:** `_nop_ur_` + +### 7. 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_` + +## Pass Composition + +All passes run in sequence in `optimize_function`: + +``` +infer_param_types → returns param_types map +eliminate_type_checks → uses param_types +simplify_algebra +simplify_booleans +eliminate_moves +(eliminate_unreachable) → disabled +eliminate_dead_jumps +``` + +Each pass is independent and can be commented out for testing or benchmarking. + +## Intrinsic Inlining + +Before streamlining, `mcode.cm` recognizes calls to built-in intrinsic functions and emits direct opcodes instead of the generic frame/setarg/invoke call sequence. This reduces a 6-instruction call pattern to a single instruction: + +| Call | Emitted opcode | +|------|---------------| +| `is_array(x)` | `is_array dest, src` | +| `is_function(x)` | `is_func dest, src` | +| `is_object(x)` | `is_record dest, src` | +| `is_stone(x)` | `is_stone dest, src` | +| `is_integer(x)` | `is_int dest, src` | +| `is_text(x)` | `is_text dest, src` | +| `is_number(x)` | `is_num dest, src` | +| `is_logical(x)` | `is_bool dest, src` | +| `is_null(x)` | `is_null dest, src` | +| `length(x)` | `length dest, src` | +| `push(arr, val)` | `push arr, val` | + +These inlined opcodes have corresponding Mach VM implementations in `mach.c`. + +## Debugging Tools + +Three dump tools inspect the IR at different stages: + +- **`dump_mcode.cm`** — prints the raw Mcode IR after `mcode.cm`, before streamlining +- **`dump_stream.cm`** — prints the IR after streamlining, with before/after instruction counts +- **`dump_types.cm`** — prints the streamlined IR with type annotations on each instruction + +Usage: +``` +./cell --core . dump_mcode.cm +./cell --core . dump_stream.cm +./cell --core . dump_types.cm +``` + +## Nop Convention + +Eliminated instructions are replaced with strings matching `_nop__`. The prefix identifies which pass created the nop. Nop strings are: + +- Skipped during interpretation (the VM ignores them) +- Skipped during QBE emission +- Not counted in instruction statistics +- Preserved in the instruction array to maintain positional stability for jump targets diff --git a/dump_ast.cm b/dump_ast.cm new file mode 100644 index 00000000..9b73666a --- /dev/null +++ b/dump_ast.cm @@ -0,0 +1,12 @@ +var fd = use("fd") +var json = use("json") +var tokenize = use("tokenize") +var parse = use("parse") +var fold = use("fold") + +var filename = args[0] +var src = text(fd.slurp(filename)) +var tok = tokenize(src, filename) +var ast = parse(tok.tokens, src, filename, tokenize) +var folded = fold(ast) +print(json.encode(folded)) diff --git a/dump_mcode.cm b/dump_mcode.cm index 291b6e46..52395280 100644 --- a/dump_mcode.cm +++ b/dump_mcode.cm @@ -1,20 +1,117 @@ +// dump_mcode.cm — pretty-print mcode IR (before streamlining) +// +// Usage: ./cell --core . dump_mcode.cm + var fd = use("fd") var json = use("json") var tokenize = use("tokenize") var parse = use("parse") var fold = use("fold") var mcode = use("mcode") -var streamline = use("streamline") -var name = args[0] -var src = text(fd.slurp(name)) -var tok = tokenize(src, name) -var ast = parse(tok.tokens, src, name, tokenize) +if (length(args) < 1) { + print("usage: cell --core . dump_mcode.cm ") + return +} + +var filename = args[0] +var src = text(fd.slurp(filename)) +var tok = tokenize(src, filename) +var ast = parse(tok.tokens, src, filename, tokenize) var folded = fold(ast) var compiled = mcode(folded) -var optimized = streamline(compiled) -var out = json.encode(optimized) -var f = fd.open("/tmp/mcode_dump.json", "w") -fd.write(f, out) -fd.close(f) -print("wrote /tmp/mcode_dump.json") + +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 json.encode(v) + } + if (is_logical(v)) { + return v ? "true" : "false" + } + return text(v) +} + +var dump_function = function(func, name) { + var nr_args = func.nr_args != null ? func.nr_args : 0 + var nr_slots = func.nr_slots != null ? func.nr_slots : 0 + var nr_close = func.nr_close_slots != null ? func.nr_close_slots : 0 + var instrs = func.instructions + var i = 0 + var pc = 0 + var instr = null + var op = null + var n = 0 + var parts = null + var j = 0 + var operands = null + var pc_str = null + var op_str = null + print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}, closures=${text(nr_close)}) ===`) + if (instrs == null || length(instrs) == 0) { + print(" (empty)") + return null + } + while (i < length(instrs)) { + instr = instrs[i] + if (is_text(instr)) { + if (!starts_with(instr, "_nop_")) { + print(`${instr}:`) + } + } else if (is_array(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, ", ") + pc_str = pad_right(text(pc), 5) + op_str = pad_right(op, 14) + print(` ${pc_str} ${op_str} ${operands}`) + pc = pc + 1 + } + i = i + 1 + } + return null +} + +var main_name = null +var fi = 0 +var func = null +var fname = null + +// Dump main +if (compiled.main != null) { + main_name = compiled.name != null ? compiled.name : "
" + dump_function(compiled.main, main_name) +} + +// Dump sub-functions +if (compiled.functions != null) { + fi = 0 + while (fi < length(compiled.functions)) { + func = compiled.functions[fi] + fname = func.name != null ? func.name : `` + dump_function(func, `[${text(fi)}] ${fname}`) + fi = fi + 1 + } +} diff --git a/dump_stream.cm b/dump_stream.cm new file mode 100644 index 00000000..e74a0443 --- /dev/null +++ b/dump_stream.cm @@ -0,0 +1,166 @@ +// dump_stream.cm — show mcode IR before and after streamlining +// +// Usage: ./cell --core . dump_stream.cm + +var fd = use("fd") +var json = use("json") +var tokenize = use("tokenize") +var parse = use("parse") +var fold = use("fold") +var mcode = use("mcode") +var streamline = use("streamline") + +if (length(args) < 1) { + print("usage: cell --core . dump_stream.cm ") + return +} + +var filename = args[0] +var src = text(fd.slurp(filename)) +var tok = tokenize(src, filename) +var ast = parse(tok.tokens, src, filename, tokenize) +var folded = fold(ast) +var compiled = mcode(folded) + +// Deep copy IR for before snapshot +var before = json.decode(json.encode(compiled)) + +var optimized = streamline(compiled) + +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 json.encode(v) + } + if (is_logical(v)) { + return v ? "true" : "false" + } + return text(v) +} + +var count_stats = function(func) { + var instrs = func.instructions + var total = 0 + var nops = 0 + var calls = 0 + var i = 0 + var instr = null + if (instrs == null) { + return {total: 0, nops: 0, real: 0, calls: 0} + } + while (i < length(instrs)) { + instr = instrs[i] + if (is_text(instr)) { + if (starts_with(instr, "_nop_")) { + nops = nops + 1 + } + } else if (is_array(instr)) { + total = total + 1 + if (instr[0] == "invoke") { + calls = calls + 1 + } + } + i = i + 1 + } + return {total: total, nops: nops, real: total - nops, calls: calls} +} + +var dump_function = function(func, show_nops) { + var instrs = func.instructions + var i = 0 + var pc = 0 + var instr = null + var op = null + var n = 0 + var parts = null + var j = 0 + var operands = null + var pc_str = null + var op_str = null + if (instrs == null || length(instrs) == 0) { + return null + } + while (i < length(instrs)) { + instr = instrs[i] + if (is_text(instr)) { + if (starts_with(instr, "_nop_")) { + if (show_nops) { + print(` ${pad_right(text(pc), 5)} --- nop ---`) + pc = pc + 1 + } + } else { + print(`${instr}:`) + } + } else if (is_array(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, ", ") + pc_str = pad_right(text(pc), 5) + op_str = pad_right(op, 14) + print(` ${pc_str} ${op_str} ${operands}`) + pc = pc + 1 + } + i = i + 1 + } + return null +} + +var dump_pair = function(before_func, after_func, name) { + var nr_args = after_func.nr_args != null ? after_func.nr_args : 0 + var nr_slots = after_func.nr_slots != null ? after_func.nr_slots : 0 + var b_stats = count_stats(before_func) + var a_stats = count_stats(after_func) + var eliminated = a_stats.nops + print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`) + print(` before: ${text(b_stats.total)} instructions, ${text(b_stats.calls)} invokes`) + print(` after: ${text(a_stats.real)} instructions (${text(eliminated)} eliminated), ${text(a_stats.calls)} invokes`) + print("\n -- streamlined --") + dump_function(after_func, false) + return null +} + +var main_name = null +var fi = 0 +var func = null +var bfunc = null +var fname = null + +// Dump main +if (optimized.main != null && before.main != null) { + main_name = optimized.name != null ? optimized.name : "
" + dump_pair(before.main, optimized.main, main_name) +} + +// Dump sub-functions +if (optimized.functions != null && before.functions != null) { + fi = 0 + while (fi < length(optimized.functions)) { + func = optimized.functions[fi] + bfunc = before.functions[fi] + fname = func.name != null ? func.name : `` + dump_pair(bfunc, func, `[${text(fi)}] ${fname}`) + fi = fi + 1 + } +} diff --git a/dump_types.cm b/dump_types.cm new file mode 100644 index 00000000..7b8d2ea5 --- /dev/null +++ b/dump_types.cm @@ -0,0 +1,242 @@ +// dump_types.cm — show streamlined IR with type annotations +// +// Usage: ./cell --core . dump_types.cm + +var fd = use("fd") +var json = use("json") +var tokenize = use("tokenize") +var parse = use("parse") +var fold = use("fold") +var mcode = use("mcode") +var streamline = use("streamline") + +if (length(args) < 1) { + print("usage: cell --core . dump_types.cm ") + return +} + +var filename = args[0] +var src = text(fd.slurp(filename)) +var tok = tokenize(src, filename) +var ast = parse(tok.tokens, src, filename, tokenize) +var folded = fold(ast) +var compiled = mcode(folded) +var optimized = streamline(compiled) + +// Type constants +def T_UNKNOWN = "unknown" +def T_INT = "int" +def T_FLOAT = "float" +def T_NUM = "num" +def T_TEXT = "text" +def T_BOOL = "bool" +def T_NULL = "null" +def T_ARRAY = "array" +def T_RECORD = "record" +def T_FUNCTION = "function" + +def int_result_ops = { + add_int: true, sub_int: true, mul_int: true, + div_int: true, mod_int: true, neg_int: true, + bitnot: true, bitand: true, bitor: true, + bitxor: true, shl: true, shr: true, ushr: true +} +def float_result_ops = { + add_float: true, sub_float: true, mul_float: true, + div_float: true, mod_float: true, neg_float: true +} +def 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, + not: true, and: true, or: true, + is_int: true, is_text: true, is_num: true, + is_bool: true, is_null: true, is_identical: true, + is_array: true, is_func: true, is_record: true, is_stone: true +} + +var access_value_type = function(val) { + if (is_number(val)) { + return is_integer(val) ? T_INT : T_FLOAT + } + if (is_text(val)) { + return T_TEXT + } + return T_UNKNOWN +} + +var track_types = function(slot_types, instr) { + var op = instr[0] + var src_type = null + if (op == "access") { + slot_types[text(instr[1])] = access_value_type(instr[2]) + } else if (op == "int") { + slot_types[text(instr[1])] = T_INT + } else if (op == "true" || op == "false") { + slot_types[text(instr[1])] = T_BOOL + } else if (op == "null") { + slot_types[text(instr[1])] = T_NULL + } else if (op == "move") { + src_type = slot_types[text(instr[2])] + slot_types[text(instr[1])] = src_type != null ? src_type : T_UNKNOWN + } else if (int_result_ops[op] == true) { + slot_types[text(instr[1])] = T_INT + } else if (float_result_ops[op] == true) { + slot_types[text(instr[1])] = T_FLOAT + } else if (op == "concat") { + slot_types[text(instr[1])] = T_TEXT + } else if (bool_result_ops[op] == true) { + slot_types[text(instr[1])] = T_BOOL + } else if (op == "typeof") { + slot_types[text(instr[1])] = T_TEXT + } else if (op == "array") { + slot_types[text(instr[1])] = T_ARRAY + } else if (op == "record") { + slot_types[text(instr[1])] = T_RECORD + } else if (op == "function") { + slot_types[text(instr[1])] = T_FUNCTION + } else if (op == "invoke") { + slot_types[text(instr[2])] = T_UNKNOWN + } else if (op == "load_field" || op == "load_index" || op == "load_dynamic") { + slot_types[text(instr[1])] = T_UNKNOWN + } else if (op == "pop" || op == "get") { + slot_types[text(instr[1])] = T_UNKNOWN + } else if (op == "length") { + slot_types[text(instr[1])] = T_INT + } + return null +} + +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 json.encode(v) + } + if (is_logical(v)) { + return v ? "true" : "false" + } + return text(v) +} + +// Build type annotation string for an instruction +var type_annotation = function(slot_types, instr) { + var n = length(instr) + var parts = [] + var j = 1 + var v = null + var t = null + while (j < n - 2) { + v = instr[j] + if (is_number(v)) { + t = slot_types[text(v)] + if (t != null && t != T_UNKNOWN) { + push(parts, `s${text(v)}:${t}`) + } + } + j = j + 1 + } + if (length(parts) == 0) { + return "" + } + return text(parts, " ") +} + +var dump_function_typed = function(func, name) { + 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 slot_types = {} + var i = 0 + var pc = 0 + var instr = null + var op = null + var n = 0 + var annotation = null + var operand_parts = null + var j = 0 + var operands = null + var pc_str = null + var op_str = null + var line = null + print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`) + if (instrs == null || length(instrs) == 0) { + print(" (empty)") + return null + } + while (i < length(instrs)) { + instr = instrs[i] + if (is_text(instr)) { + if (starts_with(instr, "_nop_")) { + i = i + 1 + continue + } + slot_types = {} + print(`${instr}:`) + } else if (is_array(instr)) { + op = instr[0] + n = length(instr) + annotation = type_annotation(slot_types, instr) + operand_parts = [] + j = 1 + while (j < n - 2) { + push(operand_parts, fmt_val(instr[j])) + j = j + 1 + } + operands = text(operand_parts, ", ") + pc_str = pad_right(text(pc), 5) + op_str = pad_right(op, 14) + line = pad_right(` ${pc_str} ${op_str} ${operands}`, 50) + if (length(annotation) > 0) { + print(`${line} ; ${annotation}`) + } else { + print(line) + } + track_types(slot_types, instr) + pc = pc + 1 + } + i = i + 1 + } + return null +} + +var main_name = null +var fi = 0 +var func = null +var fname = null + +// Dump main +if (optimized.main != null) { + main_name = optimized.name != null ? optimized.name : "
" + dump_function_typed(optimized.main, main_name) +} + +// Dump sub-functions +if (optimized.functions != null) { + fi = 0 + while (fi < length(optimized.functions)) { + func = optimized.functions[fi] + fname = func.name != null ? func.name : `` + dump_function_typed(func, `[${text(fi)}] ${fname}`) + fi = fi + 1 + } +} diff --git a/fold.mach b/fold.mach index f637ed3b..b6dd8fb1 100644 Binary files a/fold.mach and b/fold.mach differ diff --git a/internal/bootstrap.mach b/internal/bootstrap.mach index f025bb52..7bd67663 100644 Binary files a/internal/bootstrap.mach and b/internal/bootstrap.mach differ diff --git a/internal/engine.cm b/internal/engine.cm index 011fdbbc..86f6f255 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -818,6 +818,10 @@ function enet_check() actor_mod.setname(_cell.args.program) var prog = _cell.args.program +if (ends_with(prog, '.cm')) { + os.print(`error: ${prog} is a module (.cm), not an actor (.ce). Run it with: cell --core ${prog}\n`) + os.exit(1) +} if (ends_with(prog, '.ce')) prog = text(prog, 0, -3) var package = use_core('package') diff --git a/internal/engine.mach b/internal/engine.mach index 8fc4257b..a4b2dbbb 100644 Binary files a/internal/engine.mach and b/internal/engine.mach differ diff --git a/mcode.cm b/mcode.cm index 5f6df508..8439006d 100644 --- a/mcode.cm +++ b/mcode.cm @@ -1428,6 +1428,52 @@ var mcode = function(ast) { return d } + // Tier 1 intrinsic inlining: emit direct opcodes instead of frame/invoke + if (callee_kind == "name" && callee.intrinsic == true) { + fname = callee.name + nargs = args_list != null ? length(args_list) : 0 + // 1-arg type check intrinsics → direct opcode + if (nargs == 1) { + if (fname == "is_array" || fname == "is_function" || + fname == "is_object" || fname == "is_stone" || + fname == "is_integer" || fname == "is_text" || + fname == "is_number" || fname == "is_logical" || + fname == "is_null" || fname == "length") { + a0 = gen_expr(args_list[0], -1) + d = alloc_slot() + if (fname == "is_array") { + emit_2("is_array", d, a0) + } else if (fname == "is_function") { + emit_2("is_func", d, a0) + } else if (fname == "is_object") { + emit_2("is_record", d, a0) + } else if (fname == "is_stone") { + emit_2("is_stone", d, a0) + } else if (fname == "is_integer") { + emit_2("is_int", d, a0) + } else if (fname == "is_text") { + emit_2("is_text", d, a0) + } else if (fname == "is_number") { + emit_2("is_num", d, a0) + } else if (fname == "is_logical") { + emit_2("is_bool", d, a0) + } else if (fname == "is_null") { + emit_2("is_null", d, a0) + } else if (fname == "length") { + emit_2("length", d, a0) + } + return d + } + } + // 2-arg push: push(arr, val) → direct opcode + if (nargs == 2 && fname == "push") { + a0 = gen_expr(args_list[0], -1) + a1 = gen_expr(args_list[1], -1) + emit_2("push", a0, a1) + return a1 + } + } + // Collect arg slots arg_slots = [] _i = 0 diff --git a/mcode.mach b/mcode.mach index 3584d85c..71ef2a6b 100644 Binary files a/mcode.mach and b/mcode.mach differ diff --git a/parse.mach b/parse.mach index e2202fe5..10ceb5a3 100644 Binary files a/parse.mach and b/parse.mach differ diff --git a/qbe_emit.mach b/qbe_emit.mach index fb19e2c4..6b6ee4db 100644 Binary files a/qbe_emit.mach and b/qbe_emit.mach differ diff --git a/source/mach.c b/source/mach.c index 7edc15ff..679ef71f 100644 --- a/source/mach.c +++ b/source/mach.c @@ -1658,6 +1658,24 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, case MACH_IS_NULL: frame->slots[a] = JS_NewBool(ctx, JS_IsNull(frame->slots[b])); break; + case MACH_IS_ARRAY: + frame->slots[a] = JS_NewBool(ctx, JS_IsArray(frame->slots[b])); + break; + case MACH_IS_FUNC: + frame->slots[a] = JS_NewBool(ctx, JS_IsFunction(frame->slots[b])); + break; + case MACH_IS_RECORD: + frame->slots[a] = JS_NewBool(ctx, JS_IsRecord(frame->slots[b])); + break; + case MACH_IS_STONE: + frame->slots[a] = JS_NewBool(ctx, JS_IsStone(frame->slots[b])); + break; + case MACH_LENGTH: { + JSValue res = JS_CellLength(ctx, frame->slots[b]); + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); + frame->slots[a] = res; + break; + } case MACH_TYPEOF: { JSValue val = frame->slots[b]; const char *tname = "unknown"; @@ -2552,8 +2570,13 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) { else if (strcmp(op, "is_num") == 0) { AB2(MACH_IS_NUM); } else if (strcmp(op, "is_text") == 0) { AB2(MACH_IS_TEXT); } else if (strcmp(op, "is_bool") == 0) { AB2(MACH_IS_BOOL); } - else if (strcmp(op, "is_null") == 0) { AB2(MACH_IS_NULL); } - else if (strcmp(op, "typeof") == 0) { AB2(MACH_TYPEOF); } + else if (strcmp(op, "is_null") == 0) { AB2(MACH_IS_NULL); } + else if (strcmp(op, "is_array") == 0) { AB2(MACH_IS_ARRAY); } + else if (strcmp(op, "is_func") == 0) { AB2(MACH_IS_FUNC); } + else if (strcmp(op, "is_record") == 0) { AB2(MACH_IS_RECORD); } + else if (strcmp(op, "is_stone") == 0) { AB2(MACH_IS_STONE); } + else if (strcmp(op, "length") == 0) { AB2(MACH_LENGTH); } + else if (strcmp(op, "typeof") == 0) { AB2(MACH_TYPEOF); } /* Logical */ else if (strcmp(op, "not") == 0) { AB2(MACH_NOT); } else if (strcmp(op, "and") == 0) { ABC3(MACH_AND); } diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index ed9d9828..f43a2273 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -596,6 +596,13 @@ typedef enum MachOpcode { /* Misc */ MACH_IN, /* R(A) = (R(B) in R(C)) — has property (ABC) */ + /* Extended type checks (AB) */ + MACH_IS_ARRAY, /* R(A) = is_array(R(B)) */ + MACH_IS_FUNC, /* R(A) = is_function(R(B)) */ + MACH_IS_RECORD, /* R(A) = is_object(R(B)) */ + MACH_IS_STONE, /* R(A) = is_stone(R(B)) */ + MACH_LENGTH, /* R(A) = length(R(B)) — array/text/blob length */ + MACH_OP_COUNT } MachOpcode; @@ -724,6 +731,12 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = { [MACH_DISRUPT] = "disrupt", [MACH_SET_VAR] = "set_var", [MACH_IN] = "in", + /* Extended type checks */ + [MACH_IS_ARRAY] = "is_array", + [MACH_IS_FUNC] = "is_func", + [MACH_IS_RECORD] = "is_record", + [MACH_IS_STONE] = "is_stone", + [MACH_LENGTH] = "length", }; /* Compiled register-based code (off-heap, never GC'd). diff --git a/source/quickjs.h b/source/quickjs.h index 054ab737..e6c0f119 100644 --- a/source/quickjs.h +++ b/source/quickjs.h @@ -486,7 +486,7 @@ JS_BOOL JS_IsRecord(JSValue v); JS_BOOL JS_IsFunction(JSValue v); JS_BOOL JS_IsBlob(JSValue v); JS_BOOL JS_IsText(JSValue v); -static JS_BOOL JS_IsStone(JSValue v); +JS_BOOL JS_IsStone(JSValue v); // Fundamental int JS_GetLength (JSContext *ctx, JSValue obj, int64_t *pres); diff --git a/streamline.cm b/streamline.cm index 2606395f..4ccb876f 100644 --- a/streamline.cm +++ b/streamline.cm @@ -1,5 +1,5 @@ // streamline.cm — mcode IR optimizer -// Single forward pass: type inference + strength reduction +// Composed of independent passes, each a separate function. var streamline = function(ir) { // Type constants @@ -10,20 +10,19 @@ var streamline = function(ir) { var T_TEXT = "text" var T_BOOL = "bool" var T_NULL = "null" + var T_ARRAY = "array" + var T_RECORD = "record" + var T_FUNCTION = "function" + var T_BLOB = "blob" - // Integer arithmetic ops that produce integer results var int_result_ops = { add_int: true, sub_int: true, mul_int: true, div_int: true, mod_int: true } - - // Float arithmetic ops that produce float results var float_result_ops = { add_float: true, sub_float: true, mul_float: true, div_float: true, mod_float: true } - - // Comparison ops that produce bool results var bool_result_ops = { eq_int: true, ne_int: true, lt_int: true, gt_int: true, le_int: true, ge_int: true, @@ -35,19 +34,18 @@ var streamline = function(ir) { eq_tol: true, ne_tol: 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 + is_bool: true, is_null: true, is_identical: true, + is_array: true, is_func: true, is_record: true, is_stone: true } - - // Type check opcodes and what type they verify var type_check_map = { - is_int: T_INT, - is_text: T_TEXT, - is_num: T_NUM, - is_bool: T_BOOL, - is_null: T_NULL + is_int: T_INT, is_text: T_TEXT, is_num: T_NUM, + is_bool: T_BOOL, is_null: T_NULL, + is_array: T_ARRAY, is_func: T_FUNCTION, + is_record: T_RECORD, is_stone: T_RECORD } - // Determine the type of an access literal value + // --- Shared helpers --- + var access_value_type = function(val) { if (is_number(val)) { if (is_integer(val)) { @@ -61,11 +59,9 @@ var streamline = function(ir) { return T_UNKNOWN } - // Update slot_types for an instruction (shared tracking logic) var track_types = function(slot_types, instr) { var op = instr[0] var src_type = null - if (op == "access") { slot_types[text(instr[1])] = access_value_type(instr[2]) } else if (op == "int") { @@ -76,11 +72,7 @@ var streamline = function(ir) { slot_types[text(instr[1])] = T_NULL } else if (op == "move") { src_type = slot_types[text(instr[2])] - if (src_type != null) { - slot_types[text(instr[1])] = src_type - } else { - slot_types[text(instr[1])] = T_UNKNOWN - } + slot_types[text(instr[1])] = src_type != null ? src_type : T_UNKNOWN } else if (int_result_ops[op] == true) { slot_types[text(instr[1])] = T_INT } else if (float_result_ops[op] == true) { @@ -93,8 +85,16 @@ var streamline = function(ir) { slot_types[text(instr[1])] = T_UNKNOWN } else if (op == "invoke") { slot_types[text(instr[2])] = T_UNKNOWN - } else if (op == "pop" || op == "get" || op == "function") { + } else if (op == "pop" || op == "get") { slot_types[text(instr[1])] = T_UNKNOWN + } else if (op == "array") { + slot_types[text(instr[1])] = T_ARRAY + } else if (op == "record") { + slot_types[text(instr[1])] = T_RECORD + } else if (op == "function") { + slot_types[text(instr[1])] = T_FUNCTION + } else if (op == "length") { + slot_types[text(instr[1])] = T_INT } else if (op == "typeof") { slot_types[text(instr[1])] = T_TEXT } else if (op == "neg_int") { @@ -108,7 +108,6 @@ var streamline = function(ir) { return null } - // Check if a slot has a known type (with T_NUM subsumption) var slot_is = function(slot_types, slot, typ) { var known = slot_types[text(slot)] if (known == null) { @@ -123,13 +122,137 @@ var streamline = function(ir) { return false } - // Optimize a single function's instructions - var optimize_function = function(func) { + var merge_backward = function(backward_types, slot, typ) { + var sk = null + var existing = null + if (!is_number(slot)) { + return null + } + sk = text(slot) + existing = backward_types[sk] + if (existing == null) { + backward_types[sk] = typ + } else if (existing != typ && existing != T_UNKNOWN) { + if ((existing == T_INT || existing == T_FLOAT) && typ == T_NUM) { + // Keep more specific + } else if (existing == T_NUM && (typ == T_INT || typ == T_FLOAT)) { + backward_types[sk] = typ + } else if ((existing == T_INT && typ == T_FLOAT) || (existing == T_FLOAT && typ == T_INT)) { + backward_types[sk] = T_NUM + } else { + backward_types[sk] = T_UNKNOWN + } + } + return null + } + + var seed_params = function(slot_types, param_types, nr_args) { + var j = 1 + while (j <= nr_args) { + if (param_types[text(j)] != null) { + slot_types[text(j)] = param_types[text(j)] + } + j = j + 1 + } + return null + } + + // ========================================================= + // Pass: infer_param_types — backward type inference + // Scans typed operators to infer immutable parameter types. + // ========================================================= + var infer_param_types = function(func) { var instructions = func.instructions + var nr_args = func.nr_args != null ? func.nr_args : 0 + var num_instr = 0 + var backward_types = null + var param_types = null + var i = 0 + var j = 0 + var instr = null + var op = null + var bt = null + + if (instructions == null || nr_args == 0) { + return {} + } + + num_instr = length(instructions) + backward_types = {} + i = 0 + while (i < num_instr) { + instr = instructions[i] + if (is_array(instr)) { + op = instr[0] + if (op == "add_int" || op == "sub_int" || op == "mul_int" || + op == "div_int" || op == "mod_int" || + op == "eq_int" || op == "ne_int" || op == "lt_int" || + op == "gt_int" || op == "le_int" || op == "ge_int" || + op == "bitand" || op == "bitor" || op == "bitxor" || + op == "shl" || op == "shr" || op == "ushr") { + merge_backward(backward_types, instr[2], T_INT) + merge_backward(backward_types, instr[3], T_INT) + } else if (op == "neg_int" || op == "bitnot") { + merge_backward(backward_types, instr[2], T_INT) + } else if (op == "add_float" || op == "sub_float" || op == "mul_float" || + op == "div_float" || op == "mod_float" || + op == "eq_float" || op == "ne_float" || op == "lt_float" || + op == "gt_float" || op == "le_float" || op == "ge_float") { + merge_backward(backward_types, instr[2], T_FLOAT) + merge_backward(backward_types, instr[3], T_FLOAT) + } else if (op == "neg_float") { + merge_backward(backward_types, instr[2], T_FLOAT) + } else if (op == "concat" || + op == "eq_text" || op == "ne_text" || op == "lt_text" || + op == "gt_text" || op == "le_text" || op == "ge_text") { + merge_backward(backward_types, instr[2], T_TEXT) + merge_backward(backward_types, instr[3], T_TEXT) + } else if (op == "eq_bool" || op == "ne_bool") { + merge_backward(backward_types, instr[2], T_BOOL) + merge_backward(backward_types, instr[3], T_BOOL) + } else if (op == "not") { + merge_backward(backward_types, instr[2], T_BOOL) + } else if (op == "and" || op == "or") { + merge_backward(backward_types, instr[2], T_BOOL) + merge_backward(backward_types, instr[3], T_BOOL) + } else if (op == "store_index") { + merge_backward(backward_types, instr[1], T_ARRAY) + merge_backward(backward_types, instr[2], T_INT) + } else if (op == "store_field") { + merge_backward(backward_types, instr[1], T_RECORD) + } else if (op == "push") { + merge_backward(backward_types, instr[1], T_ARRAY) + } + } + i = i + 1 + } + + param_types = {} + j = 1 + while (j <= nr_args) { + bt = backward_types[text(j)] + if (bt != null && bt != T_UNKNOWN) { + param_types[text(j)] = bt + } + j = j + 1 + } + return param_types + } + + // ========================================================= + // Pass: eliminate_type_checks — language-level type narrowing + // Eliminates is_/jump pairs when type is known. + // Reduces load_dynamic/store_dynamic to field/index forms. + // ========================================================= + var eliminate_type_checks = function(func, param_types) { + var instructions = func.instructions + var nr_args = func.nr_args != null ? func.nr_args : 0 + var has_params = false var num_instr = 0 var slot_types = null - var nop_counter = 0 + var nc = 0 var i = 0 + var j = 0 var instr = null var op = null var dest = 0 @@ -140,24 +263,34 @@ var streamline = function(ir) { var target_label = null var src_known = null var jlen = 0 - var j = 0 - var peek = null if (instructions == null || length(instructions) == 0) { return null } num_instr = length(instructions) - slot_types = {} + j = 1 + while (j <= nr_args) { + if (param_types[text(j)] != null) { + has_params = true + } + j = j + 1 + } + + slot_types = {} + if (has_params) { + seed_params(slot_types, param_types, nr_args) + } - // Peephole optimization pass: type tracking + strength reduction i = 0 while (i < num_instr) { instr = instructions[i] - // Labels are join points: clear all type info (conservative) if (is_text(instr)) { slot_types = {} + if (has_params) { + seed_params(slot_types, param_types, nr_args) + } i = i + 1 continue } @@ -169,7 +302,7 @@ var streamline = function(ir) { op = instr[0] - // --- Peephole: type-check + jump where we know the type --- + // Type-check + jump elimination if (type_check_map[op] != null && i + 1 < num_instr) { dest = instr[1] src = instr[2] @@ -179,102 +312,84 @@ var streamline = function(ir) { if (is_array(next)) { next_op = next[0] - // Pattern: is_ t, x -> jump_false t, label if (next_op == "jump_false" && next[1] == dest) { target_label = next[2] - if (slot_is(slot_types, src, checked_type)) { - // Known match: check always true, never jumps — eliminate both - nop_counter = nop_counter + 1 - instructions[i] = "_nop_" + text(nop_counter) - nop_counter = nop_counter + 1 - instructions[i + 1] = "_nop_" + text(nop_counter) + nc = nc + 1 + instructions[i] = "_nop_tc_" + text(nc) + nc = nc + 1 + instructions[i + 1] = "_nop_tc_" + text(nc) slot_types[text(dest)] = T_BOOL i = i + 2 continue } - src_known = slot_types[text(src)] if (src_known != null && src_known != T_UNKNOWN && src_known != checked_type) { - // Check for T_NUM subsumption: INT and FLOAT match T_NUM if (checked_type == T_NUM && (src_known == T_INT || src_known == T_FLOAT)) { - // Actually matches — eliminate both - nop_counter = nop_counter + 1 - instructions[i] = "_nop_" + text(nop_counter) - nop_counter = nop_counter + 1 - instructions[i + 1] = "_nop_" + text(nop_counter) + nc = nc + 1 + instructions[i] = "_nop_tc_" + text(nc) + nc = nc + 1 + instructions[i + 1] = "_nop_tc_" + text(nc) slot_types[text(dest)] = T_BOOL i = i + 2 continue } - // Known mismatch: always jumps — nop the check, rewrite jump - nop_counter = nop_counter + 1 - instructions[i] = "_nop_" + text(nop_counter) + nc = nc + 1 + instructions[i] = "_nop_tc_" + text(nc) jlen = length(next) instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]] slot_types[text(dest)] = T_UNKNOWN i = i + 2 continue } - - // Unknown: can't eliminate, but narrow type on fallthrough slot_types[text(dest)] = T_BOOL slot_types[text(src)] = checked_type i = i + 2 continue } - // Pattern: is_ t, x -> jump_true t, label if (next_op == "jump_true" && next[1] == dest) { target_label = next[2] - if (slot_is(slot_types, src, checked_type)) { - // Known match: always true, always jumps — nop check, rewrite to jump - nop_counter = nop_counter + 1 - instructions[i] = "_nop_" + text(nop_counter) + nc = nc + 1 + instructions[i] = "_nop_tc_" + text(nc) jlen = length(next) instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]] slot_types[text(dest)] = T_BOOL i = i + 2 continue } - src_known = slot_types[text(src)] if (src_known != null && src_known != T_UNKNOWN && src_known != checked_type) { if (checked_type == T_NUM && (src_known == T_INT || src_known == T_FLOAT)) { - // Actually matches T_NUM — always jumps - nop_counter = nop_counter + 1 - instructions[i] = "_nop_" + text(nop_counter) + nc = nc + 1 + instructions[i] = "_nop_tc_" + text(nc) jlen = length(next) instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]] slot_types[text(dest)] = T_BOOL i = i + 2 continue } - // Known mismatch: never jumps — eliminate both - nop_counter = nop_counter + 1 - instructions[i] = "_nop_" + text(nop_counter) - nop_counter = nop_counter + 1 - instructions[i + 1] = "_nop_" + text(nop_counter) + nc = nc + 1 + instructions[i] = "_nop_tc_" + text(nc) + nc = nc + 1 + instructions[i + 1] = "_nop_tc_" + text(nc) slot_types[text(dest)] = T_BOOL i = i + 2 continue } - - // Unknown: can't optimize slot_types[text(dest)] = T_BOOL i = i + 2 continue } } - // Standalone type check (no jump following): just track the result slot_types[text(dest)] = T_BOOL i = i + 1 continue } - // --- Strength reduction: load_dynamic / store_dynamic --- + // Dynamic access reduction if (op == "load_dynamic") { if (slot_is(slot_types, instr[3], T_TEXT)) { instr[0] = "load_field" @@ -295,26 +410,361 @@ var streamline = function(ir) { continue } - // --- Standard type tracking --- track_types(slot_types, instr) + i = i + 1 + } + + return null + } + + // ========================================================= + // Pass: simplify_algebra — algebraic identity & comparison + // Tracks known constant values. Rewrites identity ops to + // moves or constants. Folds same-slot comparisons. + // ========================================================= + var simplify_algebra = function(func) { + var instructions = func.instructions + var num_instr = 0 + var slot_values = null + var nc = 0 + var i = 0 + var instr = null + var op = null + var ilen = 0 + var v2 = null + var v3 = null + var sv = null + + if (instructions == null || length(instructions) == 0) { + return null + } + + num_instr = length(instructions) + slot_values = {} + + i = 0 + while (i < num_instr) { + instr = instructions[i] + + if (is_text(instr)) { + slot_values = {} + i = i + 1 + continue + } + if (!is_array(instr)) { + i = i + 1 + continue + } + + op = instr[0] + ilen = length(instr) + + // Track known constant values + if (op == "int") { + slot_values[text(instr[1])] = instr[2] + } else if (op == "access" && is_number(instr[2])) { + slot_values[text(instr[1])] = instr[2] + } else if (op == "true") { + slot_values[text(instr[1])] = true + } else if (op == "false") { + slot_values[text(instr[1])] = false + } else if (op == "move") { + sv = slot_values[text(instr[2])] + if (sv != null) { + slot_values[text(instr[1])] = sv + } else { + slot_values[text(instr[1])] = null + } + } + + // Integer: x+0, x-0 → move x + if (op == "add_int" || op == "sub_int") { + v3 = slot_values[text(instr[3])] + if (v3 == 0) { + instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + if (op == "add_int") { + v2 = slot_values[text(instr[2])] + if (v2 == 0) { + instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + } + } else if (op == "mul_int") { + v3 = slot_values[text(instr[3])] + v2 = slot_values[text(instr[2])] + if (v3 == 1) { + instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + if (v2 == 1) { + instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + if (v3 == 0 || v2 == 0) { + instructions[i] = ["int", instr[1], 0, instr[ilen - 2], instr[ilen - 1]] + slot_values[text(instr[1])] = 0 + i = i + 1 + continue + } + } else if (op == "div_int") { + v3 = slot_values[text(instr[3])] + if (v3 == 1) { + instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + } + + // Float: x+0, x-0 → move x; x*1, x/1 → move x + // (skip mul_float * 0 — not safe with NaN/Inf) + if (op == "add_float" || op == "sub_float") { + v3 = slot_values[text(instr[3])] + if (v3 == 0) { + instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + if (op == "add_float") { + v2 = slot_values[text(instr[2])] + if (v2 == 0) { + instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + } + } else if (op == "mul_float") { + v3 = slot_values[text(instr[3])] + v2 = slot_values[text(instr[2])] + if (v3 == 1) { + instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + if (v2 == 1) { + instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + } else if (op == "div_float") { + v3 = slot_values[text(instr[3])] + if (v3 == 1) { + instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]] + i = i + 1 + continue + } + } + + // Same-slot comparisons + if (is_number(instr[2]) && instr[2] == instr[3]) { + if (op == "eq_int" || op == "eq_float" || op == "eq_text" || + op == "eq_bool" || op == "is_identical" || + op == "le_int" || op == "le_float" || op == "le_text" || + op == "ge_int" || op == "ge_float" || op == "ge_text") { + instructions[i] = ["true", instr[1], instr[ilen - 2], instr[ilen - 1]] + slot_values[text(instr[1])] = true + i = i + 1 + continue + } + if (op == "ne_int" || op == "ne_float" || op == "ne_text" || + op == "ne_bool" || + op == "lt_int" || op == "lt_float" || op == "lt_text" || + op == "gt_int" || op == "gt_float" || op == "gt_text") { + instructions[i] = ["false", instr[1], instr[ilen - 2], instr[ilen - 1]] + slot_values[text(instr[1])] = false + i = i + 1 + continue + } + } + + // Clear value tracking for dest-producing ops (not reads-only) + if (op == "invoke") { + slot_values[text(instr[2])] = null + } else if (op != "int" && op != "access" && op != "true" && + op != "false" && op != "move" && op != "null" && + op != "jump" && op != "jump_true" && op != "jump_false" && + op != "jump_not_null" && op != "return" && op != "disrupt" && + op != "store_field" && op != "store_index" && + op != "store_dynamic" && op != "push" && op != "setarg") { + if (is_number(instr[1])) { + slot_values[text(instr[1])] = null + } + } i = i + 1 } - // Second pass: remove dead jumps (jump to the immediately next label) + return null + } + + // ========================================================= + // Pass: simplify_booleans — not+jump fusion, double-not + // ========================================================= + var simplify_booleans = function(func) { + var instructions = func.instructions + var num_instr = 0 + var nc = 0 + var i = 0 + var instr = null + var next = null + var next_op = null + var nlen = 0 + + if (instructions == null || length(instructions) == 0) { + return null + } + + num_instr = length(instructions) + i = 0 + while (i < num_instr) { + instr = instructions[i] + if (!is_array(instr) || instr[0] != "not" || i + 1 >= num_instr) { + i = i + 1 + continue + } + + next = instructions[i + 1] + if (!is_array(next)) { + i = i + 1 + continue + } + + next_op = next[0] + nlen = length(next) + + // not d, x; jump_false d, label → jump_true x, label + if (next_op == "jump_false" && next[1] == instr[1]) { + nc = nc + 1 + instructions[i] = "_nop_bl_" + text(nc) + instructions[i + 1] = ["jump_true", instr[2], next[2], next[nlen - 2], next[nlen - 1]] + i = i + 2 + continue + } + + // not d, x; jump_true d, label → jump_false x, label + if (next_op == "jump_true" && next[1] == instr[1]) { + nc = nc + 1 + instructions[i] = "_nop_bl_" + text(nc) + instructions[i + 1] = ["jump_false", instr[2], next[2], next[nlen - 2], next[nlen - 1]] + i = i + 2 + continue + } + + // not d1, x; not d2, d1 → move d2, x + if (next_op == "not" && next[2] == instr[1]) { + nc = nc + 1 + instructions[i] = "_nop_bl_" + text(nc) + instructions[i + 1] = ["move", next[1], instr[2], next[nlen - 2], next[nlen - 1]] + i = i + 2 + continue + } + + i = i + 1 + } + + return null + } + + // ========================================================= + // Pass: eliminate_moves — move a, a → nop + // ========================================================= + var eliminate_moves = function(func) { + var instructions = func.instructions + var num_instr = 0 + var nc = 0 + var i = 0 + var instr = null + + if (instructions == null || length(instructions) == 0) { + return null + } + + 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) + } + i = i + 1 + } + + return null + } + + // ========================================================= + // Pass: eliminate_unreachable — nop code after return/disrupt + // ========================================================= + var eliminate_unreachable = function(func) { + var instructions = func.instructions + var num_instr = 0 + var nc = 0 + var after_return = false + var i = 0 + var instr = null + + if (instructions == null || length(instructions) == 0) { + return null + } + + num_instr = length(instructions) + i = 0 + while (i < num_instr) { + instr = instructions[i] + if (is_text(instr)) { + if (!starts_with(instr, "_nop_")) { + after_return = false + } + } else if (is_array(instr)) { + if (after_return) { + nc = nc + 1 + instructions[i] = "_nop_ur_" + text(nc) + } else if (instr[0] == "return" || instr[0] == "disrupt") { + after_return = true + } + } + i = i + 1 + } + + return null + } + + // ========================================================= + // Pass: eliminate_dead_jumps — jump to next label → nop + // ========================================================= + var eliminate_dead_jumps = function(func) { + var instructions = func.instructions + var num_instr = 0 + var nc = 0 + var i = 0 + var j = 0 + var instr = null + var target_label = null + var peek = null + + if (instructions == null || length(instructions) == 0) { + return null + } + + num_instr = length(instructions) i = 0 while (i < num_instr) { instr = instructions[i] if (is_array(instr) && instr[0] == "jump") { target_label = instr[1] - // Check if the very next non-nop item is that label j = i + 1 while (j < num_instr) { peek = instructions[j] if (is_text(peek)) { if (peek == target_label) { - nop_counter = nop_counter + 1 - instructions[i] = "_nop_" + text(nop_counter) + nc = nc + 1 + instructions[i] = "_nop_dj_" + text(nc) } break } @@ -330,6 +780,27 @@ var streamline = function(ir) { return null } + // ========================================================= + // Compose all passes + // ========================================================= + var optimize_function = function(func) { + var param_types = null + if (func.instructions == null || length(func.instructions) == 0) { + return null + } + param_types = infer_param_types(func) + eliminate_type_checks(func, param_types) + simplify_algebra(func) + simplify_booleans(func) + eliminate_moves(func) + // NOTE: eliminate_unreachable is disabled because disruption handler + // code is placed after return/disrupt without label boundaries. + // Re-enable once mcode.cm emits labels for handler entry points. + //eliminate_unreachable(func) + eliminate_dead_jumps(func) + return null + } + // Process main function if (ir.main != null) { optimize_function(ir.main) diff --git a/streamline.mach b/streamline.mach index 88a7e3ee..26875a75 100644 Binary files a/streamline.mach and b/streamline.mach differ diff --git a/test_backward.cm b/test_backward.cm new file mode 100644 index 00000000..9465c976 --- /dev/null +++ b/test_backward.cm @@ -0,0 +1,27 @@ +// Test backward type propagation +// Functions that use typed ops on parameters should have +// parameter types inferred and type checks eliminated + +var sum_ints = function(a, b, c) { + return a + b + c +} + +var count_down = function(n) { + var i = n + var total = 0 + while (i > 0) { + total = total + i + i = i - 1 + } + return total +} + +var concat_all = function(a, b, c) { + return a + b + c +} + +if (sum_ints(1, 2, 3) != 6) { print("FAIL sum_ints") } +if (count_down(5) != 15) { print("FAIL count_down") } +if (concat_all("a", "b", "c") != "abc") { print("FAIL concat_all") } + +print("backward type tests passed") diff --git a/test_intrinsics.cm b/test_intrinsics.cm new file mode 100644 index 00000000..a649aeae --- /dev/null +++ b/test_intrinsics.cm @@ -0,0 +1,63 @@ +// Test all inlined intrinsics +var arr = [1, 2, 3] +var rec = {a: 1} +var fn = function() { return 1 } +var txt = "hello" +var num = 42 +var boo = true + +// is_array +if (!is_array(arr)) { print("FAIL is_array(arr)") } +if (is_array(rec)) { print("FAIL is_array(rec)") } +if (is_array(42)) { print("FAIL is_array(42)") } + +// is_object +if (!is_object(rec)) { print("FAIL is_object(rec)") } +if (is_object(arr)) { print("FAIL is_object(arr)") } +if (is_object(42)) { print("FAIL is_object(42)") } + +// is_function +if (!is_function(fn)) { print("FAIL is_function(fn)") } +if (is_function(rec)) { print("FAIL is_function(rec)") } + +// is_stone +var frozen = stone([1, 2]) +if (!is_stone(frozen)) { print("FAIL is_stone(frozen)") } +if (is_stone(arr)) { print("FAIL is_stone(arr)") } +if (!is_stone(42)) { print("FAIL is_stone(42)") } +if (!is_stone("hi")) { print("FAIL is_stone(str)") } + +// length +if (length(arr) != 3) { print("FAIL length(arr)") } +if (length(txt) != 5) { print("FAIL length(txt)") } +if (length([]) != 0) { print("FAIL length([])") } + +// is_integer (already existed but now inlined) +if (!is_integer(42)) { print("FAIL is_integer(42)") } +if (is_integer(3.14)) { print("FAIL is_integer(3.14)") } + +// is_text +if (!is_text("hi")) { print("FAIL is_text(hi)") } +if (is_text(42)) { print("FAIL is_text(42)") } + +// is_number +if (!is_number(42)) { print("FAIL is_number(42)") } +if (!is_number(3.14)) { print("FAIL is_number(3.14)") } +if (is_number("hi")) { print("FAIL is_number(hi)") } + +// is_logical +if (!is_logical(true)) { print("FAIL is_logical(true)") } +if (is_logical(42)) { print("FAIL is_logical(42)") } + +// is_null +if (!is_null(null)) { print("FAIL is_null(null)") } +if (is_null(42)) { print("FAIL is_null(42)") } + +// push (inlined) +var a = [1] +push(a, 2) +push(a, 3) +if (length(a) != 3) { print("FAIL push length") } +if (a[2] != 3) { print("FAIL push value") } + +print("all intrinsic tests passed") diff --git a/tokenize.mach b/tokenize.mach index 6b7682f1..cd3a5f33 100644 Binary files a/tokenize.mach and b/tokenize.mach differ