From 92da853f2e03e7046e7e3712a6fad4d93d50144f Mon Sep 17 00:00:00 2001 From: Ananim353 Date: Sat, 25 Jul 2026 19:21:05 +0300 Subject: [PATCH] fix(vm): tag binary-op result with the wider operand type The arithmetic and bitwise handlers tagged the result slot's gmlStackType with instrType2(instr). A numeric binary op's result should take the WIDER of the two operand types; when type1 is wider than type2 (e.g. add.v.i, Variable + int) the result was mis-tagged as the narrow type (INT32, 4B) instead of Variable (16B). gmlStackType drives the byte-count model in bytesToSlotCount (used by the Dup swap variant and BC17 BREAK sub-opcodes), so the mismatch makes the byte walk overshoot a slot boundary and abort at require(remaining == 0). Reproduces in DELTARUNE Chapter 5, Susie's outfit ("thrashfit") menu: option.init(id, label, names, flag, 170 + ja_offset) compiles the last argument to add.v.i with no following Conv, feeding a .v Dup-swap arg list; the mis-tagged INT32 slot makes the 5 args sum to 68 bytes instead of 80. Introduce binaryResultType() and use it at every binary-op result-tag site. It differs from instrType2 only when type1 is strictly wider than type2 (the buggy case), so it cannot regress code that currently works. gmlStackType is read only by bytesToSlotCount, so widening the tag never changes a value's runtime interpretation. --- src/vm.c | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/vm.c b/src/vm.c index 7b5895fe7..39b4fc76f 100644 --- a/src/vm.c +++ b/src/vm.c @@ -124,6 +124,20 @@ static uint8_t instrType2(uint32_t instr) { return (instr >> 20) & 0xF; } +// The result of a numeric binary op (add/sub/mul/div/mod/rem and bitwise) is declared on the +// stack with the WIDER of the two operand types. gmlStackType feeds the byte-count model in +// bytesToSlotCount (used by the Dup swap variant and BC17 BREAK sub-opcodes), so it must match +// the width the compiler assigned. Tagging with instrType2 alone under-sizes the result when +// type1 is wider than type2 (e.g. add.v.i: Variable + int -> the result is a Variable-width +// 16B slot, not an int 4B). When such a result feeds a `.v` Dup-swap over a method-call arg +// list with no intervening Conv, the under-sized tag makes bytesToSlotCount overshoot a slot +// boundary and abort at `require(remaining == 0)`. +static uint8_t binaryResultType(uint32_t instr) { + uint8_t t1 = instrType1(instr); + uint8_t t2 = instrType2(instr); + return (gmlTypeNativeSize(t1) > gmlTypeNativeSize(t2)) ? t1 : t2; +} + static int16_t instrInstanceType(uint32_t instr) { return (int16_t) (instr & 0xFFFF); } @@ -1547,7 +1561,7 @@ static void handleDiv(VMContext* ctx, uint32_t instr) { GMLReal result = RValue_toReal(a) / divisor; RValue_free(&a); RValue_free(&b); - stackPushTyped(ctx, RValue_makeReal(result), instrType2(instr)); + stackPushTyped(ctx, RValue_makeReal(result), binaryResultType(instr)); } static void handleRem(VMContext* ctx, uint32_t instr) { @@ -1558,7 +1572,7 @@ static void handleRem(VMContext* ctx, uint32_t instr) { int64_t result = RValue_toInt64(a) / divisor; RValue_free(&a); RValue_free(&b); - stackPushTyped(ctx, RValue_makeInt64(result), instrType2(instr)); + stackPushTyped(ctx, RValue_makeInt64(result), binaryResultType(instr)); } static void handleMod(VMContext* ctx, uint32_t instr) { @@ -1569,14 +1583,14 @@ static void handleMod(VMContext* ctx, uint32_t instr) { GMLReal result = GMLReal_fmod(RValue_toReal(a), divisor); RValue_free(&a); RValue_free(&b); - stackPushTyped(ctx, RValue_makeReal(result), instrType2(instr)); + stackPushTyped(ctx, RValue_makeReal(result), binaryResultType(instr)); } #define SIMPLE_BYTECODE_BITWISE_OPERATION(op) \ int32_t b = stackPopInt32(ctx); \ int32_t a = stackPopInt32(ctx); \ int32_t result = a op b; \ - stackPushTyped(ctx, RValue_makeInt32(result), instrType2(instr)) + stackPushTyped(ctx, RValue_makeInt32(result), binaryResultType(instr)) static void handleAnd(VMContext* ctx, uint32_t instr) { SIMPLE_BYTECODE_BITWISE_OPERATION(&); @@ -3037,10 +3051,10 @@ static RValue executeLoop(VMContext* ctx) { slotA->real = aVal + bVal; slotA->type = RVALUE_REAL; } - slotA->gmlStackType = instrType2(instr); + slotA->gmlStackType = binaryResultType(instr); ctx->stack.top--; } else { - uint8_t resultType = instrType2(instr); + uint8_t resultType = binaryResultType(instr); RValue b = stackPop(ctx); RValue a = stackPop(ctx); if (a.type == RVALUE_STRING || b.type == RVALUE_STRING) { @@ -3074,10 +3088,10 @@ static RValue executeLoop(VMContext* ctx) { slotA->real = aVal - bVal; slotA->type = RVALUE_REAL; } - slotA->gmlStackType = instrType2(instr); + slotA->gmlStackType = binaryResultType(instr); ctx->stack.top--; } else { - uint8_t resultType = instrType2(instr); + uint8_t resultType = binaryResultType(instr); RValue b = stackPop(ctx); RValue a = stackPop(ctx); #ifndef NO_RVALUE_INT64 @@ -3107,10 +3121,10 @@ static RValue executeLoop(VMContext* ctx) { slotA->real = aVal * bVal; slotA->type = RVALUE_REAL; } - slotA->gmlStackType = instrType2(instr); + slotA->gmlStackType = binaryResultType(instr); ctx->stack.top--; } else { - uint8_t resultType = instrType2(instr); + uint8_t resultType = binaryResultType(instr); RValue b = stackPop(ctx); RValue a = stackPop(ctx); if (a.type == RVALUE_STRING) {