Merge branch 'improve_str_concat' into optimize_mcode

This commit is contained in:
2026-02-20 21:55:22 -06:00
9 changed files with 389 additions and 15 deletions

View File

@@ -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",
@@ -1395,7 +1397,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),
@@ -2029,6 +2031,7 @@ vm_dispatch:
}
target = next;
}
stone_mutable_text(target->slots[c]);
frame->slots[a] = target->slots[c];
VM_BREAK();
}
@@ -2126,6 +2129,7 @@ vm_dispatch:
}
VM_CASE(MACH_RETURN):
stone_mutable_text(frame->slots[a]);
result = frame->slots[a];
if (!JS_IsPtr(frame->caller)) goto done;
{
@@ -2288,15 +2292,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]));
@@ -3005,6 +3042,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); }

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
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);

View File

@@ -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;