diff --git a/CHANGELOG.md b/CHANGELOG.md index fb00f1f8b..721956fbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Allow underscores as unused variable identifiers: PR [#338](https://github.com/tact-lang/tact/pull/338) - The default compilation mode does decompile BoC files anymore, to additionally perform decompilation at the end of the pipeline, set the `fullWithDecompilation` mode in the `mode` project properties of `tact.config.json`: PR [#417](https://github.com/tact-lang/tact/pull/417) - Trait lists, parameters and arguments in the Tact grammar were assigned their own names in the grammar for better readability and code deduplication: PR [#422](https://github.com/tact-lang/tact/pull/422) +- The semicolon (`;`) terminating a statement is optional if the statement is the last one in the statement block: PR [#434](https://github.com/tact-lang/tact/pull/434) ### Fixed diff --git a/src/grammar/__snapshots__/grammar.spec.ts.snap b/src/grammar/__snapshots__/grammar.spec.ts.snap index 5826c3845..da1716f66 100644 --- a/src/grammar/__snapshots__/grammar.spec.ts.snap +++ b/src/grammar/__snapshots__/grammar.spec.ts.snap @@ -219,7 +219,7 @@ Line 2, col 14: `; exports[`grammar should fail literal-no-underscores-if-leading-zero 1`] = ` -":2:15: Syntax error: expected ";" +":2:15: Syntax error: expected "}" or ";" Line 2, col 15: 1 | fun test_fun(): Int { > 2 | return 012_3; @@ -229,7 +229,7 @@ Line 2, col 15: `; exports[`grammar should fail literal-non-binary-digits 1`] = ` -":2:15: Syntax error: expected ";" +":2:15: Syntax error: expected "}" or ";" Line 2, col 15: 1 | fun test_fun(): Int { > 2 | return 0b123; @@ -239,7 +239,7 @@ Line 2, col 15: `; exports[`grammar should fail literal-underscore-after-leading-zero 1`] = ` -":2:13: Syntax error: expected ";" +":2:13: Syntax error: expected "}" or ";" Line 2, col 13: 1 | fun test_fun(): Int { > 2 | return 0_123; @@ -4937,6 +4937,247 @@ exports[`grammar should parse stmt-if-else 1`] = ` } `; +exports[`grammar should parse stmt-optional-semicolon-for-last-statement 1`] = ` +{ + "entries": [ + { + "args": [], + "attributes": [], + "id": 17, + "kind": "def_function", + "name": "test1", + "origin": "user", + "ref": fun test1() { + let i: Int = 1; + while(i >= 10 || i <= 100) { i += 1 } + let i = 42 +}, + "return": null, + "statements": [ + { + "expression": { + "id": 2, + "kind": "number", + "ref": 1, + "value": 1n, + }, + "id": 3, + "kind": "statement_let", + "name": "i", + "ref": let i: Int = 1;, + "type": { + "id": 1, + "kind": "type_ref_simple", + "name": "Int", + "optional": false, + "ref": Int, + }, + }, + { + "condition": { + "id": 10, + "kind": "op_binary", + "left": { + "id": 6, + "kind": "op_binary", + "left": { + "id": 4, + "kind": "id", + "ref": i, + "value": "i", + }, + "op": ">=", + "ref": i >= 10, + "right": { + "id": 5, + "kind": "number", + "ref": 10, + "value": 10n, + }, + }, + "op": "||", + "ref": i >= 10 || i <= 100, + "right": { + "id": 9, + "kind": "op_binary", + "left": { + "id": 7, + "kind": "id", + "ref": i, + "value": "i", + }, + "op": "<=", + "ref": i <= 100, + "right": { + "id": 8, + "kind": "number", + "ref": 100, + "value": 100n, + }, + }, + }, + "id": 14, + "kind": "statement_while", + "ref": while(i >= 10 || i <= 100) { i += 1 }, + "statements": [ + { + "expression": { + "id": 12, + "kind": "number", + "ref": 1, + "value": 1n, + }, + "id": 13, + "kind": "statement_augmentedassign", + "op": "+", + "path": [ + { + "id": 11, + "kind": "lvalue_ref", + "name": "i", + "ref": i, + }, + ], + "ref": i += 1, + }, + ], + }, + { + "expression": { + "id": 15, + "kind": "number", + "ref": 42, + "value": 42n, + }, + "id": 16, + "kind": "statement_let", + "name": "i", + "ref": let i = 42, + "type": null, + }, + ], + }, + { + "args": [], + "attributes": [], + "id": 19, + "kind": "def_function", + "name": "test2", + "origin": "user", + "ref": fun test2() { return }, + "return": null, + "statements": [ + { + "expression": null, + "id": 18, + "kind": "statement_return", + "ref": return, + }, + ], + }, + { + "args": [], + "attributes": [], + "id": 23, + "kind": "def_function", + "name": "test3", + "origin": "user", + "ref": fun test3(): Int { return 42 }, + "return": { + "id": 20, + "kind": "type_ref_simple", + "name": "Int", + "optional": false, + "ref": Int, + }, + "statements": [ + { + "expression": { + "id": 21, + "kind": "number", + "ref": 42, + "value": 42n, + }, + "id": 22, + "kind": "statement_return", + "ref": return 42, + }, + ], + }, + { + "args": [], + "attributes": [], + "id": 33, + "kind": "def_function", + "name": "test4", + "origin": "user", + "ref": fun test4(): Int { + do { 21 + 21 } until (true && true) +}, + "return": { + "id": 24, + "kind": "type_ref_simple", + "name": "Int", + "optional": false, + "ref": Int, + }, + "statements": [ + { + "condition": { + "id": 27, + "kind": "op_binary", + "left": { + "id": 25, + "kind": "boolean", + "ref": true, + "value": true, + }, + "op": "&&", + "ref": true && true, + "right": { + "id": 26, + "kind": "boolean", + "ref": true, + "value": true, + }, + }, + "id": 32, + "kind": "statement_until", + "ref": do { 21 + 21 } until (true && true), + "statements": [ + { + "expression": { + "id": 30, + "kind": "op_binary", + "left": { + "id": 28, + "kind": "number", + "ref": 21, + "value": 21n, + }, + "op": "+", + "ref": 21 + 21, + "right": { + "id": 29, + "kind": "number", + "ref": 21, + "value": 21n, + }, + }, + "id": 31, + "kind": "statement_expression", + "ref": 21 + 21, + }, + ], + }, + ], + }, + ], + "id": 34, + "kind": "program", +} +`; + exports[`grammar should parse stmt-while-loop 1`] = ` { "entries": [ diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index 0e64f6e35..ba54c33b7 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -89,7 +89,6 @@ Tact { | external "(" Parameter? ")" "{" Statement* "}" --externalRegular | external "(" stringLiteral ")" "{" Statement* "}" --externalComment - // Statements Statement = StatementLet | StatementBlock | StatementReturn @@ -104,13 +103,15 @@ Tact { StatementBlock = "{" Statement* "}" - StatementLet = let id (":" Type)? "=" Expression ";" + // Do not require the terminating semicolon + // if this is the last statement in a block + StatementLet = let id (":" Type)? "=" Expression (";" | &"}") - StatementReturn = return Expression? ";" + StatementReturn = return Expression? (";" | &"}") - StatementExpression = Expression ";" + StatementExpression = Expression (";" | &"}") - StatementAssign = LValue ("=" | "+=" | "-=" | "*=" | "/=" | "%=" | "|=" | "&=" | "^=") Expression ";" + StatementAssign = LValue ("=" | "+=" | "-=" | "*=" | "/=" | "%=" | "|=" | "&=" | "^=") Expression (";" | &"}") StatementCondition = if Expression "{" Statement* "}" ~else --noElse | if Expression "{" Statement* "}" else "{" Statement* "}" --withElse @@ -120,7 +121,7 @@ Tact { StatementRepeat = repeat "(" Expression ")" "{" Statement* "}" - StatementUntil = do "{" Statement* "}" until "(" Expression ")" ";" + StatementUntil = do "{" Statement* "}" until "(" Expression ")" (";" | &"}") // making the catch clause optional using Ohm's `?` does not make sense // because Ohm will create _independent_ optional grammar nodes which diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index 570f4a218..50c9a3616 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -537,7 +537,7 @@ semantics.addOperation("astOfStatement", { optType, _equals, expression, - _semicolon, + _optSemicolonIfLastStmtInBlock, ) { checkVariableName(id.sourceString, createRef(id)); @@ -549,7 +549,7 @@ semantics.addOperation("astOfStatement", { ref: createRef(this), }); }, - StatementReturn(_returnKwd, optExpression, _semicolon) { + StatementReturn(_returnKwd, optExpression, _optSemicolonIfLastStmtInBlock) { return createNode({ kind: "statement_return", expression: unwrapOptNode(optExpression, (e) => @@ -558,14 +558,19 @@ semantics.addOperation("astOfStatement", { ref: createRef(this), }); }, - StatementExpression(expression, _semicolon) { + StatementExpression(expression, _optSemicolonIfLastStmtInBlock) { return createNode({ kind: "statement_expression", expression: expression.astOfExpression(), ref: createRef(this), }); }, - StatementAssign(lvalue, operator, expression, _semicolon) { + StatementAssign( + lvalue, + operator, + expression, + _optSemicolonIfLastStmtInBlock, + ) { if (operator.sourceString === "=") { return createNode({ kind: "statement_assign", @@ -701,7 +706,7 @@ semantics.addOperation("astOfStatement", { _lparen, condition, _rparen, - _semicolon, + _optSemicolonIfLastStmtInBlock, ) { return createNode({ kind: "statement_until", diff --git a/src/grammar/test/stmt-optional-semicolon-for-last-statement.tact b/src/grammar/test/stmt-optional-semicolon-for-last-statement.tact new file mode 100644 index 000000000..d2ed37a9e --- /dev/null +++ b/src/grammar/test/stmt-optional-semicolon-for-last-statement.tact @@ -0,0 +1,17 @@ +// what is covered: assignment, let +fun test1() { + let i: Int = 1; + while(i >= 10 || i <= 100) { i += 1 } + let i = 42 +} + +// what is covered: return without expression +fun test2() { return } + +// what is covered: return with expression +fun test3(): Int { return 42 } + +// what is covered: do-until, expression statement +fun test4(): Int { + do { 21 + 21 } until (true && true) +} \ No newline at end of file