str stone; concat

This commit is contained in:
2026-02-20 21:54:19 -06:00
parent a82c13170f
commit fca1041e52
9 changed files with 389 additions and 15 deletions

View File

@@ -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. 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.). 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).

View File

@@ -101,6 +101,11 @@ Operands are register slot numbers (integers), constant values (strings, numbers
| Instruction | Operands | Description | | Instruction | Operands | Description |
|-------------|----------|-------------| |-------------|----------|-------------|
| `concat` | `dest, a, b` | `dest = a ~ b` (text concatenation) | | `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 ### Comparison — Integer

View File

@@ -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. 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 ## 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. 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.

View File

@@ -164,7 +164,44 @@ Removes `move a, a` instructions where the source and destination are the same s
**Nop prefix:** `_nop_mv_` **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. 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_` **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. 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_` **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. 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_algebra
simplify_booleans simplify_booleans
eliminate_moves eliminate_moves
insert_stone_text → escape analysis for mutable text
eliminate_unreachable eliminate_unreachable
eliminate_dead_jumps eliminate_dead_jumps
diagnose_function → optional, when _warn is set diagnose_function → optional, when _warn is set
@@ -286,7 +324,9 @@ move 2, 7 // i = temp
subtract 2, 2, 6 // i = i - 1 (direct) 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 ## 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 ### 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 ### Forward Type Narrowing from Typed Operations

View File

@@ -1514,8 +1514,10 @@ var mcode = function(ast) {
// Standard binary ops // Standard binary ops
left_slot = gen_expr(left, -1) left_slot = gen_expr(left, -1)
right_slot = gen_expr(right, -1) right_slot = gen_expr(right, -1)
// Use target slot for ops without multi-type dispatch (add has text+num paths) // Use target slot for ops without multi-type dispatch (add has text+num paths).
dest = (target >= 0 && kind != "+") ? target : alloc_slot() // 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] op = binop_map[kind]
if (op == null) { if (op == null) {
op = "add" op = "add"

View File

@@ -212,6 +212,7 @@ typedef enum MachOpcode {
/* Text */ /* Text */
MACH_CONCAT, /* R(A) = R(B) ++ R(C) — string concatenation */ 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) */ /* Typed integer comparisons (ABC) */
MACH_EQ_INT, /* R(A) = (R(B) == R(C)) — int */ 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", [MACH_NOP] = "nop",
/* Mcode-derived */ /* Mcode-derived */
[MACH_CONCAT] = "concat", [MACH_CONCAT] = "concat",
[MACH_STONE_TEXT] = "stone_text",
[MACH_EQ_INT] = "eq_int", [MACH_EQ_INT] = "eq_int",
[MACH_NE_INT] = "ne_int", [MACH_NE_INT] = "ne_int",
[MACH_LT_INT] = "lt_int", [MACH_LT_INT] = "lt_int",
@@ -1392,7 +1394,7 @@ vm_dispatch:
DT(MACH_HASPROP), DT(MACH_REGEXP), DT(MACH_HASPROP), DT(MACH_REGEXP),
DT(MACH_EQ_TOL), DT(MACH_NEQ_TOL), DT(MACH_EQ_TOL), DT(MACH_NEQ_TOL),
DT(MACH_NOP), DT(MACH_NOP),
DT(MACH_CONCAT), DT(MACH_CONCAT), DT(MACH_STONE_TEXT),
DT(MACH_EQ_INT), DT(MACH_NE_INT), DT(MACH_EQ_INT), DT(MACH_NE_INT),
DT(MACH_LT_INT), DT(MACH_LE_INT), DT(MACH_LT_INT), DT(MACH_LE_INT),
DT(MACH_GT_INT), DT(MACH_GE_INT), DT(MACH_GT_INT), DT(MACH_GE_INT),
@@ -2026,6 +2028,7 @@ vm_dispatch:
} }
target = next; target = next;
} }
stone_mutable_text(target->slots[c]);
frame->slots[a] = target->slots[c]; frame->slots[a] = target->slots[c];
VM_BREAK(); VM_BREAK();
} }
@@ -2123,6 +2126,7 @@ vm_dispatch:
} }
VM_CASE(MACH_RETURN): VM_CASE(MACH_RETURN):
stone_mutable_text(frame->slots[a]);
result = frame->slots[a]; result = frame->slots[a];
if (!JS_IsPtr(frame->caller)) goto done; if (!JS_IsPtr(frame->caller)) goto done;
{ {
@@ -2285,15 +2289,48 @@ vm_dispatch:
/* === New mcode-derived opcodes === */ /* === New mcode-derived opcodes === */
/* Text concatenation */ /* Text concatenation — with in-place append fast path for s = s + x */
VM_CASE(MACH_CONCAT): { VM_CASE(MACH_CONCAT): {
JSValue res = JS_ConcatString(ctx, frame->slots[b], frame->slots[c]); if (a == b) {
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); /* Self-assign pattern: slot[a] = slot[a] + slot[c] */
if (JS_IsException(res)) goto disrupt; JSValue left = frame->slots[a];
frame->slots[a] = res; 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(); 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 */ /* Typed integer comparisons */
VM_CASE(MACH_EQ_INT): 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])); 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); } else if (strcmp(op, "move") == 0) { AB2(MACH_MOVE); }
/* Text */ /* Text */
else if (strcmp(op, "concat") == 0) { ABC3(MACH_CONCAT); } 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 */ /* Generic arithmetic */
else if (strcmp(op, "add") == 0) { ABC3(MACH_ADD); } else if (strcmp(op, "add") == 0) { ABC3(MACH_ADD); }
else if (strcmp(op, "subtract") == 0) { ABC3(MACH_SUB); } else if (strcmp(op, "subtract") == 0) { ABC3(MACH_SUB); }

View File

@@ -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 /* Inline type checks — use these in the VM dispatch loop to avoid
function call overhead. The public API (JS_IsArray etc. in quickjs.h) function call overhead. The public API (JS_IsArray etc. in quickjs.h)
remains non-inline for external callers; those wrappers live in runtime.c. */ 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); void *js_realloc_rt (void *ptr, size_t size);
char *js_strdup_rt (const char *str); char *js_strdup_rt (const char *str);
JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2); 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_init (JSContext *ctx, int capacity);
JSText *pretext_putc (JSContext *ctx, JSText *s, uint32_t c); JSText *pretext_putc (JSContext *ctx, JSText *s, uint32_t c);
JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v); JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v);

View File

@@ -2900,6 +2900,84 @@ JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2) {
return ret_val; 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 */ /* WARNING: proto must be an object or JS_NULL */
JSValue JS_NewObjectProtoClass (JSContext *ctx, JSValue proto_val, JSClassID class_id) { JSValue JS_NewObjectProtoClass (JSContext *ctx, JSValue proto_val, JSClassID class_id) {
JSGCRef proto_ref; JSGCRef proto_ref;

View File

@@ -78,7 +78,8 @@ var streamline = function(ir, log) {
jump: true, jump_true: true, jump_false: true, jump_not_null: true, jump: true, jump_true: true, jump_false: true, jump_not_null: true,
return: true, disrupt: true, return: true, disrupt: true,
store_field: true, store_index: true, store_dynamic: 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 --- // --- Logging support ---
@@ -1097,6 +1098,163 @@ var streamline = function(ir, log) {
return null 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 // Pass: eliminate_unreachable — nop code after return/disrupt
// ========================================================= // =========================================================
@@ -1299,7 +1457,8 @@ var streamline = function(ir, log) {
frame: [1, 2], goframe: [1, 2], frame: [1, 2], goframe: [1, 2],
jump: [], disrupt: [], jump: [], disrupt: [],
jump_true: [1], jump_false: [1], jump_not_null: [1], jump_true: [1], jump_false: [1], jump_not_null: [1],
return: [1] return: [1],
stone_text: [1]
} }
var get_slot_refs = function(instr) { var get_slot_refs = function(instr) {
@@ -2116,6 +2275,12 @@ var streamline = function(ir, log) {
}) })
if (verify_fn) verify_fn(func, "after " + name) 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 name = "eliminate_unreachable" + suffix
run_pass(func, name, function() { run_pass(func, name, function() {
return eliminate_unreachable(func) return eliminate_unreachable(func)