← 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

67d5996parent8f85ee4

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 the
24-// 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 JSON
33- // codec -- any imported function with a `local` inside a loop failed
34- // to compile): try_declare_import_constant() has no way to tell "a
35- // top-level local in an imported file" from "an ordinary local deep
36- // inside one of that file's own functions" except this check -- it
37- // only makes sense at block_depth == 0, same as the sibling guard in
38- // 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 single
23+// 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 the
76+// 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 JSON
85+ // codec -- any imported function with a `local` inside a loop failed
86+ // to compile): try_declare_import_constant() has no way to tell "a
87+ // top-level local in an imported file" from "an ordinary local deep
88+ // inside one of that file's own functions" except this check -- it
89+ // only makes sense at block_depth == 0, same as the sibling guard in
90+ // 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