diff --git a/docs/spec/mach.md b/docs/spec/mach.md index 5db79dac..470db187 100644 --- a/docs/spec/mach.md +++ b/docs/spec/mach.md @@ -93,3 +93,13 @@ Arithmetic ops (ADD, SUB, MUL, DIV, MOD, POW) are executed inline without callin DIV and MOD check for zero divisor (→ null). POW uses `pow()` with non-finite handling for finite inputs. Comparison ops (EQ through GE) and bitwise ops still use `reg_vm_binop()` for their slow paths, as they handle a wider range of type combinations (string comparisons, null equality, etc.). + +## String Concatenation + +CONCAT has a three-tier dispatch for self-assign patterns (`concat R(A), R(A), R(C)` where dest equals the left operand): + +1. **In-place append**: If `R(A)` is a mutable heap text (S bit clear) with `length + rhs_length <= cap56`, characters are appended directly. Zero allocation, zero GC. +2. **Growth allocation** (`JS_ConcatStringGrow`): Allocates a new text with 2x capacity and does **not** stone the result, leaving it mutable for subsequent appends. +3. **Exact-fit stoned** (`JS_ConcatString`): Used when dest differs from the left operand (normal non-self-assign concat). + +The `stone_text` instruction (iABC, B=0, C=0) sets the S bit on a mutable heap text in `R(A)`. For non-pointer values or already-stoned text, it is a no-op. This instruction is emitted by the streamline optimizer at escape points; see [Streamline — insert_stone_text](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) and [Stone Memory — Mutable Text](stone.md#mutable-text-concatenation). diff --git a/docs/spec/mcode.md b/docs/spec/mcode.md index 022de72a..60a0c94e 100644 --- a/docs/spec/mcode.md +++ b/docs/spec/mcode.md @@ -101,6 +101,11 @@ Operands are register slot numbers (integers), constant values (strings, numbers | Instruction | Operands | Description | |-------------|----------|-------------| | `concat` | `dest, a, b` | `dest = a ~ b` (text concatenation) | +| `stone_text` | `slot` | Stone a mutable text value (see below) | + +The `stone_text` instruction is emitted by the streamline optimizer's escape analysis pass (`insert_stone_text`). It freezes a mutable text value before it escapes its defining slot — for example, before a `move`, `setarg`, `store_field`, `push`, or `put`. The instruction is only inserted when the slot is provably `T_TEXT`; non-text values never need stoning. See [Streamline Optimizer — insert_stone_text](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) for details. + +At the VM level, `stone_text` is a single-operand instruction (iABC with B=0, C=0). If the slot holds a heap text without the S bit set, it sets the S bit. For all other values (integers, booleans, already-stoned text, etc.), it is a no-op. ### Comparison — Integer diff --git a/docs/spec/stone.md b/docs/spec/stone.md index f46066a8..5f94ca3f 100644 --- a/docs/spec/stone.md +++ b/docs/spec/stone.md @@ -77,6 +77,30 @@ Messages between actors are stoned before delivery, ensuring actors never share Literal objects and arrays that can be determined at compile time may be allocated directly in stone memory. +## Mutable Text Concatenation + +String concatenation in a loop (`s = s + "x"`) is optimized to O(n) amortized by leaving concat results **unstoned** with over-allocated capacity. On the next concatenation, if the destination text is mutable (S bit clear) and has enough room, the VM appends in-place with zero allocation. + +### How It Works + +When the VM executes `concat dest, dest, src` (same destination and left operand — a self-assign pattern): + +1. **Inline fast path**: If `dest` holds a heap text, is not stoned, and `length + src_length <= capacity` — append characters in place, update length, done. No allocation, no GC possible. + +2. **Growth path** (`JS_ConcatStringGrow`): Allocate a new text with `capacity = max(new_length * 2, 16)`, copy both operands, and return the result **without stoning** it. The 2x growth factor means a loop of N concatenations does O(log N) allocations totaling O(N) character copies. + +3. **Exact-fit path** (`JS_ConcatString`): When `dest != left` (not self-assign), the existing exact-fit stoned path is used. This is the normal case for expressions like `var c = a + b`. + +### Safety Invariant + +**An unstoned heap text is uniquely referenced by exactly one slot.** This is enforced by the `stone_text` mcode instruction, which the [streamline optimizer](streamline.md#7-insert_stone_text-mutable-text-escape-analysis) inserts before any instruction that would create a second reference to the value (move, store, push, setarg, put). Two VM-level guards cover cases where the compiler cannot prove the type: `get` (closure reads) and `return` (inter-frame returns). + +### Why Over-Allocation Is GC-Safe + +- The copying collector copies based on `cap56` (the object header's capacity field), not `length`. Over-allocated capacity survives GC. +- `js_alloc_string` zero-fills the packed data region, so padding beyond `length` is always clean. +- String comparisons, hashing, and interning all use `length`, not `cap56`. Extra capacity is invisible to string operations. + ## Relationship to GC The Cheney copying collector only operates on the mutable heap. During collection, when the collector encounters a pointer to stone memory (S bit set), it skips it — stone objects are roots that never move. This means stone memory acts as a permanent root set with zero GC overhead. diff --git a/docs/spec/streamline.md b/docs/spec/streamline.md index 0167dc49..7dfbb818 100644 --- a/docs/spec/streamline.md +++ b/docs/spec/streamline.md @@ -164,7 +164,44 @@ Removes `move a, a` instructions where the source and destination are the same s **Nop prefix:** `_nop_mv_` -### 7. eliminate_unreachable (dead code after return) +### 7. insert_stone_text (mutable text escape analysis) + +Inserts `stone_text` instructions before mutable text values escape their defining slot. This pass supports the mutable text concatenation optimization (see [Stone Memory — Mutable Text](stone.md#mutable-text-concatenation)), which leaves `concat` results unstoned with excess capacity so that subsequent `s = s + x` can append in-place. + +The invariant is: **an unstoned heap text is uniquely referenced by exactly one slot.** This pass ensures that whenever a text value is copied or shared (via move, store, push, function argument, closure write, etc.), it is stoned first. + +**Algorithm:** + +1. **Compute liveness.** Build `first_ref[slot]` and `last_ref[slot]` arrays by scanning all instructions. Extend live ranges for backward jumps (loops): if a backward jump targets label L at position `lpos`, every slot referenced between `lpos` and the jump has its `last_ref` extended to the jump position. + +2. **Forward walk with type tracking.** Walk instructions using `track_types` to maintain per-slot types. At each escape point, if the escaping slot is provably `T_TEXT`, insert `stone_text slot` before the instruction. + +3. **Move special case.** For `move dest, src`: only insert `stone_text src` if the source is `T_TEXT` **and** `last_ref[src] > i` (the source slot is still live after the move, meaning both slots alias the same text). If the source is dead after the move, the value transfers uniquely — no stoning needed. + +**Escape points and the slot that gets stoned:** + +| Instruction | Stoned slot | Why it escapes | +|---|---|---| +| `move` | source (if still live) | Two slots alias the same value | +| `store_field` | value | Stored to object property | +| `store_index` | value | Stored to array element | +| `store_dynamic` | value | Dynamic property store | +| `push` | value | Pushed to array | +| `setarg` | value | Passed as function argument | +| `put` | source | Written to outer closure frame | + +**Not handled by this pass** (handled by VM guards instead): + +| Instruction | Reason | +|---|---| +| `get` (closure read) | Value arrives from outer frame; type may be T_UNKNOWN at compile time | +| `return` | Return value's type may be T_UNKNOWN; VM stones at inter-frame boundary | + +These two cases use runtime `stone_mutable_text` guards in the VM because the streamline pass cannot always prove the slot type across frame boundaries. + +**Nop prefix:** none (inserts instructions, does not create nops) + +### 8. eliminate_unreachable (dead code after return) Nops instructions after `return` until the next real label. Only `return` is treated as a terminal instruction; `disrupt` is not, because the disruption handler code immediately follows `disrupt` and must remain reachable. @@ -172,13 +209,13 @@ The mcode compiler emits a label at disruption handler entry points (see `emit_l **Nop prefix:** `_nop_ur_` -### 8. eliminate_dead_jumps (jump-to-next-label elimination) +### 9. eliminate_dead_jumps (jump-to-next-label elimination) Removes `jump L` instructions where `L` is the immediately following label (skipping over any intervening nop strings). These are common after other passes eliminate conditional branches, leaving behind jumps that fall through naturally. **Nop prefix:** `_nop_dj_` -### 9. diagnose_function (compile-time diagnostics) +### 10. diagnose_function (compile-time diagnostics) Optional pass that runs when `_warn` is set on the mcode input. Performs a forward type-tracking scan and emits diagnostics for provably wrong operations. Diagnostics are collected in `ir._diagnostics` as `{severity, file, line, col, message}` records. @@ -219,6 +256,7 @@ eliminate_type_checks → uses param_types + write_types simplify_algebra simplify_booleans eliminate_moves +insert_stone_text → escape analysis for mutable text eliminate_unreachable eliminate_dead_jumps diagnose_function → optional, when _warn is set @@ -286,7 +324,9 @@ move 2, 7 // i = temp subtract 2, 2, 6 // i = i - 1 (direct) ``` -The `+` operator is excluded from target slot propagation when it would use the full text+num dispatch (i.e., when neither operand is a known number), because writing both `concat` and `add` to the variable's slot would pollute its write type. When the known-number shortcut applies, `+` uses `emit_numeric_binop` and would be safe for target propagation, but this is not currently implemented — the exclusion is by operator kind, not by dispatch path. +The `+` operator uses target slot propagation when the target slot equals the left operand (`target == left_slot`), i.e. for self-assign patterns like `s = s + x`. In this case both `concat` and `add` write to the same slot that already holds the left operand, so write-type pollution is acceptable — the value is being updated in place. For other cases (target differs from left operand), `+` still allocates a temp to avoid polluting the target slot's write type with both T_TEXT and T_NUM. + +This enables the VM's in-place append fast path for string concatenation: when `concat dest, dest, src` has the same destination and left operand, the VM can append directly to a mutable text's excess capacity without allocating. ## Debugging Tools @@ -375,7 +415,7 @@ This was implemented and tested but causes a bootstrap failure during self-hosti ### Target Slot Propagation for Add with Known Numbers -When the known-number add shortcut applies (one operand is a literal number), the generated code uses `emit_numeric_binop` which has a single write path. Target slot propagation should be safe in this case, but is currently blocked by the blanket `kind != "+"` exclusion. Refining the exclusion to check whether the shortcut will apply (by testing `is_known_number` on either operand) would enable direct writes for patterns like `i = i + 1`. +When the known-number add shortcut applies (one operand is a literal number), the generated code uses `emit_numeric_binop` which has a single write path. Target slot propagation is already enabled for the self-assign case (`i = i + 1`), but when the target differs from the left operand and neither operand is a known number, a temp is still used. Refining the exclusion to check `is_known_number` would enable direct writes for the remaining non-self-assign cases like `j = i + 1`. ### Forward Type Narrowing from Typed Operations diff --git a/mcode.cm b/mcode.cm index e5cbb58f..868066c0 100644 --- a/mcode.cm +++ b/mcode.cm @@ -1514,8 +1514,10 @@ var mcode = function(ast) { // Standard binary ops left_slot = gen_expr(left, -1) right_slot = gen_expr(right, -1) - // Use target slot for ops without multi-type dispatch (add has text+num paths) - dest = (target >= 0 && kind != "+") ? target : alloc_slot() + // Use target slot for ops without multi-type dispatch (add has text+num paths). + // Exception: allow + to write directly to target when target == left_slot + // (self-assign pattern like s = s + x) since concat/add reads before writing. + dest = (target >= 0 && (kind != "+" || target == left_slot)) ? target : alloc_slot() op = binop_map[kind] if (op == null) { op = "add" diff --git a/source/mach.c b/source/mach.c index 93341ac9..50403d24 100644 --- a/source/mach.c +++ b/source/mach.c @@ -212,6 +212,7 @@ typedef enum MachOpcode { /* Text */ MACH_CONCAT, /* R(A) = R(B) ++ R(C) — string concatenation */ + MACH_STONE_TEXT, /* stone(R(A)) — freeze mutable text before escape */ /* Typed integer comparisons (ABC) */ MACH_EQ_INT, /* R(A) = (R(B) == R(C)) — int */ @@ -372,6 +373,7 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = { [MACH_NOP] = "nop", /* Mcode-derived */ [MACH_CONCAT] = "concat", + [MACH_STONE_TEXT] = "stone_text", [MACH_EQ_INT] = "eq_int", [MACH_NE_INT] = "ne_int", [MACH_LT_INT] = "lt_int", @@ -1392,7 +1394,7 @@ vm_dispatch: DT(MACH_HASPROP), DT(MACH_REGEXP), DT(MACH_EQ_TOL), DT(MACH_NEQ_TOL), DT(MACH_NOP), - DT(MACH_CONCAT), + DT(MACH_CONCAT), DT(MACH_STONE_TEXT), DT(MACH_EQ_INT), DT(MACH_NE_INT), DT(MACH_LT_INT), DT(MACH_LE_INT), DT(MACH_GT_INT), DT(MACH_GE_INT), @@ -2026,6 +2028,7 @@ vm_dispatch: } target = next; } + stone_mutable_text(target->slots[c]); frame->slots[a] = target->slots[c]; VM_BREAK(); } @@ -2123,6 +2126,7 @@ vm_dispatch: } VM_CASE(MACH_RETURN): + stone_mutable_text(frame->slots[a]); result = frame->slots[a]; if (!JS_IsPtr(frame->caller)) goto done; { @@ -2285,15 +2289,48 @@ vm_dispatch: /* === New mcode-derived opcodes === */ - /* Text concatenation */ + /* Text concatenation — with in-place append fast path for s = s + x */ VM_CASE(MACH_CONCAT): { - JSValue res = JS_ConcatString(ctx, frame->slots[b], frame->slots[c]); - frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); - if (JS_IsException(res)) goto disrupt; - frame->slots[a] = res; + if (a == b) { + /* Self-assign pattern: slot[a] = slot[a] + slot[c] */ + JSValue left = frame->slots[a]; + JSValue right = frame->slots[c]; + /* Inline fast path: mutable heap text with enough capacity */ + if (JS_IsPtr(left)) { + JSText *s = (JSText *)chase(left); + int slen = (int)s->length; + int rlen = js_string_value_len(right); + int cap = (int)objhdr_cap56(s->hdr); + if (objhdr_type(s->hdr) == OBJ_TEXT + && !(s->hdr & OBJHDR_S_MASK) + && slen + rlen <= cap) { + /* Append in-place — zero allocation, no GC possible */ + for (int i = 0; i < rlen; i++) + string_put(s, slen + i, js_string_value_get(right, i)); + s->length = slen + rlen; + VM_BREAK(); + } + } + /* Slow path: allocate with growth factor, leave unstoned */ + JSValue res = JS_ConcatStringGrow(ctx, frame->slots[b], frame->slots[c]); + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); + if (JS_IsException(res)) goto disrupt; + frame->slots[a] = res; + } else { + /* Different target: use existing exact-fit stoned path */ + JSValue res = JS_ConcatString(ctx, frame->slots[b], frame->slots[c]); + frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); + if (JS_IsException(res)) goto disrupt; + frame->slots[a] = res; + } VM_BREAK(); } + /* Stone mutable text — compiler-emitted at escape points */ + VM_CASE(MACH_STONE_TEXT): + stone_mutable_text(frame->slots[a]); + VM_BREAK(); + /* Typed integer comparisons */ VM_CASE(MACH_EQ_INT): frame->slots[a] = JS_NewBool(ctx, JS_VALUE_GET_INT(frame->slots[b]) == JS_VALUE_GET_INT(frame->slots[c])); @@ -3026,6 +3063,7 @@ static MachCode *mcode_lower_func(cJSON *fobj, const char *filename) { else if (strcmp(op, "move") == 0) { AB2(MACH_MOVE); } /* Text */ else if (strcmp(op, "concat") == 0) { ABC3(MACH_CONCAT); } + else if (strcmp(op, "stone_text") == 0) { EM(MACH_ABC(MACH_STONE_TEXT, A1, 0, 0)); } /* Generic arithmetic */ else if (strcmp(op, "add") == 0) { ABC3(MACH_ADD); } else if (strcmp(op, "subtract") == 0) { ABC3(MACH_SUB); } diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index 9b806f78..5f983769 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -478,6 +478,17 @@ static inline void mach_resolve_forward(JSValue *slot) { } } +/* Stone a mutable (unstoned) heap text in-place. Used at escape points + in the VM to enforce the invariant that an unstoned text is uniquely + referenced by exactly one slot. */ +static inline void stone_mutable_text(JSValue v) { + if (JS_IsPtr(v)) { + objhdr_t *oh = (objhdr_t *)JS_VALUE_GET_PTR(v); + if (objhdr_type(*oh) == OBJ_TEXT && !(*oh & OBJHDR_S_MASK)) + *oh = objhdr_set_s(*oh, true); + } +} + /* Inline type checks — use these in the VM dispatch loop to avoid function call overhead. The public API (JS_IsArray etc. in quickjs.h) remains non-inline for external callers; those wrappers live in runtime.c. */ @@ -1205,6 +1216,7 @@ int JS_SetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key, JSValue va void *js_realloc_rt (void *ptr, size_t size); char *js_strdup_rt (const char *str); JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2); +JSValue JS_ConcatStringGrow (JSContext *ctx, JSValue op1, JSValue op2); JSText *pretext_init (JSContext *ctx, int capacity); JSText *pretext_putc (JSContext *ctx, JSText *s, uint32_t c); JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v); diff --git a/source/runtime.c b/source/runtime.c index 8847f571..36fc55df 100644 --- a/source/runtime.c +++ b/source/runtime.c @@ -2900,6 +2900,84 @@ JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2) { return ret_val; } +/* Concat with over-allocated capacity and NO stoning. + Used by MACH_CONCAT self-assign (s = s + x) slow path so that + subsequent appends can reuse the excess capacity in-place. */ +JSValue JS_ConcatStringGrow (JSContext *ctx, JSValue op1, JSValue op2) { + if (unlikely (!JS_IsText (op1))) { + JSGCRef op2_guard; + JS_PushGCRef (ctx, &op2_guard); + op2_guard.val = op2; + op1 = JS_ToString (ctx, op1); + op2 = op2_guard.val; + JS_PopGCRef (ctx, &op2_guard); + if (JS_IsException (op1)) return JS_EXCEPTION; + } + if (unlikely (!JS_IsText (op2))) { + JSGCRef op1_guard; + JS_PushGCRef (ctx, &op1_guard); + op1_guard.val = op1; + op2 = JS_ToString (ctx, op2); + op1 = op1_guard.val; + JS_PopGCRef (ctx, &op1_guard); + if (JS_IsException (op2)) return JS_EXCEPTION; + } + + int len1 = js_string_value_len (op1); + int len2 = js_string_value_len (op2); + int new_len = len1 + len2; + + /* Try immediate ASCII for short results */ + if (new_len <= MIST_ASCII_MAX_LEN) { + char buf[8]; + BOOL all_ascii = TRUE; + for (int i = 0; i < len1 && all_ascii; i++) { + uint32_t c = js_string_value_get (op1, i); + if (c >= 0x80) all_ascii = FALSE; + else buf[i] = (char)c; + } + for (int i = 0; i < len2 && all_ascii; i++) { + uint32_t c = js_string_value_get (op2, i); + if (c >= 0x80) all_ascii = FALSE; + else buf[len1 + i] = (char)c; + } + if (all_ascii) { + JSValue imm = MIST_TryNewImmediateASCII (buf, new_len); + if (!JS_IsNull (imm)) return imm; + } + } + + /* Allocate with 2x growth factor, minimum 16 */ + int capacity = new_len * 2; + if (capacity < 16) capacity = 16; + + JSGCRef op1_ref, op2_ref; + JS_PushGCRef (ctx, &op1_ref); + op1_ref.val = op1; + JS_PushGCRef (ctx, &op2_ref); + op2_ref.val = op2; + + JSText *p = js_alloc_string (ctx, capacity); + if (!p) { + JS_PopGCRef (ctx, &op2_ref); + JS_PopGCRef (ctx, &op1_ref); + return JS_EXCEPTION; + } + + op1 = op1_ref.val; + op2 = op2_ref.val; + JS_PopGCRef (ctx, &op2_ref); + JS_PopGCRef (ctx, &op1_ref); + + for (int i = 0; i < len1; i++) + string_put (p, i, js_string_value_get (op1, i)); + for (int i = 0; i < len2; i++) + string_put (p, len1 + i, js_string_value_get (op2, i)); + p->length = new_len; + /* Do NOT stone — leave mutable so in-place append can reuse capacity */ + return JS_MKPTR (p); +} + /* WARNING: proto must be an object or JS_NULL */ JSValue JS_NewObjectProtoClass (JSContext *ctx, JSValue proto_val, JSClassID class_id) { JSGCRef proto_ref; diff --git a/streamline.cm b/streamline.cm index f7858c78..1b2f0c54 100644 --- a/streamline.cm +++ b/streamline.cm @@ -78,7 +78,8 @@ var streamline = function(ir, log) { jump: true, jump_true: true, jump_false: true, jump_not_null: true, return: true, disrupt: true, store_field: true, store_index: true, store_dynamic: true, - push: true, setarg: true, invoke: true, tail_invoke: true + push: true, setarg: true, invoke: true, tail_invoke: true, + stone_text: true } // --- Logging support --- @@ -1097,6 +1098,163 @@ var streamline = function(ir, log) { return null } + // ========================================================= + // Pass: insert_stone_text — freeze mutable text at escape points + // Only inserts stone_text when the slot is provably T_TEXT. + // Escape points: setfield, setindex, store_field, store_index, + // store_dynamic, push, setarg, put (value leaving its slot). + // move: stone source only if source is still live after the move. + // ========================================================= + + // Map: escape opcode → index of the escaping slot in the instruction + var escape_slot_index = { + setfield: 3, setindex: 3, + store_field: 3, store_index: 3, store_dynamic: 3, + push: 2, setarg: 3, put: 1 + } + + var insert_stone_text = function(func, log) { + var instructions = func.instructions + var nr_slots = func.nr_slots + var events = null + var slot_types = null + var result = null + var i = 0 + var j = 0 + var s = 0 + var n = 0 + var instr = null + var op = null + var esc = null + var slot = 0 + var nc = 0 + var limit = 0 + var first_ref = null + var last_ref = null + var label_map = null + var changed = false + var target = null + var tpos = 0 + + if (instructions == null || length(instructions) == 0) { + return null + } + + if (log != null && log.events != null) { + events = log.events + } + + // Build first_ref / last_ref for liveness (needed for move) + first_ref = array(nr_slots, -1) + last_ref = array(nr_slots, -1) + n = length(instructions) + i = 0 + while (i < n) { + instr = instructions[i] + if (is_array(instr)) { + j = 1 + limit = length(instr) - 2 + while (j < limit) { + if (is_number(instr[j]) && instr[j] >= 0 && instr[j] < nr_slots) { + if (first_ref[instr[j]] < 0) first_ref[instr[j]] = i + last_ref[instr[j]] = i + } + j = j + 1 + } + } + i = i + 1 + } + + // Extend for backward jumps (loops) + label_map = {} + i = 0 + while (i < n) { + instr = instructions[i] + if (is_text(instr) && !starts_with(instr, "_nop_")) { + label_map[instr] = i + } + i = i + 1 + } + changed = true + while (changed) { + changed = false + i = 0 + while (i < n) { + instr = instructions[i] + if (is_array(instr)) { + target = null + op = instr[0] + if (op == "jump") { + target = instr[1] + } else if (op == "jump_true" || op == "jump_false" || op == "jump_not_null") { + target = instr[2] + } + if (target != null && is_text(target)) { + tpos = label_map[target] + if (tpos != null && tpos < i) { + s = 0 + while (s < nr_slots) { + if (first_ref[s] >= 0 && first_ref[s] < tpos && last_ref[s] >= tpos && last_ref[s] < i) { + last_ref[s] = i + changed = true + } + s = s + 1 + } + } + } + } + i = i + 1 + } + } + + // Walk instructions, tracking types, inserting stone_text + slot_types = array(nr_slots, T_UNKNOWN) + result = [] + i = 0 + while (i < n) { + instr = instructions[i] + if (is_array(instr)) { + op = instr[0] + esc = escape_slot_index[op] + if (esc != null) { + slot = instr[esc] + if (is_number(slot) && slot_is(slot_types, slot, T_TEXT)) { + result[] = ["stone_text", slot] + nc = nc + 1 + if (events != null) { + events[] = { + event: "insert", pass: "insert_stone_text", + rule: "escape_stone", at: i, slot: slot, op: op + } + } + } + } else if (op == "move") { + // Stone source before move only if source is provably text + // AND source slot is still live after this instruction + slot = instr[2] + if (is_number(slot) && slot_is(slot_types, slot, T_TEXT) && last_ref[slot] > i) { + result[] = ["stone_text", slot] + nc = nc + 1 + if (events != null) { + events[] = { + event: "insert", pass: "insert_stone_text", + rule: "move_alias_stone", at: i, slot: slot + } + } + } + } + track_types(slot_types, instr) + } + result[] = instr + i = i + 1 + } + + if (nc > 0) { + func.instructions = result + } + return null + } + // ========================================================= // Pass: eliminate_unreachable — nop code after return/disrupt // ========================================================= @@ -1299,7 +1457,8 @@ var streamline = function(ir, log) { frame: [1, 2], goframe: [1, 2], jump: [], disrupt: [], jump_true: [1], jump_false: [1], jump_not_null: [1], - return: [1] + return: [1], + stone_text: [1] } var get_slot_refs = function(instr) { @@ -2116,6 +2275,12 @@ var streamline = function(ir, log) { }) if (verify_fn) verify_fn(func, "after " + name) + name = "insert_stone_text" + suffix + run_pass(func, name, function() { + return insert_stone_text(func, log) + }) + if (verify_fn) verify_fn(func, "after " + name) + name = "eliminate_unreachable" + suffix run_pass(func, name, function() { return eliminate_unreachable(func)