← Commit history
perf(ashvm): fuse x = x OP operand reassignment into OP_ACC_LOCAL; lowering emits unparenthesised accumulate
lennoxrose committed on Oct 18, 2026 at 00:42 UTC
2 files changed+57−35
hearth/ashvm/src/compiler/C/stmt.cmodified
+42−33@@ -19,39 +19,10 @@ void block(void) {1919
expect(TOKEN_RBRACE, "expected '}'");2020
}2121
22
-// local name = value; -- also tries to fuse `local x = x OP (number|local);`23
-// into a single OP_ACC_LOCAL instead of a full expression compile, when the24
-// shape matches exactly (see vm/compiler.c's history / rundown.md).25
-static void local_statement(void) {26
- advance_token();27
- expect(TOKEN_IDENTIFIER, "expected variable name after 'local'");28
- const char *name = previous.start;29
- int len = previous.length;30
- expect(TOKEN_EQUAL, "expected '=' after variable name");31
-32
- // Bug (found while writing the first real forgepack module, a JSON33
- // codec -- any imported function with a `local` inside a loop failed34
- // to compile): try_declare_import_constant() has no way to tell "a35
- // top-level local in an imported file" from "an ordinary local deep36
- // inside one of that file's own functions" except this check -- it37
- // only makes sense at block_depth == 0, same as the sibling guard in38
- // statement() above (which already gets this right).39
- if (block_depth == 0) {40
- const char *ns; int ns_len;41
- get_import_namespace(&ns, &ns_len);42
- if (ns_len > 0) {43
- // A top-level `local` of an imported file is a module-level variable.44
- int state_slot = module_state_declare(name, len);45
- expression();46
- expect(TOKEN_SEMICOLON, "expected ';' after statement");47
- emit2(OP_SET_GLOBAL, (uint8_t)state_slot);48
- emit_op(OP_POP);49
- return;50
- }51
- }52
-53
- int slot = resolve_local(name, len);54
-22
+// `x OP (number|local);` right after `x =` / `local x =`, for an existing local x, compiles to a single23
+// OP_ACC_LOCAL instead of a full expression compile. Returns 1 when it fused (statement fully consumed);24
+// otherwise the token stream is left exactly as it was.25
+static int try_fuse_accumulate(int slot, const char *name, int len) {5526
int fused = 0;5627
if (slot != -1 && current.type == TOKEN_IDENTIFIER &&5728
current.length == len && strncmp(current.start, name, len) == 0) {@@ -97,6 +68,43 @@ static void local_statement(void) {9768
}9869
if (!fused) { lexer_init(save.start + save.length); current = save; }9970
}71
+ return fused;72
+}73
+74
+// local name = value; -- also tries to fuse `local x = x OP (number|local);`75
+// into a single OP_ACC_LOCAL instead of a full expression compile, when the76
+// shape matches exactly (see vm/compiler.c's history / rundown.md).77
+static void local_statement(void) {78
+ advance_token();79
+ expect(TOKEN_IDENTIFIER, "expected variable name after 'local'");80
+ const char *name = previous.start;81
+ int len = previous.length;82
+ expect(TOKEN_EQUAL, "expected '=' after variable name");83
+84
+ // Bug (found while writing the first real forgepack module, a JSON85
+ // codec -- any imported function with a `local` inside a loop failed86
+ // to compile): try_declare_import_constant() has no way to tell "a87
+ // top-level local in an imported file" from "an ordinary local deep88
+ // inside one of that file's own functions" except this check -- it89
+ // only makes sense at block_depth == 0, same as the sibling guard in90
+ // statement() above (which already gets this right).91
+ if (block_depth == 0) {92
+ const char *ns; int ns_len;93
+ get_import_namespace(&ns, &ns_len);94
+ if (ns_len > 0) {95
+ // A top-level `local` of an imported file is a module-level variable.96
+ int state_slot = module_state_declare(name, len);97
+ expression();98
+ expect(TOKEN_SEMICOLON, "expected ';' after statement");99
+ emit2(OP_SET_GLOBAL, (uint8_t)state_slot);100
+ emit_op(OP_POP);101
+ return;102
+ }103
+ }104
+105
+ int slot = resolve_local(name, len);106
+107
+ int fused = try_fuse_accumulate(slot, name, len);100108
101109
if (!fused) {102110
expression();@@ -231,6 +239,7 @@ static void identifier_statement(void) {231239
if (current.type == TOKEN_EQUAL) {232240
int slot = resolve_local_or_error(id);233241
advance_token();242
+ if (slot >= 0 && try_fuse_accumulate(slot, id.start, id.length)) return;234243
expression();235244
expect(TOKEN_SEMICOLON, "expected ';' after assignment");236245
emit_ref_set(slot);hearth/front/src/jlower/C/lower_expr.cmodified
+15−2@@ -503,6 +503,19 @@ static void assign_parts(Node *e, char **lhs, char **rhs) {503503
*rhs = lx(e->b);504504
}505505
506
+/* `x = (x + i);` -> `x = x + i;`: the VM fuses the unparenthesised accumulate shape into one instruction */507
+static char *unparen(char *t) {508
+ size_t n = strlen(t);509
+ if (n < 2 || t[0] != '(' || t[n - 1] != ')') return t;510
+ int depth = 0;511
+ for (size_t i = 0; i < n; i++) {512
+ if (t[i] == '"') { for (i++; i < n && t[i] != '"'; i++) if (t[i] == '\\') i++; continue; }513
+ if (t[i] == '(') depth++;514
+ else if (t[i] == ')' && --depth == 0 && i != n - 1) return t;515
+ }516
+ return j_strndup(t + 1, n - 2);517
+}518
+506519
void emit_assign_stmt(JBuf *b, Node *e, int ind) {507520
if (e->kind == N_ASSIGN) {508521
char *lhs, *rhs;@@ -514,15 +527,15 @@ void emit_assign_stmt(JBuf *b, Node *e, int ind) {514527
rhs = binop(op, e->a->type, e->b->type, lhs, rhs, e);515528
}516529
flush_pre(b, ind);517
- emit_line(b, ind, "%s = %s;", lhs, rhs);530
+ emit_line(b, ind, "%s = %s;", lhs, unparen(rhs));518531
return;519532
}520533
/* ++ / -- as statements */521534
Node *t = e->a;522535
char *lhs = lvalue(t);523536
int inc = e->name[0] == '+';524537
flush_pre(b, ind);525
- emit_line(b, ind, "%s = %s;", lhs, incdec_text(e, lhs, inc));538
+ emit_line(b, ind, "%s = %s;", lhs, unparen(incdec_text(e, lhs, inc)));526539
}527540
528541