Skip to content

Commit fde0b72

Browse files
authored
feat: support any layout BoC (#56)
1 parent 8ff2bac commit fde0b72

File tree

8 files changed

+245
-77
lines changed

8 files changed

+245
-77
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ yarn add @tact-lang/opcode
1010

1111
## Usage
1212

13-
For most cases you will want to disassemble a BoC file generated by the Tact/FunC/Tolk compiler.
13+
For most cases you will want to disassemble a BoC file generated by the Tact/FunC/Tolk compiler. In this case decompiler will unpack the dictionary to procedures and methods.
1414

1515
```typescript
1616
import {AssemblyWriter, disassembleRoot} from "@tact-lang/opcode"
@@ -20,12 +20,12 @@ const program = disassembleRoot(source, {
2020
computeRefs: false,
2121
})
2222

23-
// Write the program AST into a TVM bytecode string
23+
// Write the program AST into a Fift assembly string
2424
const res = AssemblyWriter.write(program, {})
2525
console.log(res)
2626
```
2727

28-
If you want to decompile BoC file with non-standard root layout (for example, wallet v1), you can do the following:
28+
If you want to decompile BoC file without unpacking of the dictionary, you can do the following:
2929

3030
```typescript
3131
import {AssemblyWriter, disassembleRawRoot} from "@tact-lang/opcode"

src/decompiler/constants.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/decompiler/disasm.ts

Lines changed: 56 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ import {
1818
ProgramNode,
1919
} from "../ast/ast"
2020
import {createBlock, createInstruction} from "../ast/helpers"
21-
import {RootLayout} from "./constants"
2221
import {getDisplayNumber, hasHint} from "../spec/helpers"
23-
import {LayoutError, OperandError, UnknownOperandTypeError} from "./errors"
22+
import {OperandError, UnknownOperandTypeError} from "./errors"
2423

2524
export interface DisassembleParams {
2625
/**
@@ -338,45 +337,52 @@ function processRefOrSliceOperand(
338337
}
339338
}
340339

341-
/**
342-
* Checks if the root layout is valid.
343-
*
344-
* Valid layout is:
345-
* - `SETCP`
346-
* - `DICTPUSHCONST`
347-
* - `DICTIGETJMPZ`
348-
* - `THROWARG`
349-
*
350-
* This is the only layout that is supported by the decompiler.
351-
* This layout is generated by the FunC and Tact compilers.
352-
*/
353-
function checkLayout(opcodes: DecompiledInstruction[]): void {
354-
if (opcodes.length !== RootLayout.instructionsCount) {
355-
throw new LayoutError(RootLayout.instructionsCount, opcodes.length, {
356-
instructions: opcodes.map(op => op.op.definition.mnemonic),
340+
function findDictOpcode(opcodes: DecompiledInstruction[]): DecompiledInstruction | undefined {
341+
return opcodes.find(it => it.op.definition.mnemonic === "DICTPUSHCONST")
342+
}
343+
344+
function findRootMethods(opcodes: DecompiledInstruction[]): MethodNode[] {
345+
const methods: MethodNode[] = []
346+
347+
if (opcodes[2]?.op.definition.mnemonic === "PUSHCONT") {
348+
const cont = opcodes[2].op.operands.at(0)
349+
if (!cont || cont.type !== "subslice") {
350+
return methods
351+
}
352+
353+
const recvInternal = disassembleRawRoot(cont.value)
354+
methods.push({
355+
type: "method",
356+
hash: recvInternal.hash,
357+
offset: recvInternal.offset,
358+
body: recvInternal,
359+
id: 0,
357360
})
358361
}
359362

360-
const isValidLayout =
361-
opcodes[RootLayout.instructions.SETCP].op.definition.mnemonic === "SETCP" &&
362-
opcodes[RootLayout.instructions.DICTPUSHCONST].op.definition.mnemonic === "DICTPUSHCONST" &&
363-
(opcodes[RootLayout.instructions.DICTIGETJMPZ].op.definition.mnemonic === "DICTIGETJMPZ" ||
364-
opcodes[RootLayout.instructions.DICTIGETJMPZ].op.definition.mnemonic ===
365-
"DICTIGETJMP") &&
366-
opcodes[RootLayout.instructions.THROWARG].op.definition.mnemonic === "THROWARG"
367-
368-
if (!isValidLayout) {
369-
throw new LayoutError(RootLayout.instructionsCount, opcodes.length, {
370-
expected: ["SETCP", "DICTPUSHCONST", "DICTIGETJMPZ", "THROWARG"],
371-
actual: opcodes.map(op => op.op.definition.mnemonic),
363+
if (opcodes[6]?.op.definition.mnemonic === "PUSHCONT") {
364+
const cont = opcodes[6].op.operands.at(0)
365+
if (!cont || cont.type !== "subslice") {
366+
return methods
367+
}
368+
369+
const recvExternal = disassembleRawRoot(cont.value)
370+
methods.push({
371+
type: "method",
372+
hash: recvExternal.hash,
373+
offset: recvExternal.offset,
374+
body: recvExternal,
375+
id: -1,
372376
})
373377
}
378+
379+
return methods
374380
}
375381

376382
/**
377383
* Disassembles the root cell into a list of instructions.
378384
*
379-
* Use this function if you want to disassemble the whole BoC file.
385+
* Use this function if you want to disassemble the whole BoC file with dictionary unpacked.
380386
*/
381387
export function disassembleRoot(
382388
cell: Cell,
@@ -388,29 +394,40 @@ export function disassembleRoot(
388394
},
389395
): ProgramNode {
390396
const opcodes = disassemble({source: cell})
391-
checkLayout(opcodes)
392-
393-
const dictOpcode = opcodes[RootLayout.instructions.DICTPUSHCONST].op
394-
const {procedures, methods} = deserializeDict(dictOpcode.operands, options.computeRefs)
395397

396398
const args = {
397399
source: cell,
398400
offset: {bits: 0, refs: 9},
399401
onCellReference: undefined,
400402
}
403+
404+
const rootMethods = findRootMethods(opcodes)
405+
406+
const dictOpcode = findDictOpcode(opcodes)
407+
if (!dictOpcode) {
408+
// Likely some non-Tact/FunC produced BoC
409+
return {
410+
type: "program",
411+
topLevelInstructions: opcodes.map(op => processInstruction(op, args)),
412+
procedures: [],
413+
methods: rootMethods,
414+
withRefs: options.computeRefs,
415+
}
416+
}
417+
418+
const {procedures, methods} = deserializeDict(dictOpcode.op.operands, options.computeRefs)
419+
401420
return {
402421
type: "program",
403422
topLevelInstructions: opcodes.map(op => processInstruction(op, args)),
404423
procedures,
405-
methods,
424+
methods: [...rootMethods, ...methods],
406425
withRefs: options.computeRefs,
407426
}
408427
}
409428

410429
/**
411-
* Disassembles a cell without any additional checks for the layout.
412-
*
413-
* Use this function if your contract use non-usual layout.
430+
* Disassembles a cell without any additional unpacking of the dictionary.
414431
*/
415432
export function disassembleRawRoot(cell: Cell): BlockNode {
416433
return disassembleAndProcess({

src/decompiler/errors.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,6 @@ export class OperandError extends DisassemblerError {
3131
}
3232
}
3333

34-
export class LayoutError extends DisassemblerError {
35-
public constructor(expected: number, actual: number, details?: Record<string, unknown>) {
36-
super(`Unexpected root layout: expected ${expected} instructions, got ${actual}`, {
37-
expected,
38-
actual,
39-
...details,
40-
})
41-
this.name = "LayoutError"
42-
}
43-
}
44-
4534
export class UnknownOperandTypeError extends DisassemblerError {
4635
public constructor(operand: OperandValue, details?: Record<string, unknown>) {
4736
super(`Unknown operand type: ${operand.type}`, {

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export type {AssemblyWriterOptions} from "./printer/assembly-writer"
1212
export {AssemblyWriter} from "./printer/assembly-writer"
1313

1414
export {debugSymbols} from "./utils/known-methods"
15-
export {Cell} from "@ton/core"
15+
export {Cell, Dictionary} from "@ton/core"

src/printer/assembly-writer.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,21 @@ export class AssemblyWriter {
132132
}
133133
}
134134

135-
// if (node.topLevelInstructions.length > 0) {
136-
// node.topLevelInstructions.forEach(instruction => {
137-
// // if (i === 1) return
138-
// this.writer.write("// ")
139-
// this.writeInstructionNode(instruction)
140-
// })
141-
// }
142-
143135
this.writer.writeLine(`"Asm.fif" include`)
136+
137+
if (node.procedures.length === 0 && node.methods.length === 0) {
138+
this.writer.writeLine("<{")
139+
140+
this.writer.indent(() => {
141+
node.topLevelInstructions.forEach(instruction => {
142+
this.writeInstructionNode(instruction)
143+
})
144+
})
145+
146+
this.writer.write("}>c")
147+
return
148+
}
149+
144150
this.writer.writeLine("PROGRAM{")
145151
this.writer.indent(() => {
146152
const methods = [...node.methods].sort((a, b) => a.id - b.id)

src/test/e2e/__snapshots__/known-contracts.spec.ts.snap

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,153 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`known contracts > should decompile Tact 1.6.0 with other layout 1`] = `
4+
""Asm.fif" include
5+
PROGRAM{
6+
DECLPROC recv_internal
7+
78250 DECLMETHOD ?fun_78250
8+
DECLPROC ?fun_ref_92183b49329bb4e4
9+
recv_internal PROC:<{
10+
DROP
11+
DROP
12+
CTOS
13+
TWO
14+
SDSKIPFIRST
15+
1 LDI
16+
1 LDI
17+
LDMSGADDR
18+
OVER
19+
s3 s4 XCHG
20+
s5 s5 XCHG2
21+
4 TUPLE
22+
1 SETGLOB
23+
SWAP
24+
2 SETGLOB
25+
PUSHROOT
26+
CTOS
27+
1 LDI
28+
DROP
29+
<{
30+
NULL
31+
}> PUSHCONT
32+
<{
33+
NULL
34+
}> PUSHCONT
35+
IFELSE
36+
DROP
37+
IFRET
38+
130 THROW
39+
}>
40+
?fun_78250 PROC:<{
41+
PUSHROOT
42+
CTOS
43+
1 LDI
44+
DROP
45+
<{
46+
NULL
47+
}> PUSHCONT
48+
<{
49+
NULL
50+
}> PUSHCONT
51+
IFELSE
52+
?fun_ref_92183b49329bb4e4 INLINECALLDICT
53+
NIP
54+
}>
55+
?fun_ref_92183b49329bb4e4 PROCREF:<{
56+
x{68656C6C6F20776F726C64} PUSHSLICE
57+
}>
58+
}END>c"
59+
`;
60+
61+
exports[`known contracts > should decompile Tact 1.6.0 with other layout and recv_external 1`] = `
62+
""Asm.fif" include
63+
PROGRAM{
64+
-1 DECLMETHOD recv_external
65+
DECLPROC recv_internal
66+
78250 DECLMETHOD ?fun_78250
67+
DECLPROC ?fun_ref_92183b49329bb4e4
68+
recv_external PROC:<{
69+
DROP
70+
DROP
71+
PUSHROOT
72+
CTOS
73+
1 LDI
74+
DROP
75+
<{
76+
NULL
77+
}> PUSHCONT
78+
<{
79+
NULL
80+
}> PUSHCONT
81+
IFELSE
82+
1 GETGLOB
83+
4 UNTUPLE
84+
s2 s3 XCHG
85+
3 BLKDROP
86+
41351 PUSHINT
87+
MYADDR
88+
ROT
89+
SDEQ
90+
THROWANYIFNOT
91+
DROP
92+
NEWC
93+
-1 PUSHINT
94+
SWAP
95+
1 STI
96+
ENDC
97+
POPROOT
98+
}>
99+
recv_internal PROC:<{
100+
DROP
101+
DROP
102+
CTOS
103+
TWO
104+
SDSKIPFIRST
105+
1 LDI
106+
1 LDI
107+
LDMSGADDR
108+
OVER
109+
s3 s4 XCHG
110+
s5 s5 XCHG2
111+
4 TUPLE
112+
1 SETGLOB
113+
SWAP
114+
2 SETGLOB
115+
PUSHROOT
116+
CTOS
117+
1 LDI
118+
DROP
119+
<{
120+
NULL
121+
}> PUSHCONT
122+
<{
123+
NULL
124+
}> PUSHCONT
125+
IFELSE
126+
DROP
127+
IFRET
128+
130 THROW
129+
}>
130+
?fun_78250 PROC:<{
131+
PUSHROOT
132+
CTOS
133+
1 LDI
134+
DROP
135+
<{
136+
NULL
137+
}> PUSHCONT
138+
<{
139+
NULL
140+
}> PUSHCONT
141+
IFELSE
142+
?fun_ref_92183b49329bb4e4 INLINECALLDICT
143+
NIP
144+
}>
145+
?fun_ref_92183b49329bb4e4 PROCREF:<{
146+
x{68656C6C6F20776F726C64} PUSHSLICE
147+
}>
148+
}END>c"
149+
`;
150+
3151
exports[`known contracts > should decompile echo 1`] = `
4152
""Asm.fif" include
5153
PROGRAM{

0 commit comments

Comments
 (0)