diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e8b58c7..c9cbbf0d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `let` statements can now be used without an explicit type declaration and determine the type automatically if it was not specified: PR [#198](https://github.com/tact-lang/tact/pull/198) - The outdated TextMate-style grammar files for text editors have been removed (the most recent grammar files can be found in the [tact-sublime](https://github.com/tact-lang/tact-sublime) repo): PR [#404](https://github.com/tact-lang/tact/pull/404) - The JSON schema for `tact.config.json` has been moved to the `json-schemas` project folder: PR [#404](https://github.com/tact-lang/tact/pull/404) +- 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) ### Fixed diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index eda59a495..10cbb6e63 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -90,6 +90,12 @@ export function writeStatement( } return; } else if (f.kind === "statement_let") { + // Underscore name case + if (f.name === "_") { + ctx.append(`${writeExpression(f.expression, ctx)};`); + return; + } + // Contract/struct case const t = f.type === null @@ -195,7 +201,11 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } }); - ctx.append(`} catch (_, ${id(f.catchName)}) {`); + if (f.catchName == "_") { + ctx.append(`} catch (_) {`); + } else { + ctx.append(`} catch (_, ${id(f.catchName)}) {`); + } ctx.inIndent(() => { for (const s of f.catchStatements) { writeStatement(s, self, returns, ctx); @@ -215,6 +225,12 @@ export function writeStatement( } const flag = freshIdentifier("flag"); + const key = + f.keyName == "_" ? freshIdentifier("underscore") : id(f.keyName); + const value = + f.valueName == "_" + ? freshIdentifier("underscore") + : id(f.valueName); // Handle Int key if (t.key === "Int") { @@ -237,7 +253,7 @@ export function writeStatement( } ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_${vKind}`)}(${path}, ${bits}, ${vBits});`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_${vKind}`)}(${path}, ${bits}, ${vBits});`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { @@ -245,13 +261,13 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_${vKind}`)}(${path}, ${bits}, ${id(f.keyName)}, ${vBits});`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_${vKind}`)}(${path}, ${bits}, ${key}, ${vBits});`, ); }); ctx.append(`}`); } else if (t.value === "Bool") { ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_int`)}(${path}, ${bits}, 1);`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_int`)}(${path}, ${bits}, 1);`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { @@ -259,13 +275,13 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_int`)}(${path}, ${bits}, ${id(f.keyName)}, 1);`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_int`)}(${path}, ${bits}, ${key}, 1);`, ); }); ctx.append(`}`); } else if (t.value === "Cell") { ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_cell`)}(${path}, ${bits});`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_cell`)}(${path}, ${bits});`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { @@ -273,13 +289,13 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_cell`)}(${path}, ${bits}, ${id(f.keyName)});`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_cell`)}(${path}, ${bits}, ${key});`, ); }); ctx.append(`}`); } else if (t.value === "Address") { ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_slice`)}(${path}, ${bits});`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_slice`)}(${path}, ${bits});`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { @@ -287,25 +303,25 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_slice`)}(${path}, ${bits}, ${id(f.keyName)});`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_slice`)}(${path}, ${bits}, ${key});`, ); }); ctx.append(`}`); } else { // value is struct ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_cell`)}(${path}, ${bits});`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_${kind}_cell`)}(${path}, ${bits});`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { ctx.append( - `var ${resolveFuncTypeUnpack(t.value, id(f.valueName), ctx)} = ${ops.typeNotNull(t.value, ctx)}(${ops.readerOpt(t.value, ctx)}(${id(f.valueName)}));`, + `var ${resolveFuncTypeUnpack(t.value, id(f.valueName), ctx)} = ${ops.typeNotNull(t.value, ctx)}(${ops.readerOpt(t.value, ctx)}(${value}));`, ); for (const s of f.statements) { writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_cell`)}(${path}, ${bits}, ${id(f.keyName)});`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_cell`)}(${path}, ${bits}, ${key});`, ); }); ctx.append(`}`); @@ -324,7 +340,7 @@ export function writeStatement( vKind = "uint"; } ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_${vKind}`)}(${path}, 267, ${vBits});`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_${vKind}`)}(${path}, 267, ${vBits});`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { @@ -332,13 +348,13 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_${vKind}`)}(${path}, 267, ${id(f.keyName)}, ${vBits});`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_${vKind}`)}(${path}, 267, ${key}, ${vBits});`, ); }); ctx.append(`}`); } else if (t.value === "Bool") { ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_int`)}(${path}, 267, 1);`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_int`)}(${path}, 267, 1);`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { @@ -346,13 +362,13 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_int`)}(${path}, 267, ${id(f.keyName)}, 1);`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_int`)}(${path}, 267, ${key}, 1);`, ); }); ctx.append(`}`); } else if (t.value === "Cell") { ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_cell`)}(${path}, 267);`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_cell`)}(${path}, 267);`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { @@ -360,13 +376,13 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_cell`)}(${path}, 267, ${id(f.keyName)});`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_cell`)}(${path}, 267, ${key});`, ); }); ctx.append(`}`); } else if (t.value === "Address") { ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_slice`)}(${path}, 267);`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_slice`)}(${path}, 267);`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { @@ -374,25 +390,25 @@ export function writeStatement( writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_slice`)}(${path}, 267, ${id(f.keyName)});`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_slice`)}(${path}, 267, ${key});`, ); }); ctx.append(`}`); } else { // value is struct ctx.append( - `var (${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_cell`)}(${path}, 267);`, + `var (${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_min_slice_cell`)}(${path}, 267);`, ); ctx.append(`while (${flag}) {`); ctx.inIndent(() => { ctx.append( - `var ${resolveFuncTypeUnpack(t.value, id(f.valueName), ctx)} = ${ops.typeNotNull(t.value, ctx)}(${ops.readerOpt(t.value, ctx)}(${id(f.valueName)}));`, + `var ${resolveFuncTypeUnpack(t.value, id(f.valueName), ctx)} = ${ops.typeNotNull(t.value, ctx)}(${ops.readerOpt(t.value, ctx)}(${value}));`, ); for (const s of f.statements) { writeStatement(s, self, returns, ctx); } ctx.append( - `(${id(f.keyName)}, ${id(f.valueName)}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_cell`)}(${path}, 267, ${id(f.keyName)});`, + `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_cell`)}(${path}, 267, ${key});`, ); }); ctx.append(`}`); diff --git a/src/test/e2e-emulated/contracts/underscore-variable.tact b/src/test/e2e-emulated/contracts/underscore-variable.tact new file mode 100644 index 000000000..73fbaeef7 --- /dev/null +++ b/src/test/e2e-emulated/contracts/underscore-variable.tact @@ -0,0 +1,57 @@ +contract UnderscoreVariableTestContract { + something: Int; + + init() { + self.something = 0; + } + + receive() { + // Nothing to do + } + + fun increaseSomething(): Int { + self.something += 1; + return 123; + } + + get fun test1(): Int { + try { + nativeThrow(1); + } catch (_) { + return 0; + } + return 1; + } + + get fun test2(): Int { + let m: map = emptyMap(); + m.set(1, 2); + m.set(2, 4); + m.set(3, 6); + let x: Int = 0; + foreach (_, v in m) { + x += v; + } + return x; + } + + get fun test3(): Int { + let m: map = emptyMap(); + m.set(1, 2); + m.set(2, 4); + m.set(3, 6); + let x: Int = 0; + foreach (k, _ in m) { + x += k; + } + return x; + } + + get fun test4(): Int { + let _: Int = self.increaseSomething(); + let _: Int = self.increaseSomething(); + let _ = self.increaseSomething(); + let _ = self.increaseSomething(); + return self.something; + } +} \ No newline at end of file diff --git a/src/test/e2e-emulated/underscore-variable.spec.ts b/src/test/e2e-emulated/underscore-variable.spec.ts new file mode 100644 index 000000000..e447c2cda --- /dev/null +++ b/src/test/e2e-emulated/underscore-variable.spec.ts @@ -0,0 +1,26 @@ +import { toNano } from "@ton/core"; +import { ContractSystem } from "@tact-lang/emulator"; +import { __DANGER_resetNodeId } from "../../grammar/ast"; +import { UnderscoreVariableTestContract } from "./contracts/output/underscore-variable_UnderscoreVariableTestContract"; + +describe("underscore-variable", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + it("should implement underscore variables correctly", async () => { + // Init + const system = await ContractSystem.create(); + const treasure = system.treasure("treasure"); + const contract = system.open( + await UnderscoreVariableTestContract.fromInit(), + ); + await contract.send(treasure, { value: toNano("10") }, null); + await system.run(); + + // Check methods + expect(await contract.getTest1()).toEqual(0n); + expect(await contract.getTest2()).toEqual(12n); + expect(await contract.getTest3()).toEqual(6n); + expect(await contract.getTest4()).toEqual(4n); + }); +}); diff --git a/src/types/__snapshots__/resolveStatements.spec.ts.snap b/src/types/__snapshots__/resolveStatements.spec.ts.snap index fe3e1af24..c8d73e1c3 100644 --- a/src/types/__snapshots__/resolveStatements.spec.ts.snap +++ b/src/types/__snapshots__/resolveStatements.spec.ts.snap @@ -580,6 +580,36 @@ Line 7, col 9: " `; +exports[`resolveStatements should fail statements for var-underscore-name-access 1`] = ` +":6:16: Wildcard variable name '_' cannot be accessed +Line 6, col 16: + 5 | foreach (_, _ in m) { +> 6 | return _; + ^ + 7 | } +" +`; + +exports[`resolveStatements should fail statements for var-underscore-name-access2 1`] = ` +":7:14: Wildcard variable name '_' cannot be accessed +Line 7, col 14: + 6 | foreach (_, v in m) { +> 7 | x += _; + ^ + 8 | } +" +`; + +exports[`resolveStatements should fail statements for var-underscore-name-access3 1`] = ` +":9:12: Wildcard variable name '_' cannot be accessed +Line 9, col 12: + 8 | let _: Int = someImpureFunction(); +> 9 | return _; + ^ + 10 | } +" +`; + exports[`resolveStatements should resolve statements for contract-receiver-bounced 1`] = ` [ [ @@ -1602,3 +1632,66 @@ exports[`resolveStatements should resolve statements for var-scope-valueOf-fun 1 ], ] `; + +exports[`resolveStatements should resolve statements for var-underscore-name-in-foreach 1`] = ` +[ + [ + "emptyMap()", + "", + ], + [ + "m", + "map", + ], +] +`; + +exports[`resolveStatements should resolve statements for var-underscore-name-in-foreach2 1`] = ` +[ + [ + "emptyMap()", + "", + ], + [ + "0", + "Int", + ], + [ + "m", + "map", + ], + [ + "x", + "Int", + ], + [ + "v", + "Int", + ], + [ + "x", + "Int", + ], +] +`; + +exports[`resolveStatements should resolve statements for var-underscore-name-in-let 1`] = ` +[ + [ + "123", + "Int", + ], + [ + "someImpureFunction()", + "Int", + ], + [ + "someImpureFunction()", + "Int", + ], + [ + "123", + "Int", + ], +] +`; diff --git a/src/types/resolveExpression.ts b/src/types/resolveExpression.ts index 99d72ac27..33c920846 100644 --- a/src/types/resolveExpression.ts +++ b/src/types/resolveExpression.ts @@ -766,6 +766,12 @@ export function resolveExpression( const v = sctx.vars.get(exp.value); if (!v) { if (!hasStaticConstant(ctx, exp.value)) { + if (exp.value === "_") { + throwError( + "Wildcard variable name '_' cannot be accessed", + exp.ref, + ); + } throwError("Unable to resolve id " + exp.value, exp.ref); } else { const cc = getStaticConstant(ctx, exp.value); diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index d5502fe1a..3f39c9898 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -29,6 +29,20 @@ function emptyContext(root: ASTRef, returns: TypeRef): StatementContext { }; } +function checkVariableExists( + ctx: StatementContext, + name: string, + ref?: ASTRef, +): void { + if (ctx.vars.has(name)) { + if (ref) { + throwError(`Variable already exists: "${name}"`, ref); + } else { + throw Error(`Variable already exists: "${name}"`); + } + } +} + function addRequiredVariables( name: string, src: StatementContext, @@ -61,8 +75,9 @@ function addVariable( ref: TypeRef, src: StatementContext, ): StatementContext { - if (src.vars.has(name)) { - throw Error("Variable already exists: " + name); // Should happen earlier + checkVariableExists(src, name); // Should happen earlier + if (name == "_") { + return src; } return { ...src, @@ -171,9 +186,7 @@ function processStatements( ctx = resolveExpression(s.expression, sctx, ctx); // Check variable name - if (sctx.vars.has(s.name)) { - throwError(`Variable "${s.name}" already exists`, s.ref); - } + checkVariableExists(sctx, s.name, s.ref); // Check type const expressionType = getExpType(ctx, s.expression); @@ -388,11 +401,11 @@ function processStatements( const r = processStatements(s.statements, sctx, ctx); ctx = r.ctx; + let catchCtx = sctx; + // Process catchName variable for exit code - if (initialCtx.vars.has(s.catchName)) { - throwError(`Variable already exists: "${s.catchName}"`, s.ref); - } - let catchCtx = addVariable( + checkVariableExists(initialCtx, s.catchName, s.ref); + catchCtx = addVariable( s.catchName, { kind: "ref", name: "Int", optional: false }, initialCtx, @@ -432,23 +445,25 @@ function processStatements( ); } + let foreachCtx = sctx; + // Add key and value to statement context - if (initialCtx.vars.has(s.keyName)) { - throwError(`Variable already exists: "${s.keyName}"`, s.ref); + if (s.keyName != "_") { + checkVariableExists(initialCtx, s.keyName, s.ref); + foreachCtx = addVariable( + s.keyName, + { kind: "ref", name: mapType.key, optional: false }, + initialCtx, + ); } - let foreachCtx = addVariable( - s.keyName, - { kind: "ref", name: mapType.key, optional: false }, - initialCtx, - ); - if (foreachCtx.vars.has(s.valueName)) { - throwError(`Variable already exists: "${s.valueName}"`, s.ref); + if (s.valueName != "_") { + checkVariableExists(foreachCtx, s.valueName, s.ref); + foreachCtx = addVariable( + s.valueName, + { kind: "ref", name: mapType.value, optional: false }, + foreachCtx, + ); } - foreachCtx = addVariable( - s.valueName, - { kind: "ref", name: mapType.value, optional: false }, - foreachCtx, - ); // Process inner statements const r = processStatements(s.statements, foreachCtx, ctx); diff --git a/src/types/stmts-failed/var-underscore-name-access.tact b/src/types/stmts-failed/var-underscore-name-access.tact new file mode 100644 index 000000000..82fdae5bc --- /dev/null +++ b/src/types/stmts-failed/var-underscore-name-access.tact @@ -0,0 +1,9 @@ +primitive Int; + +fun test(): Int { + let m: map = emptyMap(); + foreach (_, _ in m) { + return _; + } + return 0; +} diff --git a/src/types/stmts-failed/var-underscore-name-access2.tact b/src/types/stmts-failed/var-underscore-name-access2.tact new file mode 100644 index 000000000..236352d4a --- /dev/null +++ b/src/types/stmts-failed/var-underscore-name-access2.tact @@ -0,0 +1,10 @@ +primitive Int; + +fun test(): Int { + let m: map = emptyMap(); + let x: Int = 0; + foreach (_, v in m) { + x += _; + } + return x; +} diff --git a/src/types/stmts-failed/var-underscore-name-access3.tact b/src/types/stmts-failed/var-underscore-name-access3.tact new file mode 100644 index 000000000..b83d7e3f8 --- /dev/null +++ b/src/types/stmts-failed/var-underscore-name-access3.tact @@ -0,0 +1,10 @@ +primitive Int; + +fun someImpureFunction(): Int { + return 123; +} + +fun test(): Int { + let _: Int = someImpureFunction(); + return _; +} diff --git a/src/types/stmts/var-underscore-name-in-foreach.tact b/src/types/stmts/var-underscore-name-in-foreach.tact new file mode 100644 index 000000000..a8410783e --- /dev/null +++ b/src/types/stmts/var-underscore-name-in-foreach.tact @@ -0,0 +1,8 @@ +primitive Int; + +fun test() { + let m: map = emptyMap(); + foreach (_, _ in m) { + // something + } +} diff --git a/src/types/stmts/var-underscore-name-in-foreach2.tact b/src/types/stmts/var-underscore-name-in-foreach2.tact new file mode 100644 index 000000000..ea816f116 --- /dev/null +++ b/src/types/stmts/var-underscore-name-in-foreach2.tact @@ -0,0 +1,10 @@ +primitive Int; + +fun test(): Int { + let m: map = emptyMap(); + let x: Int = 0; + foreach (_, v in m) { + x += v; + } + return x; +} diff --git a/src/types/stmts/var-underscore-name-in-let.tact b/src/types/stmts/var-underscore-name-in-let.tact new file mode 100644 index 000000000..6422a59bc --- /dev/null +++ b/src/types/stmts/var-underscore-name-in-let.tact @@ -0,0 +1,11 @@ +primitive Int; + +fun someImpureFunction(): Int { + return 123; +} + +fun test(): Int { + let _: Int = someImpureFunction(); + let _ = someImpureFunction(); + return 123; +} diff --git a/tact.config.json b/tact.config.json index 11753842d..0203b4139 100644 --- a/tact.config.json +++ b/tact.config.json @@ -94,6 +94,11 @@ "path": "./src/test/e2e-emulated/contracts/mutating-method-chaining.tact", "output": "./src/test/e2e-emulated/contracts/output" }, + { + "name": "underscore-variable", + "path": "./src/test/e2e-emulated/contracts/underscore-variable.tact", + "output": "./src/test/e2e-emulated/contracts/output" + }, { "name": "optionals", "path": "./src/test/e2e-emulated/contracts/optionals.tact",