From 7ab4d034faa0a0b35a7f6264cff7293123943e14 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 5 Jun 2026 18:39:22 +0000 Subject: [PATCH 1/7] Guard total varstack size against the 127-variable limit rule_initialize already rejects a single ruleset whose own varsize exceeds INT8_MAX variables, but the varstack accumulates across all rulesets. A ruleset that fits on its own could still push the running varstack total past 127 slots, overflowing the int8 variable index. Add a second guard on (varstack->nrbytes + varsize) so the combined total is checked, with a matching error message. Co-Authored-By: Claude Opus 4.8 --- src/rules/rules.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/rules/rules.cpp b/src/rules/rules.cpp index 0e1afd9..6291f2b 100755 --- a/src/rules/rules.cpp +++ b/src/rules/rules.cpp @@ -5386,10 +5386,14 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr /*LCOV_EXCL_STOP*/ if(rule_prepare((char **)&input->payload, &bcsize, &heapsize, &varsize, &memsize, &newlen) == -1 || - (varsize/sizeof(struct vm_vchar_t)) > INT8_MAX) { + (varsize/sizeof(struct vm_vchar_t)) > INT8_MAX || + ((varstack->nrbytes + varsize) / sizeof(struct vm_vchar_t)) > INT8_MAX) { if(varsize/sizeof(struct vm_vchar_t) > INT8_MAX) { logprintf_P(F("ERROR: maximum number of 127 variables reached")); } + if((varstack->nrbytes + varsize) / sizeof(struct vm_vchar_t) > INT8_MAX) { + logprintf_P(F("ERROR: maximum number of 127 variables reached")); + } if((*rules = (struct rules_t **)REALLOC(*rules, sizeof(struct rules_t **)*((*nrrules)))) == NULL) { OUT_OF_MEMORY } From 07aa214fb792416875561355a16cfc35fe199ea1 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 5 Jun 2026 18:39:38 +0000 Subject: [PATCH 2/7] Reset mathcnt when a call statement closes an if-block Two consecutive `if then end` blocks miscompiled: the first block's trailing OP_AND/OP_OR was relocated into the second block, so both bodies fired with the wrong condition. The assignment-statement path in rule_create resets mathcnt to 0, but the call/expression-statement terminator did not. mathcnt then leaked across the block boundary, so the second block's first operand no longer had a == 1, and bc_parse_math_order's backward limit search overshot past the first block's call body and JMP into its first OP_GETVAL. The operator-reorder window then spanned both blocks. Reset mathcnt to 0 in the TEND branch of the TSEMICOLON handler, i.e. only when a call/expression statement is the last in its block. Doing it at every OP_CLEAR instead changes slot reuse for multi-statement bodies and regresses byte counts. Fixes #894 Co-Authored-By: Claude Opus 4.8 --- src/rules/rules.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/rules/rules.cpp b/src/rules/rules.cpp index 6291f2b..6d95a00 100755 --- a/src/rules/rules.cpp +++ b/src/rules/rules.cpp @@ -3376,6 +3376,14 @@ static int16_t rule_create(char **text, struct rules_t *obj) { return -1; /* LCOV_EXCL_STOP*/ } + /* + * A call/expression statement closes this if-block. Reset the + * temporary slot counter so a following sibling if-condition + * starts fresh. Otherwise mathcnt leaks across the block and + * bc_parse_math_order's backward slot search overshoots into + * this block, relocating its trailing logical operator (#894). + */ + mathcnt = 0; go = TEND; ret = TIF; continue; From 81f25750259eb1492b8627000e00104eca37d230 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 5 Jun 2026 19:08:40 +0000 Subject: [PATCH 3/7] Don't scan preceding bytecode for standalone-call slot assignment When a standalone call (e.g. foo();) is the only statement in an if-block, the TSEMICOLON handler sets OP_CALL.a to 0. bc_assign_slots' segment detection only advances `start` when it finds a node with a > 0, so with OP_CALL.a = 0 `start` stayed at its previous value and the slot-assignment loops scanned preceding bytecode, in some cases corrupting OP_JMP jump offsets and over-allocating heap slots. Fix 1: initialise start = end after the skip-JMPs step, so when no positive-slot node is found the loops scan only the actual segment. Fix 2: add the same OP_CALL/a==0 continue to loop2 that loop1 already has, so loop2 does not overwrite the no-return-value sentinel. This yields a tighter (4 bytes smaller) layout for 10 standalone-call unittests with identical runtime output; their expected byte counts are updated accordingly. Co-Authored-By: Claude Opus 4.8 --- main.cpp | 20 ++++++++++---------- src/rules/rules.cpp | 4 ++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/main.cpp b/main.cpp index 0c860f1..c480197 100755 --- a/main.cpp +++ b/main.cpp @@ -394,9 +394,9 @@ bar", 127 } }, 0 }, // Newline { "if 1 == 1 then $a = 3; end", { "[1]$a = 3", 107 }, { "[1]$a = 3", 107 }, 0 }, { "if 1 == 1 then $a = 3.1; $b = $a; end", { "[1]$a = 3.1[1]$b = 3.1", 134 }, { "[1]$a = 3.1[1]$b = 3.1", 134 }, 0 }, { "if 1 == 1 then $a = $a + 1; end", { "[1]$a = 2", 111 }, { "[1]$a = 2", 111 }, 0 }, - { "if 1 == 1 then $a = 1; print($a); end", { "[1]$a = 1", 123 }, { "[1]$a = 1", 123 }, 0 }, + { "if 1 == 1 then $a = 1; print($a); end", { "[1]$a = 1", 119 }, { "[1]$a = 1", 119 }, 0 }, { "if 1 == 1 then $a = 1; $b = 1.2; print($a, '-', $b, '-', $c); end", { "[1]$a = 1[1]$b = 1.2", 231 }, { "[1]$a = 1[1]$b = 1.2", 231 }, 0 }, - { "if 1 == 1 then $a = 1; max($a); end", { "[1]$a = 1", 123 }, { "[1]$a = 1", 123 }, 0 }, + { "if 1 == 1 then $a = 1; max($a); end", { "[1]$a = 1", 119 }, { "[1]$a = 1", 119 }, 0 }, { "if 1 == 1 then $a = max(foo#bar); end", { "[1]$a = 3", 139 }, { "[1]$a = 3", 139 }, 0 }, { "if 1 == 1 then $a = max(foo#bar, 4); end", { "[1]$a = 4", 151 }, { "[1]$a = 4", 151 }, 0 }, { "if 1 == 1 then $a = max(foo#bar, 4, 5.5); end", { "[1]$a = 5.5", 159 }, { "[1]$a = 5.5", 159 }, 0 }, @@ -425,7 +425,7 @@ bar", 127 } }, 0 }, // Newline { "if 3 == 3 then $a = max(1, 2, 3, 4); end", { "[1]$a = 4", 139 }, { "[1]$a = 4", 139 }, 0 }, { "if 3 == 3 then $a = max(1, 4, 5, 3, 2); end", { "[1]$a = 5", 147 }, { "[1]$a = 5", 147 }, 0 }, { "if 3 == 3 then $a = max(max(1, 4), 2); end", { "[1]$a = 4", 147 }, { "[1]$a = 4", 147 }, 0 }, - { "if 3 == 3 then max(max(1, 4), 2); end", { "", 128 }, { "", 128 }, 0 }, + { "if 3 == 3 then max(max(1, 4), 2); end", { "", 124 }, { "", 124 }, 0 }, { "if 3 == 3 then max(2); min(2); end", { "", 108 }, { "", 108 }, 0 }, { "if 3 == 3 then max(2); min(2); max(3); end", { "", 120 }, { "", 120 }, 0 }, { "if 3 == 3 then max($a, 4, 2); $b = max(1, 3); end", { "[1]$b = 3", 174 }, { "[1]$b = 3", 174 }, 0 }, @@ -538,7 +538,7 @@ bar", 127 } }, 0 }, // Newline { "on foo($a, $b) then $a = $b; end if 3 == 3 then foo(1); $b = 3; end ", { { "[1]$a = NULL[1]$b = NULL", 142 }, { "[1]$a = NULL[1]$b = NULL[2]$b = 3", 198 } }, { { "[1]$a = NULL[1]$b = NULL", 202 }, { "[1]$a = NULL[1]$b = NULL[2]$b = 3", 202 } }, 0 }, { "on foo($a, $c) then $a = $c; end if 3 == 3 then foo(NULL, 1); $b = 3; end ", { { "[1]$a = NULL[1]$c = NULL", 142 }, { "[1]$a = 1[1]$c = 1[2]$b = 3", 225 } }, { { "[1]$a = NULL[1]$c = NULL", 202 }, { "[1]$a = 1[1]$c = 1[2]$b = 3", 202 } }, 0 }, { "if 3 == 3 then foo(1, 2); $b = 3; end ", { "[1]$b = 3", 147 }, { "[1]$b = 3", 147 }, 0 }, - { "if 3 == 3 then $a = 1; foo($a, 2); end", { { "[1]$a = 1", 155 }, { "[1]$a = 1", 155 } }, { { "[1]$a = 1", 155 }, { "[1]$x = 1[1]$y = 2[1]$z = 3", 202 } }, 0 }, + { "if 3 == 3 then $a = 1; foo($a, 2); end", { { "[1]$a = 1", 151 }, { "[1]$a = 1", 155 } }, { { "[1]$a = 1", 155 }, { "[1]$x = 1[1]$y = 2[1]$z = 3", 202 } }, 0 }, { "on foo($b, $c) then $a = $b + $c; end if 3 == 3 then $a = 1; $b = 2; foo($a, $b); end ", { { "[1]$b = NULL[1]$c = NULL[1]$a = NULL", 173 }, { "[1]$b = 1[1]$c = 2[1]$a = 3[2]$a = 1[2]$b = 2", 241 } }, { { "[1]$b = NULL[1]$c = NULL[1]$a = NULL", 202 }, { "[1]$b = 1[1]$c = 2[1]$a = 3[2]$a = 1[2]$b = 2", 202 } }, 0 }, { "on foo($a, $b) then $a = $b; end if 3 == 3 then foo(1, 5); $b = 3; end ", { { "[1]$a = NULL[1]$b = NULL", 142 }, { "[1]$a = 5[1]$b = 5[2]$b = 3", 206 } }, { { "[1]$a = NULL[1]$b = NULL", 202 }, { "[1]$a = 5[1]$b = 5[2]$b = 3", 202 } }, 0 }, { "on foo then max(1, 2); end", { "", 108 }, { "", 108 }, 0 }, @@ -554,13 +554,13 @@ bar", 127 } }, 0 }, // Newline { "on foo then if 5 == 6 then $a = 1; end if 1 == 3 then $b = 3; end $a = 2; end", { "[1]$a = 2[1]$b = 3", 174 }, { "[1]$a = 2", 174 }, 0 }, { "if 1 == 1 then $a = 1; else $a = min(1, 2, 3); end", { "[1]$a = 1", 139 }, { "[1]$a = 1", 139 }, 0 }, { "if 1 == 1 then $a = 1; else $a = min(max(1, 2), 2, 3); end", { "[1]$a = 2", 155 }, { "[1]$a = 1", 155 }, 0 }, - { "on bar then $a = 1; end on foo then $b = max(1, 2); bar(); end if 3 == 3 then foo(); $a = min(1, 2); end", { { "[1]$a = 1", 111 }, { "[1]$a = 1[2]$b = 2", 226 }, { "[1]$a = 1[2]$b = 2[3]$a = 1", 274 } }, { { "[1]$a = 1", 16 }, { "[1]$a = 1[2]$b = 2", 16 }, { "[1]$a = 1[2]$b = 2[3]$a = 1", 16 } }, 0 }, + { "on bar then $a = 1; end on foo then $b = max(1, 2); bar(); end if 3 == 3 then foo(); $a = min(1, 2); end", { { "[1]$a = 1", 111 }, { "[1]$a = 1[2]$b = 2", 222 }, { "[1]$a = 1[2]$b = 2[3]$a = 1", 274 } }, { { "[1]$a = 1", 16 }, { "[1]$a = 1[2]$b = 2", 16 }, { "[1]$a = 1[2]$b = 2[3]$a = 1", 16 } }, 0 }, { "on foo then if max(1) == max(1) then $a = 1; end end", { "[1]$a = 1", 143 }, { "[1]$a = 1", 143 }, 0 }, { "on foo then if max($c) == 3 && max($a) then $a = 1; end end", { "[1]$a = 1", 178 }, { "[1]$a = 1", 178 }, 0 }, { "on foo then $a = 6; end if 3 == 3 then $b = 3; end ", { { "[1]$a = 6", 111 }, { "[2]$b = 3", 182 } }, { { "[1]$a = 6", 111 }, { "[2]$b = 3", 111 } }, 0 }, { "on foo then $a = 6; end if 3 == 3 then foo(); $b = 3; end ", { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 190 } }, { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 111 } }, 0 }, { "on foo then $a = 6; end if 3 == 3 then $b = 3; foo(); end ", { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 190 } }, { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 190 } }, 0 }, - { "on foo then $a = 6; end if 3 == 3 then foo(max(1, 2), 2); $b = 3; end ", { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 226 } }, { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 147 } }, 0 }, + { "on foo then $a = 6; end if 3 == 3 then foo(max(1, 2), 2); $b = 3; end ", { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 222 } }, { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 147 } }, 0 }, { "on foo then $a = 6; end if 3 == 3 then foo(1, 2); $b = 3; end ", { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 206 } }, { { "[1]$a = 6", 111 }, { "[1]$a = 6[2]$b = 3", 147 } }, 0 }, { "on foo then $a = coalesce($b, 0); end ", { { "[1]$a = 2", 154 } }, { { "[1]$a = 2", 154 } }, 0 }, // FIXME { "on foo then if 1 == 2 then $a = 1; elseif 2 == 2 then $a = 3; else $a = 2; end end", { "[1]$a = 2", 155 }, { "[1]$a = 3", 139 }, 0 }, @@ -576,12 +576,12 @@ bar", 127 } }, 0 }, // Newline { "on foo then $a = 1; end if 3 == 3 then $a = 'foo'; end", { { "[1]$a = 1", 111 }, { "[2]$a = foo", 163 } }, { { "[1]$a = 1", 111 }, { "[2]$a = foo", 202 } }, 0 }, { "if 1 == 1 then $a = 1; $b = 2; $aa = round($a / (($b * 230) + 50) * 10) / 10; $b = 2.1; $bb = round($a / (($b * 230) + 50) * 10) / 10; $b = 2.2; $cc = round($a / (($b * 230) + 50) * 10) / 10; $b = 2.3; $dd = round($a / (($b * 230) + 50) * 10) / 10; $b = 2.4; $dd = round($a / (($b * 230) + 50) * 10) / 10; $b = 2.5; $ee = round($a / (($b * 230) + 50) * 10) / 10; $b = 2.6; $ff = round($a / (($b * 230) + 50) * 10) / 10; $b = 2.7; $ff = round($a / (($b * 230) + 50) * 10) / 10; $b = 2.8; $gg = round($a / (($b * 230) + 50) * 10) / 10; end", { "[1]$a = 1[1]$b = 2.8[1]$aa = 0[1]$bb = 0[1]$cc = 0[1]$dd = 0[1]$ee = 0[1]$ff = 0[1]$gg = 0", 710 }, { "[1]$a = 1[1]$b = 2.8[1]$aa = 0[1]$bb = 0[1]$cc = 0[1]$dd = 0[1]$ee = 0[1]$ff = 0[1]$gg = 0", 710 }, 0 }, { "on foo then coalesce(10, 5); $a = 1; $b = 2; if $c == 12 && $d == 0 then $e = 1; end", { "[1]$a = 1[1]$b = 2[1]$e = 1", 263 }, { "[1]$a = 1[1]$b = 2", 263 }, 0 }, - { "on foo($a, $b) then print($a); $b = 1; end", { "[1]$a = NULL[1]$b = 1", 162 }, { "[1]$a = NULL[1]$b = 1", 158 }, 0 }, - { "on foo then print($a); $b = 1; end", { "[1]$b = 1", 154 }, { "[1]$b = 1", 150 }, 0 }, - { "on sub2($a) then print($a); $b = $a; end if 1 == 1 then print($a); sub2(2); end", { { "[1]$a = NULL[1]$b = NULL", 159 }, { "[1]$a = 2[1]$b = 2", 215 } }, { { "[1]$a = NULL[1]$b = NULL", 128 },{ "[1]$a = 2[1]$b = 2", 215 } }, 0 }, + { "on foo($a, $b) then print($a); $b = 1; end", { "[1]$a = NULL[1]$b = 1", 158 }, { "[1]$a = NULL[1]$b = 1", 158 }, 0 }, + { "on foo then print($a); $b = 1; end", { "[1]$b = 1", 150 }, { "[1]$b = 1", 150 }, 0 }, + { "on sub2($a) then print($a); $b = $a; end if 1 == 1 then print($a); sub2(2); end", { { "[1]$a = NULL[1]$b = NULL", 159 }, { "[1]$a = 2[1]$b = 2", 211 } }, { { "[1]$a = NULL[1]$b = NULL", 128 },{ "[1]$a = 2[1]$b = 2", 215 } }, 0 }, { "on sub2($a) then print($a); $c = $a + 1; end on sub1($a) then print($a); sub2($a + 1); $b = $a - 1; end if 1 == 1 then sub1(1); end", { { "[1]$a = NULL[1]$c = NULL", 167 }, { "[1]$a = NULL[1]$c = NULL[2]$a = NULL[2]$b = NULL", 271 }, { "[1]$a = 2[1]$c = 3[2]$a = 1[2]$b = 0", 271 } }, { { "[1]$a = NULL[1]$c = NULL", 128 }, { "[1]$a = NULL[1]$c = NULL[2]$a = NULL[2]$b = NULL", 196 }, { "[1]$a = 2[1]$c = 3[2]$a = 1[2]$b = 0", 196 } }, 0 }, { "if 1 == 1 then $a = 1; $b = 1; $c = 1; $d = 1; $e = 1; $f = 1; $g = 1; $h = 1; $i = 1; $j = 1; $k = 1; $l = 1; $m = 1; $n = 1; $o = 1; $p = 1; $q = 1; $r = 1; $s = 1; $t = 1; $u = 1; $v = 1; $w = 1; $x = 1; $y = 1; $z = 1; $aa = 1; $ab = 1; $ac = 1; $ad = 1; $ae = 1; $af = 1; $ag = 1; $ah = 1; $aj = 1; $aj = 1; $ak = 1; $al = 1; $am = 1; $an = 1; $ao = 1; $ap = 1; $aq = 1; $ar = 1; $as = 1; $at = 1; $au = 1; $av = 1; $aw = 1; $ax = 1; $ay = 1; $az = 1; $ba = 1; $bb = 1; $bc = 1; $bd = 1; $be = 1; $bf = 1; $bg = 1; $bh = 1; $bj = 1; $bj = 1; $bk = 1; $bl = 1; $bm = 1; $bn = 1; $bo = 1; $bp = 1; $bq = 1; $br = 1; $bs = 1; $bt = 1; $bu = 1; $bv = 1; $bw = 1; $bx = 1; $by = 1; $bz = 1; $ca = 1; $cb = 1; $cc = 1; $cd = 1; $ce = 1; $cf = 1; $cg = 1; $ch = 1; $cj = 1; $cj = 1; $ck = 1; $cl = 1; $cm = 1; $cn = 1; $co = 1; $cp = 1; $cq = 1; $cr = 1; $cs = 1; $ct = 1; $cu = 1; $cv = 1; $cw = 1; $cx = 1; $cy = 1; $cz = 1; $da = 1; $db = 1; $dc = 1; $dd = 1; $de = 1; $df = 1; $dg = 1; $dh = 1; $dj = 1; end", { { "[1]$de = 1[1]$df = 1[1]$dg = 1[1]$dh = 1[1]$dj = 1", 2706 } }, { { "[1]$de = 1[1]$df = 1[1]$dg = 1[1]$dh = 1[1]$dj = 1", 0 } }, 0 }, - { "on sub2 then sub1(1); end on sub1($a) then print($a); end on sub3 then sub2(); end", { { "", 122 }, { "[2]$a = NULL", 193 }, { "[2]$a = 1", 234 } }, { { "", 128 },{ "[2]$a = NULL", 215 }, { "[2]$a = 1", 233 } }, 0 }, + { "on sub2 then sub1(1); end on sub1($a) then print($a); end on sub3 then sub2(); end", { { "", 122 }, { "[2]$a = NULL", 189 }, { "[2]$a = 1", 234 } }, { { "", 128 },{ "[2]$a = NULL", 215 }, { "[2]$a = 1", 233 } }, 0 }, { "on System#Boot then max(10, 20); end on timer=10 then $Z = 3.5; $T = max(3 + 0.5); max(10, 2); if $T == $Z then $Z = 2; end end", { { "", 116 }, { "[2]$Z = 2[2]$T = 3.5", 283 } }, { { "", 134 }, { "[2]$Z = 2[2]$T = 3.5", 358 } }, 0 }, /* diff --git a/src/rules/rules.cpp b/src/rules/rules.cpp index 6d95a00..0df9d47 100755 --- a/src/rules/rules.cpp +++ b/src/rules/rules.cpp @@ -2024,6 +2024,7 @@ static void bc_assign_slots(struct rules_t *obj) { } } end = a; + start = end; for(a=end;abc.nrbytes);a = bc_next(obj, a)) { if(a == -1) { @@ -2202,6 +2203,9 @@ static void bc_assign_slots(struct rules_t *obj) { if(gettype(obj->bc.buffer[a]) == OP_CLEAR) { continue; } + if(gettype(obj->bc.buffer[a]) == OP_CALL && getval(x->a) == 0) { + continue; + } if(d >= min) { if(tmp > 0 && gettype(obj->bc.buffer[a]) == OP_CALL && gettype(obj->bc.buffer[tmp]) != OP_CLEAR) { From 1f5c887b8907b75d260b90aa157bd2b9094d0efb Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 5 Jun 2026 19:15:37 +0000 Subject: [PATCH 4/7] Report varstack slot usage in the bytecode debug log Append "varstack slots: %d/127" to the two bytecode debug log lines so the running variable-slot count is visible alongside the new total varstack guard. Cosmetic; debug logging only, no functional change. Co-Authored-By: Claude Opus 4.8 --- src/rules/rules.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/rules/rules.cpp b/src/rules/rules.cpp index 0df9d47..da4424d 100755 --- a/src/rules/rules.cpp +++ b/src/rules/rules.cpp @@ -5535,7 +5535,7 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr timestamp.second = micros(); logprintf_P(F("rule #%d bytecode was created in %d microseconds"), getval(obj->nr), timestamp.second - timestamp.first); - logprintf_P(F("bytecode: %d/%d, heap: %d/%d, stack: %d/%d bytes, varstack: %d/%d bytes"), + logprintf_P(F("bytecode: %d/%d, heap: %d/%d, stack: %d/%d bytes, varstack: %d/%d bytes, varstack slots: %d/127"), getval(obj->bc.nrbytes), getval(obj->bc.bufsize), getval(obj->heap->nrbytes), @@ -5543,7 +5543,8 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr ((stack == NULL) ? 0 : getval(stack->nrbytes)), ((stack == NULL) ? 0 : getval(stack->bufsize)), ((varstack->nrbytes == 0) ? 0 : varstack->nrbytes), - (varstack->bufsize) + (varstack->bufsize), + varstack->nrbytes / sizeof(struct vm_vchar_t) ); #else clock_gettime(CLOCK_MONOTONIC, ×tamp.second); @@ -5611,7 +5612,7 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr timestamp.second = micros(); logprintf_P(F("rule #%d was executed in %d microseconds"), getval(obj->nr), timestamp.second - timestamp.first); - logprintf_P(F("bytecode: %d/%d, heap: %d/%d, stack: %d/%d bytes, varstack: %d/%d bytes"), + logprintf_P(F("bytecode: %d/%d, heap: %d/%d, stack: %d/%d bytes, varstack: %d/%d bytes, varstack slots: %d/127"), getval(obj->bc.nrbytes), getval(obj->bc.bufsize), getval(obj->heap->nrbytes), @@ -5619,7 +5620,8 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr ((stack == NULL) ? 0 : getval(stack->nrbytes)), ((stack == NULL) ? 0 : getval(stack->bufsize)), ((varstack->nrbytes == 0) ? 0 : varstack->nrbytes), - (varstack->bufsize) + (varstack->bufsize), + varstack->nrbytes / sizeof(struct vm_vchar_t) ); #else clock_gettime(CLOCK_MONOTONIC, ×tamp.second); From 0df59aae2b836ca241d96ff30d9eeaf1e3bddc3d Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Thu, 30 Jul 2026 06:06:54 +0000 Subject: [PATCH 5/7] Reset mathcnt when a call statement is followed by a sibling if/event/function Extends the #894 fix, which only reset mathcnt when a call/expression statement closed an if-block via `end`. The same leak happens when the call statement is instead followed by a sibling statement (nested if, event, or function call) at the same level - bc_parse_math_order's backward slot search can still overshoot into the call's slots and corrupt the next statement's condition, notably a following && compound condition (HeishaMon issue #946). Updates the one existing test fixture whose expected bytecode size grew by the extra reset (283 -> 291); all other outputs/sizes are unchanged. --- main.cpp | 2 +- src/rules/rules.cpp | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index c480197..55b5d57 100755 --- a/main.cpp +++ b/main.cpp @@ -582,7 +582,7 @@ bar", 127 } }, 0 }, // Newline { "on sub2($a) then print($a); $c = $a + 1; end on sub1($a) then print($a); sub2($a + 1); $b = $a - 1; end if 1 == 1 then sub1(1); end", { { "[1]$a = NULL[1]$c = NULL", 167 }, { "[1]$a = NULL[1]$c = NULL[2]$a = NULL[2]$b = NULL", 271 }, { "[1]$a = 2[1]$c = 3[2]$a = 1[2]$b = 0", 271 } }, { { "[1]$a = NULL[1]$c = NULL", 128 }, { "[1]$a = NULL[1]$c = NULL[2]$a = NULL[2]$b = NULL", 196 }, { "[1]$a = 2[1]$c = 3[2]$a = 1[2]$b = 0", 196 } }, 0 }, { "if 1 == 1 then $a = 1; $b = 1; $c = 1; $d = 1; $e = 1; $f = 1; $g = 1; $h = 1; $i = 1; $j = 1; $k = 1; $l = 1; $m = 1; $n = 1; $o = 1; $p = 1; $q = 1; $r = 1; $s = 1; $t = 1; $u = 1; $v = 1; $w = 1; $x = 1; $y = 1; $z = 1; $aa = 1; $ab = 1; $ac = 1; $ad = 1; $ae = 1; $af = 1; $ag = 1; $ah = 1; $aj = 1; $aj = 1; $ak = 1; $al = 1; $am = 1; $an = 1; $ao = 1; $ap = 1; $aq = 1; $ar = 1; $as = 1; $at = 1; $au = 1; $av = 1; $aw = 1; $ax = 1; $ay = 1; $az = 1; $ba = 1; $bb = 1; $bc = 1; $bd = 1; $be = 1; $bf = 1; $bg = 1; $bh = 1; $bj = 1; $bj = 1; $bk = 1; $bl = 1; $bm = 1; $bn = 1; $bo = 1; $bp = 1; $bq = 1; $br = 1; $bs = 1; $bt = 1; $bu = 1; $bv = 1; $bw = 1; $bx = 1; $by = 1; $bz = 1; $ca = 1; $cb = 1; $cc = 1; $cd = 1; $ce = 1; $cf = 1; $cg = 1; $ch = 1; $cj = 1; $cj = 1; $ck = 1; $cl = 1; $cm = 1; $cn = 1; $co = 1; $cp = 1; $cq = 1; $cr = 1; $cs = 1; $ct = 1; $cu = 1; $cv = 1; $cw = 1; $cx = 1; $cy = 1; $cz = 1; $da = 1; $db = 1; $dc = 1; $dd = 1; $de = 1; $df = 1; $dg = 1; $dh = 1; $dj = 1; end", { { "[1]$de = 1[1]$df = 1[1]$dg = 1[1]$dh = 1[1]$dj = 1", 2706 } }, { { "[1]$de = 1[1]$df = 1[1]$dg = 1[1]$dh = 1[1]$dj = 1", 0 } }, 0 }, { "on sub2 then sub1(1); end on sub1($a) then print($a); end on sub3 then sub2(); end", { { "", 122 }, { "[2]$a = NULL", 189 }, { "[2]$a = 1", 234 } }, { { "", 128 },{ "[2]$a = NULL", 215 }, { "[2]$a = 1", 233 } }, 0 }, - { "on System#Boot then max(10, 20); end on timer=10 then $Z = 3.5; $T = max(3 + 0.5); max(10, 2); if $T == $Z then $Z = 2; end end", { { "", 116 }, { "[2]$Z = 2[2]$T = 3.5", 283 } }, { { "", 134 }, { "[2]$Z = 2[2]$T = 3.5", 358 } }, 0 }, + { "on System#Boot then max(10, 20); end on timer=10 then $Z = 3.5; $T = max(3 + 0.5); max(10, 2); if $T == $Z then $Z = 2; end end", { { "", 116 }, { "[2]$Z = 2[2]$T = 3.5", 291 } }, { { "", 134 }, { "[2]$Z = 2[2]$T = 3.5", 358 } }, 0 }, /* * Invalid rules diff --git a/src/rules/rules.cpp b/src/rules/rules.cpp index da4424d..4db0d9d 100755 --- a/src/rules/rules.cpp +++ b/src/rules/rules.cpp @@ -3363,6 +3363,14 @@ static int16_t rule_create(char **text, struct rules_t *obj) { case TEVENT: case TFUNCTION: case LPAREN: { + /* + * A call/expression statement is followed by a sibling + * statement (nested if, event, or function call) rather + * than closing the block. Reset mathcnt here too, for the + * same reason as the TEND case below (#894): otherwise it + * leaks into the next statement's slot allocation. + */ + mathcnt = 0; go = type; ret = TIF; continue; From 7898b10da71968620abf4044176ec5e5ab912c06 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Thu, 30 Jul 2026 06:19:35 +0000 Subject: [PATCH 6/7] Add regression test for mathcnt leak past a sibling if (#946) Covers the exact shape that was crashing: an if-block containing a call statement immediately followed by a sibling if whose condition uses a compound && operator. Verified this test FATALs on the parent commit (mathcnt reset missing) and passes with it applied. --- main.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/main.cpp b/main.cpp index 55b5d57..83e4530 100755 --- a/main.cpp +++ b/main.cpp @@ -583,6 +583,7 @@ bar", 127 } }, 0 }, // Newline { "if 1 == 1 then $a = 1; $b = 1; $c = 1; $d = 1; $e = 1; $f = 1; $g = 1; $h = 1; $i = 1; $j = 1; $k = 1; $l = 1; $m = 1; $n = 1; $o = 1; $p = 1; $q = 1; $r = 1; $s = 1; $t = 1; $u = 1; $v = 1; $w = 1; $x = 1; $y = 1; $z = 1; $aa = 1; $ab = 1; $ac = 1; $ad = 1; $ae = 1; $af = 1; $ag = 1; $ah = 1; $aj = 1; $aj = 1; $ak = 1; $al = 1; $am = 1; $an = 1; $ao = 1; $ap = 1; $aq = 1; $ar = 1; $as = 1; $at = 1; $au = 1; $av = 1; $aw = 1; $ax = 1; $ay = 1; $az = 1; $ba = 1; $bb = 1; $bc = 1; $bd = 1; $be = 1; $bf = 1; $bg = 1; $bh = 1; $bj = 1; $bj = 1; $bk = 1; $bl = 1; $bm = 1; $bn = 1; $bo = 1; $bp = 1; $bq = 1; $br = 1; $bs = 1; $bt = 1; $bu = 1; $bv = 1; $bw = 1; $bx = 1; $by = 1; $bz = 1; $ca = 1; $cb = 1; $cc = 1; $cd = 1; $ce = 1; $cf = 1; $cg = 1; $ch = 1; $cj = 1; $cj = 1; $ck = 1; $cl = 1; $cm = 1; $cn = 1; $co = 1; $cp = 1; $cq = 1; $cr = 1; $cs = 1; $ct = 1; $cu = 1; $cv = 1; $cw = 1; $cx = 1; $cy = 1; $cz = 1; $da = 1; $db = 1; $dc = 1; $dd = 1; $de = 1; $df = 1; $dg = 1; $dh = 1; $dj = 1; end", { { "[1]$de = 1[1]$df = 1[1]$dg = 1[1]$dh = 1[1]$dj = 1", 2706 } }, { { "[1]$de = 1[1]$df = 1[1]$dg = 1[1]$dh = 1[1]$dj = 1", 0 } }, 0 }, { "on sub2 then sub1(1); end on sub1($a) then print($a); end on sub3 then sub2(); end", { { "", 122 }, { "[2]$a = NULL", 189 }, { "[2]$a = 1", 234 } }, { { "", 128 },{ "[2]$a = NULL", 215 }, { "[2]$a = 1", 233 } }, 0 }, { "on System#Boot then max(10, 20); end on timer=10 then $Z = 3.5; $T = max(3 + 0.5); max(10, 2); if $T == $Z then $Z = 2; end end", { { "", 116 }, { "[2]$Z = 2[2]$T = 3.5", 291 } }, { { "", 134 }, { "[2]$Z = 2[2]$T = 3.5", 358 } }, 0 }, + { "on foo then $x = 1; end on bar then if $a == 1 then foo(); if $b >= 9 && $b < 18 then $c = 1; end end end", { { "[1]$x = 1", 111 }, { "[1]$x = 1[2]$c = 1", 292 } }, { { "[1]$x = 1", 0 }, { "[1]$x = 1", 0 } }, 0 }, /* * Invalid rules From b7257f08c93830670260a5c21fb3b48cc98163b8 Mon Sep 17 00:00:00 2001 From: IgorYbema Date: Fri, 31 Jul 2026 20:08:23 +0000 Subject: [PATCH 7/7] Add requested unittests and non-ESP varstack slots debug output Adds the two regression tests and extends the non-ESP printf debug lines with varstack slot usage, per CurlyMoo's review comment. Co-Authored-By: Claude Sonnet 5 --- main.cpp | 2 ++ src/rules/rules.cpp | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/main.cpp b/main.cpp index 83e4530..046ca67 100755 --- a/main.cpp +++ b/main.cpp @@ -584,6 +584,8 @@ bar", 127 } }, 0 }, // Newline { "on sub2 then sub1(1); end on sub1($a) then print($a); end on sub3 then sub2(); end", { { "", 122 }, { "[2]$a = NULL", 189 }, { "[2]$a = 1", 234 } }, { { "", 128 },{ "[2]$a = NULL", 215 }, { "[2]$a = 1", 233 } }, 0 }, { "on System#Boot then max(10, 20); end on timer=10 then $Z = 3.5; $T = max(3 + 0.5); max(10, 2); if $T == $Z then $Z = 2; end end", { { "", 116 }, { "[2]$Z = 2[2]$T = 3.5", 291 } }, { { "", 134 }, { "[2]$Z = 2[2]$T = 3.5", 358 } }, 0 }, { "on foo then $x = 1; end on bar then if $a == 1 then foo(); if $b >= 9 && $b < 18 then $c = 1; end end end", { { "[1]$x = 1", 111 }, { "[1]$x = 1[2]$c = 1", 292 } }, { { "[1]$x = 1", 0 }, { "[1]$x = 1", 0 } }, 0 }, + { "on foo then max(1, 2); end on timer=1 then if $a > 0 then foo(); end max(1, 2); end", { { "", 108 }, { "", 219 } }, { { "", 167 }, { "", 151 } }, 0 }, + { "if 1 == 1 then if 2 == 2 then foo(1); else foo(2); end end on foo($b) then $a = $b; end ", { { "", 156 }, { "[2]$b = NULL[2]$a = NULL", 194 } }, { { "", 167 }, { "[2]$b = NULL[2]$a = NULL", 151 } }, 0 }, /* * Invalid rules diff --git a/src/rules/rules.cpp b/src/rules/rules.cpp index 4db0d9d..9a4fc57 100755 --- a/src/rules/rules.cpp +++ b/src/rules/rules.cpp @@ -5561,7 +5561,7 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr ((double)timestamp.second.tv_sec + 1.0e-9*timestamp.second.tv_nsec) - ((double)timestamp.first.tv_sec + 1.0e-9*timestamp.first.tv_nsec)); - printf("bytecode: %d/%d, heap: %d/%d, stack: %d/%d bytes, varstack: %d/%d bytes\n", + printf("bytecode: %d/%d, heap: %d/%d, stack: %d/%d bytes, varstack: %d/%d bytes, varstack slots: %lu/127\n", getval(obj->bc.nrbytes), getval(obj->bc.bufsize), getval(obj->heap->nrbytes), @@ -5569,7 +5569,8 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr ((stack == NULL) ? 0 : getval(stack->nrbytes)), ((stack == NULL) ? 0 : getval(stack->bufsize)), ((varstack->nrbytes == 0) ? 0 : varstack->nrbytes), - (varstack->bufsize) + (varstack->bufsize), + varstack->nrbytes / sizeof(struct vm_vchar_t) ); #endif /*LCOV_EXCL_STOP*/ @@ -5638,7 +5639,7 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr ((double)timestamp.second.tv_sec + 1.0e-9*timestamp.second.tv_nsec) - ((double)timestamp.first.tv_sec + 1.0e-9*timestamp.first.tv_nsec)); - printf("bytecode: %d/%d, heap: %d/%d, stack: %d/%d bytes, varstack %d/%d bytes\n", + printf("bytecode: %d/%d, heap: %d/%d, stack: %d/%d bytes, varstack: %d/%d bytes, varstack slots: %lu/127\n", getval(obj->bc.nrbytes), getval(obj->bc.bufsize), getval(obj->heap->nrbytes), @@ -5646,7 +5647,8 @@ int8_t rule_initialize(struct pbuf *input, struct rules_t ***rules, uint8_t *nrr ((stack == NULL) ? 0 : getval(stack->nrbytes)), ((stack == NULL) ? 0 : getval(stack->bufsize)), ((varstack->nrbytes == 0) ? 0 : varstack->nrbytes), - (varstack->bufsize) + (varstack->bufsize), + varstack->nrbytes / sizeof(struct vm_vchar_t) ); #endif /*LCOV_EXCL_STOP*/