From 041a44756fe4cad3bf7efbabeff77608da5d4ddd Mon Sep 17 00:00:00 2001 From: Rosco Kalis Date: Tue, 29 Sep 2026 11:13:37 +0200 Subject: [PATCH] fix: fix slice() variable final-use handling --- .../src/semantic/SymbolTableTraversal.ts | 10 ++++++ .../slice_variable_reuse.ts | 32 +++++++++++++++++++ .../slice_variable_reuse.cash | 8 +++++ 3 files changed, 50 insertions(+) create mode 100644 packages/cashc/test/generation/fixtures/valid-contract-files/slice_variable_reuse.ts create mode 100644 packages/cashc/test/valid-contract-files/slice_variable_reuse.cash diff --git a/packages/cashc/src/semantic/SymbolTableTraversal.ts b/packages/cashc/src/semantic/SymbolTableTraversal.ts index b95bf2b07..f52edb7ec 100644 --- a/packages/cashc/src/semantic/SymbolTableTraversal.ts +++ b/packages/cashc/src/semantic/SymbolTableTraversal.ts @@ -18,6 +18,7 @@ import { ConsoleStatementNode, ConsoleParameterNode, ForNode, + SliceNode, TupleAssignmentTarget, } from '../ast/AST.js'; import AstTraversal from '../ast/AstTraversal.js'; @@ -202,6 +203,15 @@ export default class SymbolTableTraversal extends AstTraversal { return node; } + // Code generation evaluates the end before the start (element.split(end)[0].split(start)[1]), so the final use + // of a variable (where it is rolled instead of picked) has to be tracked in that same order + visitSlice(node: SliceNode): Node { + node.element = this.visit(node.element); + node.end = this.visit(node.end); + node.start = this.visit(node.start); + return node; + } + // When we enter a console statement, visitConsoleStatement(node: ConsoleStatementNode): Node { this.insideConsoleStatement = true; diff --git a/packages/cashc/test/generation/fixtures/valid-contract-files/slice_variable_reuse.ts b/packages/cashc/test/generation/fixtures/valid-contract-files/slice_variable_reuse.ts new file mode 100644 index 000000000..0b331806e --- /dev/null +++ b/packages/cashc/test/generation/fixtures/valid-contract-files/slice_variable_reuse.ts @@ -0,0 +1,32 @@ +import { Fixture } from '../../fixture-utils.js'; + +// .slice(start, end) evaluates end before start, so a variable used in both arguments is picked for end and +// rolled for start (instead of being rolled for end and then missing, or read from a stale slot, for start) +export const fixtures: Fixture[] = [ + { + artifact: { + contractName: 'SliceVariableReuse', + constructorInputs: [], + abi: [{ name: 'spend', inputs: [{ name: 'data', type: 'bytes' }, { name: 'index', type: 'int' }] }], + bytecode: + // int next = index + 1; + 'OP_OVER OP_1ADD ' + // next = next + 1; + + 'OP_DUP OP_1ADD ' + // require(data.slice(next, next + 1) == 0x33); + + 'OP_3DUP OP_NIP OP_1ADD OP_SPLIT OP_DROP OP_SWAP OP_SPLIT OP_NIP 33 OP_EQUALVERIFY ' + // require(data.slice(index, index + 1) == 0x11); + + 'OP_SWAP OP_2 OP_PICK OP_1ADD OP_SPLIT OP_DROP OP_ROT OP_SPLIT OP_NIP 11 OP_EQUAL ' + // Cleanup + + 'OP_NIP', + fingerprint: 'bee1541625c082916f1c0bed69773886d459872311414c0e46877ea1d37cdf43', + debug: { + bytecode: '788b768b6f778b7f757c7f770133887c52798b7f757b7f7701118777', + sourceMap: '3:19:3:24;:::28:1;4:15:4:19:0;:::23:1;5:16:5:37:0;;:33::41:1;:16::42;;:27::31:0;:16::42:1;;:46::50:0;:8::52:1;6:16:6:20:0;:34::39;;:::43:1;:16::44;;:27::32:0;:16::44:1;;:48::52:0;:8::54:1;2:42:7:5', + logs: [], + requires: [{ ip: 13, line: 5 }, { ip: 25, line: 6 }], + sourceTags: '25:25:sc', + }, + }, + }, +]; diff --git a/packages/cashc/test/valid-contract-files/slice_variable_reuse.cash b/packages/cashc/test/valid-contract-files/slice_variable_reuse.cash new file mode 100644 index 000000000..b6153de92 --- /dev/null +++ b/packages/cashc/test/valid-contract-files/slice_variable_reuse.cash @@ -0,0 +1,8 @@ +contract SliceVariableReuse() { + function spend(bytes data, int index) { + int next = index + 1; + next = next + 1; + require(data.slice(next, next + 1) == 0x33); + require(data.slice(index, index + 1) == 0x11); + } +}