str stone; concat
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
6
mcode.cm
6
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"
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
169
streamline.cm
169
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)
|
||||
|
||||
Reference in New Issue
Block a user