diff --git a/CHANGELOG.md b/CHANGELOG.md index 2965aa27b..96b27a14a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reserve mode constants in `@stdlib/reserve`, namely `ReserveExact`, `ReserveAllExcept`, `ReserveAtMost`, `ReserveAddOriginalBalance`, `ReserveInvertSign`, `ReserveBounceIfActionFail`: PR [#173](https://github.com/tact-lang/tact/pull/173) - JSON Schema for `tact.config.json`: PR [#194](https://github.com/tact-lang/tact/pull/194) - Display an error for integer overflow at compile-time: PR [#200](https://github.com/tact-lang/tact/pull/200) +- Try-Catch statements: PR [#212](https://github.com/tact-lang/tact/pull/212) - Non-modifying `StringBuilder`'s `concat` method for chained string concatenations: PR [#217](https://github.com/tact-lang/tact/pull/217) - `toString` extension function for `Address` type: PR [#224](https://github.com/tact-lang/tact/pull/224) - Bitwise XOR operation (`^`): PR [#238](https://github.com/tact-lang/tact/pull/238) diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index 1e886efbc..0e1a89cd5 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -169,6 +169,30 @@ export function writeStatement( }); ctx.append(`}`); return; + } else if (f.kind === "statement_try") { + ctx.append(`try {`); + ctx.inIndent(() => { + for (const s of f.statements) { + writeStatement(s, self, returns, ctx); + } + }); + ctx.append("} catch (_) { }"); + return; + } else if (f.kind === "statement_try_catch") { + ctx.append(`try {`); + ctx.inIndent(() => { + for (const s of f.statements) { + writeStatement(s, self, returns, ctx); + } + }); + ctx.append(`} catch (_, ${id(f.catchName)}) {`); + ctx.inIndent(() => { + for (const s of f.catchStatements) { + writeStatement(s, self, returns, ctx); + } + }); + ctx.append(`}`); + return; } throw Error("Unknown statement kind"); diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index bb68bef0d..73e3a6411 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -474,6 +474,22 @@ export type ASTStatementRepeat = { ref: ASTRef; }; +export type ASTStatementTry = { + kind: "statement_try"; + id: number; + statements: ASTStatement[]; + ref: ASTRef; +}; + +export type ASTStatementTryCatch = { + kind: "statement_try_catch"; + id: number; + statements: ASTStatement[]; + catchName: string; + catchStatements: ASTStatement[]; + ref: ASTRef; +}; + // // Unions // @@ -487,7 +503,9 @@ export type ASTStatement = | ASTCondition | ASTStatementWhile | ASTStatementUntil - | ASTStatementRepeat; + | ASTStatementRepeat + | ASTStatementTry + | ASTStatementTryCatch; export type ASTNode = | ASTExpression | ASTStruct @@ -514,6 +532,8 @@ export type ASTNode = | ASTStatementWhile | ASTStatementUntil | ASTStatementRepeat + | ASTStatementTry + | ASTStatementTryCatch | ASTReceive | ASTLvalueRef | ASTString @@ -728,6 +748,19 @@ export function traverse(node: ASTNode, callback: (node: ASTNode) => void) { traverse(e, callback); } } + if (node.kind === "statement_try") { + for (const e of node.statements) { + traverse(e, callback); + } + } + if (node.kind === "statement_try_catch") { + for (const e of node.statements) { + traverse(e, callback); + } + for (const e of node.catchStatements) { + traverse(e, callback); + } + } if (node.kind === "op_binary") { traverse(node.left, callback); traverse(node.right, callback); diff --git a/src/grammar/clone.ts b/src/grammar/clone.ts index 15d0d1415..254eb777d 100644 --- a/src/grammar/clone.ts +++ b/src/grammar/clone.ts @@ -113,6 +113,17 @@ export function cloneNode(src: T): T { condition: cloneNode(src.condition), statements: src.statements.map(cloneNode), }); + } else if (src.kind === "statement_try") { + return cloneASTNode({ + ...src, + statements: src.statements.map(cloneNode), + }); + } else if (src.kind === "statement_try_catch") { + return cloneASTNode({ + ...src, + statements: src.statements.map(cloneNode), + catchStatements: src.catchStatements.map(cloneNode), + }); } else if (src.kind === "def_function") { return cloneASTNode({ ...src, diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index a15799bde..6dd84bcc9 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -97,6 +97,7 @@ Tact { | StatementWhile | StatementRepeat | StatementUntil + | StatementTry StatementBlock = "{" Statement* "}" StatementLet = let id ":" Type "=" Expression ";" StatementReturn = return Expression ";" --withExpression @@ -119,6 +120,8 @@ Tact { StatementWhile = while "(" Expression ")" "{" Statement* "}" StatementRepeat = repeat "(" Expression ")" "{" Statement* "}" StatementUntil = do "{" Statement* "}" until "(" Expression ")" ";" + StatementTry = try "{" Statement* "}" ~catch --simple + | try "{" Statement* "}" catch "(" id ")" "{" Statement* "}" --withCatch // L-value LValue = id "." LValue --more @@ -262,6 +265,8 @@ Tact { | repeat | do | until + | try + | catch | as | mutates | extends @@ -288,6 +293,8 @@ Tact { repeat = "repeat" ~idPart do = "do" ~idPart until = "until" ~idPart + try = "try" ~idPart + catch = "catch" ~idPart as = "as" ~idPart mutates = "mutates" ~idPart extends = "extends" ~idPart diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index bf73472df..83c36fa3c 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -823,6 +823,34 @@ semantics.addOperation("resolve_statement", { ref: createRef(this), }); }, + StatementTry_simple(_arg0, _arg1, arg2, _arg3) { + return createNode({ + kind: "statement_try", + statements: arg2.children.map((v) => v.resolve_statement()), + ref: createRef(this), + }); + }, + StatementTry_withCatch( + _arg0, + _arg1, + arg2, + _arg3, + _arg4, + _arg5, + arg6, + _arg7, + _arg8, + arg9, + _arg10, + ) { + return createNode({ + kind: "statement_try_catch", + statements: arg2.children.map((v) => v.resolve_statement()), + catchName: arg6.sourceString, + catchStatements: arg9.children.map((v) => v.resolve_statement()), + ref: createRef(this), + }); + }, }); // LValue diff --git a/src/test/__snapshots__/feature-try-catch.spec.ts.snap b/src/test/__snapshots__/feature-try-catch.spec.ts.snap new file mode 100644 index 000000000..5f4ee19f5 --- /dev/null +++ b/src/test/__snapshots__/feature-try-catch.spec.ts.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`feature-ternary should implement try-catch statements correctly 1`] = ` +[ + { + "$seq": 1, + "events": [ + { + "$type": "storage-charged", + "amount": "0.000000007", + }, + { + "$type": "received", + "message": { + "body": { + "text": "increment", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQAZccETl-kyR0WEGf-7CT-k7zXmJGLmD8_jF9zsGay7wpHn", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 3929n, + }, + ], + }, + { + "$seq": 2, + "events": [ + { + "$type": "storage-charged", + "amount": "0.000000007", + }, + { + "$type": "received", + "message": { + "body": { + "text": "incrementTryCatch", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQAZccETl-kyR0WEGf-7CT-k7zXmJGLmD8_jF9zsGay7wpHn", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 4507n, + }, + ], + }, + { + "$seq": 3, + "events": [ + { + "$type": "storage-charged", + "amount": "0.000000007", + }, + { + "$type": "received", + "message": { + "body": { + "text": "tryCatchRegisters", + "type": "text", + }, + "bounce": true, + "from": "@treasure(treasure)", + "to": "kQAZccETl-kyR0WEGf-7CT-k7zXmJGLmD8_jF9zsGay7wpHn", + "type": "internal", + "value": "10", + }, + }, + { + "$type": "processed", + "gasUsed": 12023n, + }, + { + "$type": "sent", + "messages": [ + { + "body": { + "text": "hello world 1", + "type": "text", + }, + "bounce": true, + "from": "kQAZccETl-kyR0WEGf-7CT-k7zXmJGLmD8_jF9zsGay7wpHn", + "to": "@treasure(treasure)", + "type": "internal", + "value": "9.986741", + }, + ], + }, + ], + }, +] +`; diff --git a/src/test/feature-try-catch.spec.ts b/src/test/feature-try-catch.spec.ts new file mode 100644 index 000000000..937ecea35 --- /dev/null +++ b/src/test/feature-try-catch.spec.ts @@ -0,0 +1,47 @@ +import { toNano } from "@ton/core"; +import { ContractSystem } from "@tact-lang/emulator"; +import { __DANGER_resetNodeId } from "../grammar/ast"; +import { TryCatchTester } from "./features/output/try-catch_TryCatchTester"; + +describe("feature-ternary", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + it("should implement try-catch statements correctly", async () => { + // Init + const system = await ContractSystem.create(); + const treasure = system.treasure("treasure"); + const contract = system.open(await TryCatchTester.fromInit()); + await contract.send(treasure, { value: toNano("10") }, null); + await system.run(); + + // Check methods + expect(await contract.getTestTryCatch1()).toEqual(7n); + expect(await contract.getTestTryCatch2()).toEqual(101n); + expect(await contract.getTestTryCatch3()).toEqual(4n); + + // Check state rollbacks + const tracker = system.track(contract); + + expect(await contract.getGetCounter()).toEqual(0n); + await contract.send(treasure, { value: toNano("10") }, "increment"); + await system.run(); + expect(await contract.getGetCounter()).toEqual(1n); + await contract.send( + treasure, + { value: toNano("10") }, + "incrementTryCatch", + ); + await system.run(); + expect(await contract.getGetCounter()).toEqual(1n); + await contract.send( + treasure, + { value: toNano("10") }, + "tryCatchRegisters", + ); + await system.run(); + expect(await contract.getGetCounter()).toEqual(2n); + + expect(tracker.collect()).toMatchSnapshot(); + }); +}); diff --git a/src/test/features/try-catch.tact b/src/test/features/try-catch.tact new file mode 100644 index 000000000..41136b839 --- /dev/null +++ b/src/test/features/try-catch.tact @@ -0,0 +1,65 @@ +contract TryCatchTester { + counter: Int = 0; + + receive() {} + + receive("increment") { + self.counter += 1; + } + + receive("incrementTryCatch") { + try { + self.counter += 1; + nativeThrow(123); + } + } + + receive("tryCatchRegisters") { + self.counter += 1; + self.reply("hello world 1".asComment()); + try { + self.counter += 1; + self.reply("hello world 2".asComment()); + emit("Something".asComment()); + nativeThrow(123); + } + } + + get fun getCounter(): Int { + return self.counter; + } + + get fun testTryCatch1(): Int { + try { + throw(101); + return 42; + } + return 7; + } + + get fun testTryCatch2(): Int { + try { + throw(101); + return 42; + } catch (e) { + return e; + } + return 0; + } + + get fun testTryCatch3(): Int { + try { + let xs: Slice = beginCell().storeUint(0, 1).endCell().beginParse(); + let x: Int = xs.loadUint(1); + try { + throw(101); + } catch (e) { + return e / x; // division by zero, exit code = 4 + } + let e: Int = 123; + return e; + } catch (e) { + return e; + } + } +} diff --git a/src/types/__snapshots__/resolveStatements.spec.ts.snap b/src/types/__snapshots__/resolveStatements.spec.ts.snap index 31b3bb29c..5095414ca 100644 --- a/src/types/__snapshots__/resolveStatements.spec.ts.snap +++ b/src/types/__snapshots__/resolveStatements.spec.ts.snap @@ -320,6 +320,46 @@ Line 9, col 12: " `; +exports[`resolveStatements should fail statements for case-32 1`] = ` +":9:12: Unable to resolve id e +Line 9, col 12: + 8 | } +> 9 | return e; + ^ + 10 | } +" +`; + +exports[`resolveStatements should fail statements for case-33 1`] = ` +":5:16: Unable to resolve id e +Line 5, col 16: + 4 | try { +> 5 | return e; + ^ + 6 | } catch (e) { +" +`; + +exports[`resolveStatements should fail statements for case-34 1`] = ` +":6:5: Variable already exists: e +Line 6, col 5: + 5 | let e: String = "qwe"; +> 6 | try { + ^~~~~ + 7 | return e; +" +`; + +exports[`resolveStatements should fail statements for case-35 1`] = ` +":8:9: Type mismatch: Int is not assignable to String +Line 8, col 9: + 7 | } catch (e) { +> 8 | return e; + ^~~~~~~~~ + 9 | } +" +`; + exports[`resolveStatements should resolve statements for case-0 1`] = ` [ [ diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index 88481c762..cd9bf6939 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -335,6 +335,43 @@ function processStatements( s.ref, ); } + } else if (s.kind === "statement_try") { + // Process inner statements + const r = processStatements(s.statements, sctx, ctx); + ctx = r.ctx; + sctx = r.sctx; + } else if (s.kind === "statement_try_catch") { + let initialCtx = sctx; + + // Process inner statements + const r = processStatements(s.statements, sctx, ctx); + ctx = r.ctx; + + // Process catchName variable for exit code + if (initialCtx.vars.has(s.catchName)) { + throwError(`Variable already exists: ${s.catchName}`, s.ref); + } + let catchCtx = addVariable( + s.catchName, + { kind: "ref", name: "Int", optional: false }, + initialCtx, + ); + + // Process catch statements + const rCatch = processStatements(s.catchStatements, catchCtx, ctx); + ctx = rCatch.ctx; + catchCtx = rCatch.sctx; + + // Merge statement contexts + const removed: string[] = []; + for (const f of initialCtx.requiredFields) { + if (!catchCtx.requiredFields.find((v) => v === f)) { + removed.push(f); + } + } + for (const r of removed) { + initialCtx = removeRequiredVariable(r, initialCtx); + } } else { throw Error("Unknown statement"); } diff --git a/src/types/stmts-failed/case-32.tact b/src/types/stmts-failed/case-32.tact new file mode 100644 index 000000000..589de059c --- /dev/null +++ b/src/types/stmts-failed/case-32.tact @@ -0,0 +1,10 @@ +primitive Int; + +fun test() { + try { + + } catch (e) { + + } + return e; +} \ No newline at end of file diff --git a/src/types/stmts-failed/case-33.tact b/src/types/stmts-failed/case-33.tact new file mode 100644 index 000000000..f8b3a806f --- /dev/null +++ b/src/types/stmts-failed/case-33.tact @@ -0,0 +1,9 @@ +primitive Int; + +fun test() { + try { + return e; + } catch (e) { + + } +} \ No newline at end of file diff --git a/src/types/stmts-failed/case-34.tact b/src/types/stmts-failed/case-34.tact new file mode 100644 index 000000000..8c4250b3d --- /dev/null +++ b/src/types/stmts-failed/case-34.tact @@ -0,0 +1,12 @@ +primitive Int; +primitive String; + +fun test(): String { + let e: String = "qwe"; + try { + return e; + } catch (e) { + return e; + } + return "test"; +} \ No newline at end of file diff --git a/src/types/stmts-failed/case-35.tact b/src/types/stmts-failed/case-35.tact new file mode 100644 index 000000000..1ccd4ac53 --- /dev/null +++ b/src/types/stmts-failed/case-35.tact @@ -0,0 +1,11 @@ +primitive Int; +primitive String; + +fun test(): String { + try { + return "qwe"; + } catch (e) { + return e; + } + return "test"; +} \ No newline at end of file diff --git a/tact.config.json b/tact.config.json index 42d65880f..695c25d2c 100644 --- a/tact.config.json +++ b/tact.config.json @@ -221,6 +221,11 @@ "debug": true } }, + { + "name": "try-catch", + "path": "./src/test/features/try-catch.tact", + "output": "./src/test/features/output" + }, { "name": "masterchain-allow", "path": "./src/test/features/masterchain.tact",