Compare commits
4 Commits
mach_suite
...
mach
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ba2783b48 | ||
|
|
6de542f0d0 | ||
|
|
6ba4727119 | ||
|
|
900db912a5 |
@@ -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_<type>` + 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`)
|
||||
|
||||
@@ -113,6 +119,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 |
|
||||
@@ -122,3 +136,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 |
|
||||
|
||||
202
docs/spec/streamline.md
Normal file
202
docs/spec/streamline.md
Normal file
@@ -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 <file.ce|file.cm>
|
||||
./cell --core . dump_stream.cm <file.ce|file.cm>
|
||||
./cell --core . dump_types.cm <file.ce|file.cm>
|
||||
```
|
||||
|
||||
## Nop Convention
|
||||
|
||||
Eliminated instructions are replaced with strings matching `_nop_<prefix>_<counter>`. 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
|
||||
12
dump_ast.cm
Normal file
12
dump_ast.cm
Normal file
@@ -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))
|
||||
119
dump_mcode.cm
119
dump_mcode.cm
@@ -1,20 +1,117 @@
|
||||
// dump_mcode.cm — pretty-print mcode IR (before streamlining)
|
||||
//
|
||||
// Usage: ./cell --core . dump_mcode.cm <file.ce|file.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 <file>")
|
||||
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 : "<main>"
|
||||
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 : `<func_${text(fi)}>`
|
||||
dump_function(func, `[${text(fi)}] ${fname}`)
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
|
||||
166
dump_stream.cm
Normal file
166
dump_stream.cm
Normal file
@@ -0,0 +1,166 @@
|
||||
// dump_stream.cm — show mcode IR before and after streamlining
|
||||
//
|
||||
// Usage: ./cell --core . dump_stream.cm <file.ce|file.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 <file>")
|
||||
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 : "<main>"
|
||||
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 : `<func_${text(fi)}>`
|
||||
dump_pair(bfunc, func, `[${text(fi)}] ${fname}`)
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
242
dump_types.cm
Normal file
242
dump_types.cm
Normal file
@@ -0,0 +1,242 @@
|
||||
// dump_types.cm — show streamlined IR with type annotations
|
||||
//
|
||||
// Usage: ./cell --core . dump_types.cm <file.ce|file.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 <file>")
|
||||
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 : "<main>"
|
||||
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 : `<func_${text(fi)}>`
|
||||
dump_function_typed(func, `[${text(fi)}] ${fname}`)
|
||||
fi = fi + 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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 <path> ${prog}\n`)
|
||||
os.exit(1)
|
||||
}
|
||||
if (ends_with(prog, '.ce')) prog = text(prog, 0, -3)
|
||||
|
||||
var package = use_core('package')
|
||||
|
||||
Binary file not shown.
46
mcode.cm
46
mcode.cm
@@ -1509,6 +1509,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
|
||||
|
||||
BIN
mcode.mach
BIN
mcode.mach
Binary file not shown.
BIN
parse.mach
BIN
parse.mach
Binary file not shown.
BIN
qbe_emit.mach
BIN
qbe_emit.mach
Binary file not shown.
202
source/mach.c
202
source/mach.c
@@ -1142,174 +1142,6 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
|
||||
break;
|
||||
}
|
||||
|
||||
case MACH_CALL: {
|
||||
/* Lua-style call: R(A)=func, B=nargs in R(A+1)..R(A+B), C=nresults */
|
||||
int base = a;
|
||||
int nargs = b;
|
||||
int nresults = c;
|
||||
JSValue func_val = frame->slots[base];
|
||||
|
||||
if (!JS_IsFunction(func_val)) {
|
||||
JS_ThrowTypeError(ctx, "not a function");
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
goto disrupt;
|
||||
}
|
||||
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(func_val);
|
||||
if (fn->kind == JS_FUNC_KIND_C) {
|
||||
/* C function: copy args to C stack */
|
||||
JSValue args[nargs > 0 ? nargs : 1];
|
||||
for (int i = 0; i < nargs; i++)
|
||||
args[i] = frame->slots[base + 1 + i];
|
||||
ctx->reg_current_frame = frame_ref.val;
|
||||
ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
|
||||
JSValue ret = js_call_c_function(ctx, func_val, JS_NULL, nargs, args);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
ctx->reg_current_frame = JS_NULL;
|
||||
if (JS_IsException(ret)) { goto disrupt; }
|
||||
if (nresults > 0) frame->slots[base] = ret;
|
||||
} else if (fn->kind == JS_FUNC_KIND_REGISTER) {
|
||||
/* Register function: allocate frame, copy args, switch */
|
||||
JSCodeRegister *fn_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;
|
||||
}
|
||||
/* Re-read pointers — GC may have moved them */
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
func_val = frame->slots[base];
|
||||
fn = JS_VALUE_GET_FUNCTION(func_val);
|
||||
new_frame->function = func_val;
|
||||
new_frame->slots[0] = JS_NULL; /* this */
|
||||
for (int i = 0; i < nargs && i < fn_code->arity; i++)
|
||||
new_frame->slots[1 + i] = frame->slots[base + 1 + i];
|
||||
|
||||
/* Save return info: pc in upper 16 bits, base reg or 0xFFFF (discard) in lower */
|
||||
int ret_slot = (nresults > 0) ? base : 0xFFFF;
|
||||
frame->address = JS_NewInt32(ctx, (pc << 16) | ret_slot);
|
||||
new_frame->caller = JS_MKPTR(frame);
|
||||
|
||||
frame = new_frame;
|
||||
frame_ref.val = JS_MKPTR(frame);
|
||||
code = fn_code;
|
||||
env = fn->u.reg.env_record;
|
||||
pc = code->entry_point;
|
||||
} else {
|
||||
/* Other function kinds (bytecode) — copy args to C stack */
|
||||
JSValue args[nargs > 0 ? nargs : 1];
|
||||
for (int i = 0; i < nargs; i++)
|
||||
args[i] = frame->slots[base + 1 + i];
|
||||
JSValue ret = JS_CallInternal(ctx, func_val, JS_NULL, nargs, args, 0);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
if (JS_IsException(ret)) { goto disrupt; }
|
||||
if (nresults > 0) frame->slots[base] = ret;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case MACH_CALLMETHOD: {
|
||||
/* Method call: R(A)=obj, B=nargs in R(A+2)..R(A+1+B), C=cpool key index
|
||||
Result stored in R(A). C=0xFF means key is in R(A+1).
|
||||
If obj is a function (proxy): call obj(key_str, [args...])
|
||||
Else (record): get property, call property(obj_as_this, args...) */
|
||||
int base = a;
|
||||
int nargs = b;
|
||||
JSGCRef key_ref;
|
||||
JS_PushGCRef(ctx, &key_ref);
|
||||
key_ref.val = (c == 0xFF) ? frame->slots[base + 1] : code->cpool[c];
|
||||
|
||||
if (JS_IsFunction(frame->slots[base]) && JS_IsText(key_ref.val) &&
|
||||
JS_VALUE_GET_FUNCTION(frame->slots[base])->length == 2) {
|
||||
/* Proxy call (arity-2 functions only): obj(name, [args...]) */
|
||||
JSValue arr = JS_NewArray(ctx);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
if (JS_IsException(arr)) { JS_PopGCRef(ctx, &key_ref); goto disrupt; }
|
||||
frame->slots[base + 1] = arr; /* protect from GC in temp slot */
|
||||
for (int i = 0; i < nargs; i++) {
|
||||
JS_SetPropertyNumber(ctx, frame->slots[base + 1], i, frame->slots[base + 2 + i]);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
}
|
||||
/* Call proxy with key and array from C stack */
|
||||
JSValue call_args[2] = { key_ref.val, frame->slots[base + 1] };
|
||||
ctx->reg_current_frame = frame_ref.val;
|
||||
ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
|
||||
JSValue ret = JS_CallInternal(ctx, frame->slots[base], JS_NULL, 2, call_args, 0);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
ctx->reg_current_frame = JS_NULL;
|
||||
if (JS_IsException(ret)) { JS_PopGCRef(ctx, &key_ref); goto disrupt; }
|
||||
frame->slots[base] = ret;
|
||||
} else if (JS_IsFunction(frame->slots[base])) {
|
||||
/* Non-proxy function with non-text key: disrupt */
|
||||
JS_ThrowTypeError(ctx, "cannot use bracket notation on non-proxy function");
|
||||
JS_PopGCRef(ctx, &key_ref);
|
||||
goto disrupt;
|
||||
} else {
|
||||
/* Record method call: get property, call with this=obj */
|
||||
if (JS_IsNull(frame->slots[base])) {
|
||||
JS_ThrowTypeError(ctx, "cannot read properties of null");
|
||||
JS_PopGCRef(ctx, &key_ref);
|
||||
goto disrupt;
|
||||
}
|
||||
JSValue method = JS_GetProperty(ctx, frame->slots[base], key_ref.val);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
if (JS_IsException(method)) { JS_PopGCRef(ctx, &key_ref); goto disrupt; }
|
||||
if (!JS_IsFunction(method)) {
|
||||
JS_ThrowTypeError(ctx, "not a function");
|
||||
JS_PopGCRef(ctx, &key_ref);
|
||||
goto disrupt;
|
||||
}
|
||||
JSFunction *fn = JS_VALUE_GET_FUNCTION(method);
|
||||
if (fn->kind == JS_FUNC_KIND_C) {
|
||||
JSValue args[nargs > 0 ? nargs : 1];
|
||||
for (int i = 0; i < nargs; i++)
|
||||
args[i] = frame->slots[base + 2 + i];
|
||||
ctx->reg_current_frame = frame_ref.val;
|
||||
ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
|
||||
JSValue ret = js_call_c_function(ctx, method, frame->slots[base], nargs, args);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
ctx->reg_current_frame = JS_NULL;
|
||||
if (JS_IsException(ret)) { JS_PopGCRef(ctx, &key_ref); goto disrupt; }
|
||||
frame->slots[base] = ret;
|
||||
} else if (fn->kind == JS_FUNC_KIND_REGISTER) {
|
||||
JSCodeRegister *fn_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);
|
||||
JS_PopGCRef(ctx, &key_ref);
|
||||
goto disrupt;
|
||||
}
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
method = JS_GetProperty(ctx, frame->slots[base], key_ref.val);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
fn = JS_VALUE_GET_FUNCTION(method);
|
||||
new_frame->function = method;
|
||||
new_frame->slots[0] = frame->slots[base]; /* this */
|
||||
for (int i = 0; i < nargs && i < fn_code->arity; i++)
|
||||
new_frame->slots[1 + i] = frame->slots[base + 2 + i];
|
||||
int ret_slot = base;
|
||||
frame->address = JS_NewInt32(ctx, (pc << 16) | ret_slot);
|
||||
new_frame->caller = JS_MKPTR(frame);
|
||||
frame = new_frame;
|
||||
frame_ref.val = JS_MKPTR(frame);
|
||||
code = fn_code;
|
||||
env = fn->u.reg.env_record;
|
||||
pc = code->entry_point;
|
||||
} else {
|
||||
/* Bytecode or other function */
|
||||
JSValue args[nargs > 0 ? nargs : 1];
|
||||
for (int i = 0; i < nargs; i++)
|
||||
args[i] = frame->slots[base + 2 + i];
|
||||
JSValue ret = JS_CallInternal(ctx, method, frame->slots[base], nargs, args, 0);
|
||||
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
|
||||
if (JS_IsException(ret)) { JS_PopGCRef(ctx, &key_ref); goto disrupt; }
|
||||
frame->slots[base] = ret;
|
||||
}
|
||||
}
|
||||
JS_PopGCRef(ctx, &key_ref);
|
||||
break;
|
||||
}
|
||||
|
||||
case MACH_RETURN:
|
||||
result = frame->slots[a];
|
||||
if (JS_IsNull(frame->caller)) goto done;
|
||||
@@ -1658,9 +1490,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_IS_PROXY: {
|
||||
JSValue v = frame->slots[b];
|
||||
int is_proxy = 0;
|
||||
@@ -2568,11 +2415,15 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) {
|
||||
else if (strcmp(op, "is_int") == 0) { AB2(MACH_IS_INT); }
|
||||
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, "is_func") == 0) { AB2(MACH_IS_FUNC); }
|
||||
else if (strcmp(op, "is_proxy") == 0) { AB2(MACH_IS_PROXY); }
|
||||
else if (strcmp(op, "typeof") == 0) { AB2(MACH_TYPEOF); }
|
||||
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, "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, "is_proxy") == 0) { AB2(MACH_IS_PROXY); }
|
||||
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); }
|
||||
@@ -3248,11 +3099,6 @@ static void dump_register_code(JSContext *ctx, JSCodeRegister *code, int indent)
|
||||
break;
|
||||
}
|
||||
|
||||
/* Call */
|
||||
case MACH_CALL:
|
||||
printf("r%d, %d, %d", a, b, c);
|
||||
break;
|
||||
|
||||
/* Return / throw */
|
||||
case MACH_RETURN:
|
||||
case MACH_THROW:
|
||||
|
||||
@@ -469,7 +469,7 @@ typedef enum MachOpcode {
|
||||
MACH_JMPNULL, /* if R(A)==null: pc += sBx */
|
||||
|
||||
/* Function calls — Lua-style consecutive registers (legacy .mach) */
|
||||
MACH_CALL, /* Call R(A) with B args R(A+1)..R(A+B), C=0 discard, C=1 keep result in R(A) */
|
||||
MACH_CALL, /* (removed — placeholder to preserve opcode numbering) */
|
||||
MACH_RETURN, /* Return R(A) */
|
||||
MACH_RETNIL, /* Return null */
|
||||
|
||||
@@ -488,7 +488,7 @@ typedef enum MachOpcode {
|
||||
MACH_HASPROP, /* R(A) = R(C) in R(B) — has property check */
|
||||
MACH_REGEXP, /* R(A) = regexp(K(B), K(C)) — regex literal */
|
||||
|
||||
MACH_CALLMETHOD, /* Method call: R(A)=obj, B=nargs in R(A+2)..R(A+1+B), C=cpool key */
|
||||
MACH_CALLMETHOD, /* (removed — placeholder to preserve opcode numbering) */
|
||||
|
||||
MACH_EQ_TOL, /* R(A) = eq_tol(R(B), R(B+1), R(B+2)), C=3 */
|
||||
MACH_NEQ_TOL, /* R(A) = ne_tol(R(B), R(B+1), R(B+2)), C=3 */
|
||||
@@ -595,7 +595,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_IS_PROXY, /* R(A) = is_function(R(B)) && R(B).length == 2 */
|
||||
|
||||
MACH_OP_COUNT
|
||||
@@ -726,7 +732,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",
|
||||
[MACH_IS_PROXY] = "is_proxy",
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
621
streamline.cm
621
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
|
||||
}
|
||||
|
||||
// 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_<type>/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_<type> 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_<type> 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)
|
||||
|
||||
BIN
streamline.mach
BIN
streamline.mach
Binary file not shown.
27
test_backward.cm
Normal file
27
test_backward.cm
Normal file
@@ -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")
|
||||
63
test_intrinsics.cm
Normal file
63
test_intrinsics.cm
Normal file
@@ -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")
|
||||
BIN
tokenize.mach
BIN
tokenize.mach
Binary file not shown.
Reference in New Issue
Block a user