From c66a567193aed4c019c4f0f36a3d6a70c4a183fb Mon Sep 17 00:00:00 2001 From: Daniil Sedov <42098239+Gusarich@users.noreply.github.com> Date: Fri, 14 Jun 2024 10:18:37 +0300 Subject: [PATCH] feat: local type inference for `let` statements (#198) Type annotations for `let`-binders are optional now --- CHANGELOG.md | 1 + src/generator/writers/writeFunction.ts | 6 +- src/grammar/ast.ts | 3 +- src/grammar/clone.ts | 5 +- src/grammar/grammar.ohm | 2 +- src/grammar/grammar.ts | 12 +- .../local-type-inference.spec.ts.snap | 473 ++++++++++++++++++ .../contracts/local-type-inference.tact | 116 +++++ .../e2e-emulated/local-type-inference.spec.ts | 67 +++ .../resolveStatements.spec.ts.snap | 46 ++ src/types/resolveStatements.ts | 31 +- .../stmt-let-unknown-type-inference.tact | 9 + .../stmt-let-unknown-type-inference2.tact | 9 + .../stmts/stmt-let-map-type-inference.tact | 10 + .../stmt-let-nullable-type-inference.tact | 10 + tact.config.json | 8 + 16 files changed, 789 insertions(+), 19 deletions(-) create mode 100644 src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap create mode 100644 src/test/e2e-emulated/contracts/local-type-inference.tact create mode 100644 src/test/e2e-emulated/local-type-inference.spec.ts create mode 100644 src/types/stmts-failed/stmt-let-unknown-type-inference.tact create mode 100644 src/types/stmts-failed/stmt-let-unknown-type-inference2.tact create mode 100644 src/types/stmts/stmt-let-map-type-inference.tact create mode 100644 src/types/stmts/stmt-let-nullable-type-inference.tact diff --git a/CHANGELOG.md b/CHANGELOG.md index a761d4fce..5001a245d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Trailing semicolons in struct and message declarations are optional now: PR [#395](https://github.com/tact-lang/tact/pull/395) - Tests are refactored and renamed to convey the sense of what is being tested and to reduce the amount of merge conflicts during development: PR [#402](https://github.com/tact-lang/tact/pull/402) +- `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) diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index af556c88d..eda59a495 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -91,7 +91,11 @@ export function writeStatement( return; } else if (f.kind === "statement_let") { // Contract/struct case - const t = resolveTypeRef(ctx.ctx, f.type); + const t = + f.type === null + ? getExpType(ctx.ctx, f.expression) + : resolveTypeRef(ctx.ctx, f.type); + if (t.kind === "ref") { const tt = getType(ctx.ctx, t.name); if (tt.kind === "contract" || tt.kind === "struct") { diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 7b08d3066..8cbef0f38 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -418,7 +418,7 @@ export type ASTStatementLet = { kind: "statement_let"; id: number; name: string; - type: ASTTypeRef; + type: ASTTypeRef | null; expression: ASTExpression; ref: ASTRef; }; @@ -729,7 +729,6 @@ export function traverse(node: ASTNode, callback: (node: ASTNode) => void) { // if (node.kind === "statement_let") { - traverse(node.type, callback); traverse(node.expression, callback); } if (node.kind === "statement_return") { diff --git a/src/grammar/clone.ts b/src/grammar/clone.ts index 4f323bf35..bcea020c8 100644 --- a/src/grammar/clone.ts +++ b/src/grammar/clone.ts @@ -28,7 +28,7 @@ export function cloneNode(src: T): T { } else if (src.kind === "statement_let") { return cloneASTNode({ ...src, - type: cloneASTNode(src.type), + type: src.type ? cloneASTNode(src.type) : null, expression: cloneNode(src.expression), }); } else if (src.kind === "statement_condition") { @@ -132,12 +132,14 @@ export function cloneNode(src: T): T { } else if (src.kind === "def_function") { return cloneASTNode({ ...src, + return: src.return ? cloneASTNode(src.return) : null, statements: src.statements ? src.statements.map(cloneNode) : null, args: src.args.map(cloneNode), }); } else if (src.kind === "def_native_function") { return cloneASTNode({ ...src, + return: src.return ? cloneASTNode(src.return) : null, args: src.args.map(cloneNode), }); } else if (src.kind === "def_receive") { @@ -158,6 +160,7 @@ export function cloneNode(src: T): T { } else if (src.kind === "def_constant") { return cloneASTNode({ ...src, + type: cloneASTNode(src.type), value: src.value ? cloneNode(src.value) : src.value, }); } diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index 2be2d2372..913812563 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -100,7 +100,7 @@ Tact { StatementBlock = "{" Statement* "}" - StatementLet = let id ":" Type "=" Expression ";" + StatementLet = let id (":" Type)? "=" Expression ";" StatementReturn = return Expression? ";" diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index cc857cb98..0ddf4118a 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -571,13 +571,21 @@ semantics.addOperation("astOfDeclaration", { semantics.addOperation("astOfStatement", { // TODO: process StatementBlock - StatementLet(_letKwd, id, _colon, type, _equals, expression, _semicolon) { + StatementLet( + _letKwd, + id, + _optColon, + optType, + _equals, + expression, + _semicolon, + ) { checkVariableName(id.sourceString, createRef(id)); return createNode({ kind: "statement_let", name: id.sourceString, - type: type.astOfType(), + type: unwrapOptNode(optType, (t) => t.astOfType()), expression: expression.astOfExpression(), ref: createRef(this), }); diff --git a/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap b/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap new file mode 100644 index 000000000..f08d8ccac --- /dev/null +++ b/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap @@ -0,0 +1,473 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`local-type-inference should automatically set types for let statements 1`] = ` +{ + "errors": { + "10": { + "message": "Dictionary error", + }, + "128": { + "message": "Null reference exception", + }, + "129": { + "message": "Invalid serialization prefix", + }, + "13": { + "message": "Out of gas error", + }, + "130": { + "message": "Invalid incoming message", + }, + "131": { + "message": "Constraints error", + }, + "132": { + "message": "Access denied", + }, + "133": { + "message": "Contract stopped", + }, + "134": { + "message": "Invalid argument", + }, + "135": { + "message": "Code of a contract was not found", + }, + "136": { + "message": "Invalid address", + }, + "137": { + "message": "Masterchain support is not enabled for this contract", + }, + "2": { + "message": "Stack underflow", + }, + "3": { + "message": "Stack overflow", + }, + "32": { + "message": "Method ID not found", + }, + "34": { + "message": "Action is invalid or not supported", + }, + "37": { + "message": "Not enough TON", + }, + "38": { + "message": "Not enough extra-currencies", + }, + "4": { + "message": "Integer overflow", + }, + "5": { + "message": "Integer out of expected range", + }, + "6": { + "message": "Invalid opcode", + }, + "7": { + "message": "Type check error", + }, + "8": { + "message": "Cell overflow", + }, + "9": { + "message": "Cell underflow", + }, + }, + "getters": [ + { + "arguments": [], + "name": "test1", + "returnType": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "arguments": [], + "name": "test2", + "returnType": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "arguments": [], + "name": "test3", + "returnType": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + { + "arguments": [], + "name": "test4", + "returnType": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + { + "arguments": [], + "name": "test5", + "returnType": { + "kind": "simple", + "optional": false, + "type": "bool", + }, + }, + { + "arguments": [], + "name": "test6", + "returnType": { + "kind": "simple", + "optional": false, + "type": "slice", + }, + }, + { + "arguments": [], + "name": "test7", + "returnType": { + "kind": "simple", + "optional": false, + "type": "cell", + }, + }, + { + "arguments": [], + "name": "test8", + "returnType": { + "kind": "simple", + "optional": false, + "type": "builder", + }, + }, + { + "arguments": [], + "name": "test9", + "returnType": { + "kind": "simple", + "optional": false, + "type": "string", + }, + }, + { + "arguments": [], + "name": "test10", + "returnType": { + "kind": "simple", + "optional": false, + "type": "string", + }, + }, + { + "arguments": [], + "name": "test11", + "returnType": { + "kind": "simple", + "optional": false, + "type": "StateInit", + }, + }, + { + "arguments": [], + "name": "test12", + "returnType": { + "key": "int", + "kind": "dict", + "value": "int", + }, + }, + { + "arguments": [], + "name": "test13", + "returnType": { + "key": "int", + "kind": "dict", + "value": "uint", + "valueFormat": 32, + }, + }, + { + "arguments": [], + "name": "test14", + "returnType": { + "kind": "simple", + "optional": false, + "type": "MyStruct", + }, + }, + { + "arguments": [], + "name": "test15", + "returnType": { + "kind": "simple", + "optional": false, + "type": "MyStruct", + }, + }, + { + "arguments": [], + "name": "test16", + "returnType": { + "format": 257, + "kind": "simple", + "optional": true, + "type": "int", + }, + }, + { + "arguments": [], + "name": "test17", + "returnType": { + "format": 257, + "kind": "simple", + "optional": true, + "type": "int", + }, + }, + { + "arguments": [], + "name": "test18", + "returnType": { + "format": 257, + "kind": "simple", + "optional": true, + "type": "int", + }, + }, + { + "arguments": [], + "name": "test19", + "returnType": { + "format": 257, + "kind": "simple", + "optional": true, + "type": "int", + }, + }, + ], + "receivers": [ + { + "message": { + "kind": "typed", + "type": "Deploy", + }, + "receiver": "internal", + }, + ], + "types": [ + { + "fields": [ + { + "name": "code", + "type": { + "kind": "simple", + "optional": false, + "type": "cell", + }, + }, + { + "name": "data", + "type": { + "kind": "simple", + "optional": false, + "type": "cell", + }, + }, + ], + "header": null, + "name": "StateInit", + }, + { + "fields": [ + { + "name": "bounced", + "type": { + "kind": "simple", + "optional": false, + "type": "bool", + }, + }, + { + "name": "sender", + "type": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + { + "name": "value", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "raw", + "type": { + "kind": "simple", + "optional": false, + "type": "slice", + }, + }, + ], + "header": null, + "name": "Context", + }, + { + "fields": [ + { + "name": "bounce", + "type": { + "kind": "simple", + "optional": false, + "type": "bool", + }, + }, + { + "name": "to", + "type": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + { + "name": "value", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "mode", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "body", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + { + "name": "code", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + { + "name": "data", + "type": { + "kind": "simple", + "optional": true, + "type": "cell", + }, + }, + ], + "header": null, + "name": "SendParameters", + }, + { + "fields": [ + { + "name": "queryId", + "type": { + "format": 64, + "kind": "simple", + "optional": false, + "type": "uint", + }, + }, + ], + "header": 2490013878, + "name": "Deploy", + }, + { + "fields": [ + { + "name": "queryId", + "type": { + "format": 64, + "kind": "simple", + "optional": false, + "type": "uint", + }, + }, + ], + "header": 2952335191, + "name": "DeployOk", + }, + { + "fields": [ + { + "name": "queryId", + "type": { + "format": 64, + "kind": "simple", + "optional": false, + "type": "uint", + }, + }, + { + "name": "cashback", + "type": { + "kind": "simple", + "optional": false, + "type": "address", + }, + }, + ], + "header": 1829761339, + "name": "FactoryDeploy", + }, + { + "fields": [ + { + "name": "x", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + { + "name": "y", + "type": { + "format": 257, + "kind": "simple", + "optional": false, + "type": "int", + }, + }, + ], + "header": null, + "name": "MyStruct", + }, + ], +} +`; diff --git a/src/test/e2e-emulated/contracts/local-type-inference.tact b/src/test/e2e-emulated/contracts/local-type-inference.tact new file mode 100644 index 000000000..dfa631f21 --- /dev/null +++ b/src/test/e2e-emulated/contracts/local-type-inference.tact @@ -0,0 +1,116 @@ +import "@stdlib/deploy"; + +struct MyStruct { + x: Int; + y: Int; +} + +contract LocalTypeInferenceTester with Deployable { + get fun test1(): Int { + let x = 1; + return x; + } + + get fun test2(): Int { + let x = 1; + let y = x + 1; + return y; + } + + get fun test3(): Address { + let x = myAddress(); + return x; + } + + get fun test4(): Address { + let x = myAddress(); + let y = x; + return y; + } + + get fun test5(): Bool { + let x: Int = 123; + let y = x == 123; + return y; + } + + get fun test6(): Slice { + let x = beginCell().storeUint(123, 64).endCell().asSlice(); + return x; + } + + get fun test7(): Cell { + let x = beginCell().storeUint(123, 64).endCell(); + return x; + } + + get fun test8(): Builder { + let x = beginCell().storeUint(123, 64); + return x; + } + + get fun test9(): String { + let x = beginString().concat("hello").toString(); + return x; + } + + get fun test10(): String { + let x = beginString(); + let y = x.concat("hello").toString(); + return y; + } + + get fun test11(): StateInit { + let x = initOf LocalTypeInferenceTester(); + return x; + } + + get fun test12(): map { + let x: map = emptyMap(); + let y = x; + return y; + } + + get fun test13(): map { + let x: map = emptyMap(); + let y = x; + return y; + } + + get fun test14(): MyStruct { + let x = MyStruct{ x: 1, y: 2 }; + return x; + } + + get fun test15(): MyStruct { + let x = MyStruct{ x: 1, y: 2 }; + let y = x; + return y; + } + + get fun test16(): Int? { + let m: map = emptyMap(); + let x = m.get(1); + return x; + } + + get fun test17(): Int? { + let m: map = emptyMap(); + let x = m.get(1); + let y = x; + return y; + } + + get fun test18(): Int? { + let m: map = emptyMap(); + m.set(1, 2); + let x = m.get(1); + return x; + } + + get fun test19(): Int? { + let x: Int? = null; + let y = x; + return y; + } +} \ No newline at end of file diff --git a/src/test/e2e-emulated/local-type-inference.spec.ts b/src/test/e2e-emulated/local-type-inference.spec.ts new file mode 100644 index 000000000..b6a986d07 --- /dev/null +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -0,0 +1,67 @@ +import { beginCell, toNano } from "@ton/core"; +import { ContractSystem } from "@tact-lang/emulator"; +import { __DANGER_resetNodeId } from "../../grammar/ast"; +import { LocalTypeInferenceTester } from "./contracts/output/local-type-inference_LocalTypeInferenceTester"; + +describe("local-type-inference", () => { + beforeEach(() => { + __DANGER_resetNodeId(); + }); + it("should automatically set types for let statements", async () => { + // Init + const system = await ContractSystem.create(); + const treasure = system.treasure("treasure"); + const contract = system.open(await LocalTypeInferenceTester.fromInit()); + await contract.send( + treasure, + { value: toNano("10") }, + { $$type: "Deploy", queryId: 0n }, + ); + await system.run(); + + expect(contract.abi).toMatchSnapshot(); + expect(await contract.getTest1()).toStrictEqual(1n); + expect(await contract.getTest2()).toStrictEqual(2n); + expect((await contract.getTest3()).toRawString()).toBe( + contract.address.toRawString(), + ); + expect((await contract.getTest4()).toRawString()).toBe( + contract.address.toRawString(), + ); + expect(await contract.getTest5()).toStrictEqual(true); + expect((await contract.getTest6()).toString()).toStrictEqual( + beginCell().storeUint(123, 64).endCell().asSlice().toString(), + ); + expect((await contract.getTest7()).toString()).toStrictEqual( + beginCell().storeUint(123, 64).endCell().toString(), + ); + expect((await contract.getTest8()).toString()).toStrictEqual( + beginCell().storeUint(123, 64).endCell().toString(), + ); + expect(await contract.getTest9()).toStrictEqual("hello"); + expect(await contract.getTest10()).toStrictEqual("hello"); + const test11 = await contract.getTest11(); + expect(test11.code.toString()).toStrictEqual( + contract.init?.code.toString(), + ); + expect(test11.data.toString()).toStrictEqual( + contract.init?.data.toString(), + ); + // test12 tested by abi + // test13 tested by abi + expect(await contract.getTest14()).toStrictEqual({ + $$type: "MyStruct", + x: 1n, + y: 2n, + }); + expect(await contract.getTest15()).toStrictEqual({ + $$type: "MyStruct", + x: 1n, + y: 2n, + }); + expect(await contract.getTest16()).toBeNull(); + expect(await contract.getTest17()).toBeNull(); + expect(await contract.getTest18()).toBe(2n); + expect(await contract.getTest19()).toBeNull(); + }); +}); diff --git a/src/types/__snapshots__/resolveStatements.spec.ts.snap b/src/types/__snapshots__/resolveStatements.spec.ts.snap index 7b1483f80..fe3e1af24 100644 --- a/src/types/__snapshots__/resolveStatements.spec.ts.snap +++ b/src/types/__snapshots__/resolveStatements.spec.ts.snap @@ -430,6 +430,26 @@ Line 9, col 5: " `; +exports[`resolveStatements should fail statements for stmt-let-unknown-type-inference 1`] = ` +":8:5: Cannot infer type for "a" +Line 8, col 5: + 7 | fun test() { +> 8 | let a = null; + ^~~~~~~~~~~~~ + 9 | } +" +`; + +exports[`resolveStatements should fail statements for stmt-let-unknown-type-inference2 1`] = ` +":8:5: Cannot infer type for "a" +Line 8, col 5: + 7 | fun test() { +> 8 | let a = emptyMap(); + ^~~~~~~~~~~~~~~~~~~ + 9 | } +" +`; + exports[`resolveStatements should fail statements for stmt-let-wrong-rhs 1`] = ` ":10:5: Type mismatch: "Int" is not assignable to "Bool" Line 10, col 5: @@ -1412,6 +1432,32 @@ exports[`resolveStatements should resolve statements for stmt-let-if-elseif 1`] ] `; +exports[`resolveStatements should resolve statements for stmt-let-map-type-inference 1`] = ` +[ + [ + "emptyMap()", + "", + ], + [ + "a", + "map", + ], +] +`; + +exports[`resolveStatements should resolve statements for stmt-let-nullable-type-inference 1`] = ` +[ + [ + "null", + "", + ], + [ + "a", + "Int?", + ], +] +`; + exports[`resolveStatements should resolve statements for var-scope-let-toString 1`] = ` [ [ diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index 031df29b7..d5502fe1a 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -170,21 +170,28 @@ function processStatements( // Process expression ctx = resolveExpression(s.expression, sctx, ctx); - // Check type - const expressionType = getExpType(ctx, s.expression); - const variableType = resolveTypeRef(ctx, s.type); - if (!isAssignable(expressionType, variableType)) { - throwError( - `Type mismatch: "${printTypeRef(expressionType)}" is not assignable to "${printTypeRef(variableType)}"`, - s.ref, - ); - } - - // Add variable to statement context + // Check variable name if (sctx.vars.has(s.name)) { throwError(`Variable "${s.name}" already exists`, s.ref); } - sctx = addVariable(s.name, variableType, sctx); + + // Check type + const expressionType = getExpType(ctx, s.expression); + if (s.type !== null) { + const variableType = resolveTypeRef(ctx, s.type); + if (!isAssignable(expressionType, variableType)) { + throwError( + `Type mismatch: "${printTypeRef(expressionType)}" is not assignable to "${printTypeRef(variableType)}"`, + s.ref, + ); + } + sctx = addVariable(s.name, variableType, sctx); + } else { + if (expressionType.kind === "null") { + throwError(`Cannot infer type for "${s.name}"`, s.ref); + } + sctx = addVariable(s.name, expressionType, sctx); + } } else if (s.kind === "statement_assign") { // Process lvalue ctx = resolveLValueRef(s.path, sctx, ctx); diff --git a/src/types/stmts-failed/stmt-let-unknown-type-inference.tact b/src/types/stmts-failed/stmt-let-unknown-type-inference.tact new file mode 100644 index 000000000..cca532547 --- /dev/null +++ b/src/types/stmts-failed/stmt-let-unknown-type-inference.tact @@ -0,0 +1,9 @@ +primitive Int; + +trait BaseTrait { + +} + +fun test() { + let a = null; +} \ No newline at end of file diff --git a/src/types/stmts-failed/stmt-let-unknown-type-inference2.tact b/src/types/stmts-failed/stmt-let-unknown-type-inference2.tact new file mode 100644 index 000000000..b47e754c6 --- /dev/null +++ b/src/types/stmts-failed/stmt-let-unknown-type-inference2.tact @@ -0,0 +1,9 @@ +primitive Int; + +trait BaseTrait { + +} + +fun test() { + let a = emptyMap(); +} \ No newline at end of file diff --git a/src/types/stmts/stmt-let-map-type-inference.tact b/src/types/stmts/stmt-let-map-type-inference.tact new file mode 100644 index 000000000..7b5fb4bd8 --- /dev/null +++ b/src/types/stmts/stmt-let-map-type-inference.tact @@ -0,0 +1,10 @@ +primitive Int; + +trait BaseTrait { + +} + +fun test() { + let a: map = emptyMap(); + let b = a; +} \ No newline at end of file diff --git a/src/types/stmts/stmt-let-nullable-type-inference.tact b/src/types/stmts/stmt-let-nullable-type-inference.tact new file mode 100644 index 000000000..bc57f5de5 --- /dev/null +++ b/src/types/stmts/stmt-let-nullable-type-inference.tact @@ -0,0 +1,10 @@ +primitive Int; + +trait BaseTrait { + +} + +fun test() { + let a: Int? = null; + let b = a; +} \ No newline at end of file diff --git a/tact.config.json b/tact.config.json index acd7ec3e4..11753842d 100644 --- a/tact.config.json +++ b/tact.config.json @@ -293,6 +293,14 @@ "debug": true } }, + { + "name": "local-type-inference", + "path": "./src/test/e2e-emulated/contracts/local-type-inference.tact", + "output": "./src/test/e2e-emulated/contracts/output", + "options": { + "debug": true + } + }, { "name": "benchmark_functions", "path": "./src/benchmarks/contracts/functions.tact",