Skip to content

Commit 7d11a8a

Browse files
fix(cashc): lower integer * and / to OP_MUL/OP_DIV, not OP_2MUL/OP_2DIV
The numeric branch of compileBinaryOp() mapped the binary `*` and `/` operators to the UNARY OP_2MUL / OP_2DIV (multiply / divide by 2). Since visitBinaryOp pushes both operands and models the op as binary (pop 2, push 1), `a * b` compiled to `a b OP_2MUL` — which doubles `b`, leaves `a` dangling, and silently computes the wrong value. OP_2MUL/OP_2DIV are enabled in Radiant consensus, so this produced valid-but-wrong scripts rather than failing loudly. It breaks any contract doing real arithmetic (e.g. a constant-product AMM); no shipped example multiplied variables, so it went unnoticed. Map MUL->OP_MUL and DIV->OP_DIV (both re-enabled in Radiant). Add a generation regression test and correct the SplitSize fixture, whose baked ASM (and comment) had encoded the buggy OP_2DIV for `b.length / 2`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 23e63a9 commit 7d11a8a

3 files changed

Lines changed: 53 additions & 5 deletions

File tree

packages/cashc/src/generation/utils.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,12 @@ export function compileBinaryOp(op: BinaryOperator, numeric: boolean = false): S
101101

102102
if (numeric) {
103103
mapping[BinaryOperator.PLUS] = [Op.OP_ADD];
104-
mapping[BinaryOperator.MUL] = [Op.OP_2MUL];
105-
mapping[BinaryOperator.DIV] = [Op.OP_2DIV];
104+
// OP_MUL / OP_DIV are full binary multiply/divide (re-enabled in Radiant
105+
// consensus). OP_2MUL / OP_2DIV are UNARY multiply/divide-by-2 and must NOT
106+
// be used for the binary `*` / `/` operators — doing so silently computes
107+
// `x * 2` / `x / 2` and (being unary) corrupts the compiler's stack model.
108+
mapping[BinaryOperator.MUL] = [Op.OP_MUL];
109+
mapping[BinaryOperator.DIV] = [Op.OP_DIV];
106110
mapping[BinaryOperator.EQ] = [Op.OP_NUMEQUAL];
107111
mapping[BinaryOperator.NE] = [Op.OP_NUMNOTEQUAL];
108112
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* arithmetic.test.ts
2+
*
3+
* Regression test: the binary `*` and `/` operators on integers must lower to
4+
* the full binary OP_MUL / OP_DIV opcodes, NOT the unary OP_2MUL / OP_2DIV
5+
* (multiply / divide by 2). See generation/utils.ts compileBinaryOp().
6+
*/
7+
8+
import { compileString } from '../../src/index.js';
9+
10+
const contract = `
11+
pragma radiantscript ^1.1.0;
12+
contract Arith() {
13+
return {
14+
f(int a, int b) {
15+
require(a * b == b * a);
16+
require(a / b >= 0);
17+
require(a % b >= 0);
18+
}
19+
};
20+
}
21+
`;
22+
23+
describe('Integer arithmetic lowering', () => {
24+
const artifact = compileString(contract);
25+
const asm: string = (artifact as { asm: string }).asm;
26+
27+
it('lowers `*` to OP_MUL', () => {
28+
expect(asm).toContain('OP_MUL');
29+
});
30+
31+
it('lowers `/` to OP_DIV', () => {
32+
expect(asm).toContain('OP_DIV');
33+
});
34+
35+
it('lowers `%` to OP_MOD', () => {
36+
expect(asm).toContain('OP_MOD');
37+
});
38+
39+
it('never emits the unary OP_2MUL / OP_2DIV for binary operators', () => {
40+
expect(asm).not.toContain('OP_2MUL');
41+
expect(asm).not.toContain('OP_2DIV');
42+
});
43+
});

packages/cashc/test/generation/fixtures.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,10 @@ export const fixtures: Fixture[] = [
153153
asm:
154154
'$_b '
155155
// bytes x = b.split(b.length / 2)[1]
156-
// Post V2-fork: the compiler emits OP_2DIV instead of OP_DIV-by-2 as
157-
// an optimisation, matching the new opcode added in commit ac69ad2.
158-
+ 'OP_DUP OP_DUP OP_SIZE OP_NIP OP_2 OP_2DIV OP_SPLIT OP_NIP '
156+
// `b.length / 2` is binary division and must lower to OP_DIV. (The unary
157+
// OP_2DIV would halve the pushed literal 2, not the length — see the
158+
// OP_MUL/OP_DIV fix in generation/utils.ts compileBinaryOp.)
159+
+ 'OP_DUP OP_DUP OP_SIZE OP_NIP OP_2 OP_DIV OP_SPLIT OP_NIP '
159160
// require(x != b)
160161
+ 'OP_2DUP OP_EQUAL OP_NOT OP_VERIFY '
161162
// bytes x = b.split(b.length / 2)[1]

0 commit comments

Comments
 (0)