From 672658f7812e8d0cb16dce938e3229ea8b87e7d9 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 27 Mar 2024 11:39:16 +0300 Subject: [PATCH 01/11] implement local type inference --- src/generator/writers/writeFunction.ts | 24 ++++++++++++++ src/grammar/ast.ts | 13 ++++++++ src/grammar/clone.ts | 5 +++ src/grammar/grammar.ohm | 3 +- src/grammar/grammar.ts | 10 ++++++ .../contracts/local-type-inference.tact | 31 +++++++++++++++++++ .../e2e-emulated/local-type-inference.spec.ts | 27 ++++++++++++++++ src/types/resolveStatements.ts | 12 +++++++ tact.config.json | 8 +++++ 9 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/test/e2e-emulated/contracts/local-type-inference.tact create mode 100644 src/test/e2e-emulated/local-type-inference.spec.ts diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index af556c88d..261cf89fa 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -112,6 +112,30 @@ export function writeStatement( `${resolveFuncType(t, ctx)} ${id(f.name)} = ${writeCastedExpression(f.expression, t, ctx)};`, ); return; + } else if (f.kind === "statement_let_no_type") { + const varType = getExpType(ctx.ctx, f.expression); + + // Contract/struct case + if (varType.kind === "ref") { + const tt = getType(ctx.ctx, varType.name); + if (tt.kind === "contract" || tt.kind === "struct") { + if (varType.optional) { + ctx.append( + `tuple ${id(f.name)} = ${writeCastedExpression(f.expression, varType, ctx)};`, + ); + } else { + ctx.append( + `var ${resolveFuncTypeUnpack(varType, id(f.name), ctx)} = ${writeCastedExpression(f.expression, varType, ctx)};`, + ); + } + return; + } + } + + ctx.append( + `${resolveFuncType(varType, ctx)} ${id(f.name)} = ${writeCastedExpression(f.expression, varType, ctx)};`, + ); + return; } else if (f.kind === "statement_assign") { // Prepare lvalue const path = f.path diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 7b08d3066..7b7d16d62 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -423,6 +423,14 @@ export type ASTStatementLet = { ref: ASTRef; }; +export type ASTStatementLetNoType = { + kind: "statement_let_no_type"; + id: number; + name: string; + expression: ASTExpression; + ref: ASTRef; +}; + export type ASTStatementReturn = { kind: "statement_return"; id: number; @@ -530,6 +538,7 @@ export type ASTStatementForEach = { export type ASTStatement = | ASTStatementLet + | ASTStatementLetNoType | ASTStatementReturn | ASTStatementExpression | ASTStatementAssign @@ -550,6 +559,7 @@ export type ASTNode = | ASTFunction | ASTOpCall | ASTStatementLet + | ASTStatementLetNoType | ASTStatementReturn | ASTProgram | ASTPrimitive @@ -732,6 +742,9 @@ export function traverse(node: ASTNode, callback: (node: ASTNode) => void) { traverse(node.type, callback); traverse(node.expression, callback); } + if (node.kind === "statement_let_no_type") { + traverse(node.expression, callback); + } if (node.kind === "statement_return") { if (node.expression) { traverse(node.expression, callback); diff --git a/src/grammar/clone.ts b/src/grammar/clone.ts index 4f323bf35..3fb669f7e 100644 --- a/src/grammar/clone.ts +++ b/src/grammar/clone.ts @@ -31,6 +31,11 @@ export function cloneNode(src: T): T { type: cloneASTNode(src.type), expression: cloneNode(src.expression), }); + } else if (src.kind === "statement_let_no_type") { + return cloneASTNode({ + ...src, + expression: cloneNode(src.expression), + }); } else if (src.kind === "statement_condition") { return cloneASTNode({ ...src, diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index 2be2d2372..a93383469 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -100,7 +100,8 @@ Tact { StatementBlock = "{" Statement* "}" - StatementLet = let id ":" Type "=" Expression ";" + StatementLet = let id ":" Type "=" Expression ";" --withType + | let id "=" Expression ";" --withoutType StatementReturn = return Expression? ";" diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index cc857cb98..79e8296a3 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -830,6 +830,16 @@ semantics.addOperation("astOfType", { ref: createRef(this), }); }, + StatementLet_withoutType(arg0, arg1, arg2, arg3, arg4) { + checkVariableName(arg1.sourceString, createRef(arg1)); + + return createNode({ + kind: "statement_let_no_type", + name: arg1.sourceString, + expression: arg3.resolve_expression(), + ref: createRef(this), + }); + }, Type_regular(typeId) { return createNode({ kind: "type_ref_simple", 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..56c20b63a --- /dev/null +++ b/src/test/e2e-emulated/contracts/local-type-inference.tact @@ -0,0 +1,31 @@ +import "@stdlib/deploy"; + +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; + } +} \ 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..d44b95d52 --- /dev/null +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -0,0 +1,27 @@ +import { toNano } from '@ton/core'; +import { ContractSystem } from '@tact-lang/emulator'; +import { __DANGER_resetNodeId } from '../grammar/ast'; +import { LocalTypeInferenceTester } from './features/output/local-type-inference_LocalTypeInferenceTester'; + +describe('feature-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()); + const tracker = system.track(contract.address); + 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); + }); +}); \ No newline at end of file diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index 031df29b7..587a213e6 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -185,6 +185,18 @@ function processStatements( throwError(`Variable "${s.name}" already exists`, s.ref); } sctx = addVariable(s.name, variableType, sctx); + } else if (s.kind === "statement_let_no_type") { + // Process expression + ctx = resolveExpression(s.expression, sctx, ctx); + + // Check type + const expressionType = getExpType(ctx, s.expression); + + // Add variable to statement context + if (sctx.vars[s.name]) { + throwError(`Variable already exists: ${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/tact.config.json b/tact.config.json index 43f8f872d..e657354bc 100644 --- a/tact.config.json +++ b/tact.config.json @@ -285,6 +285,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", From 374333b653eb91cfb8c00848c7d17dcaa6adc415 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 27 Mar 2024 11:41:55 +0300 Subject: [PATCH 02/11] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b376658..60fac4f19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,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) ### Fixed From 07bf0e481bd61d4338a87a341e7ef418278539eb Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 27 Mar 2024 11:49:14 +0300 Subject: [PATCH 03/11] fix eslint --- src/grammar/grammar.ts | 30 ++++++++++++------- .../e2e-emulated/local-type-inference.spec.ts | 1 - 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index 79e8296a3..b3fdbdc17 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -571,7 +571,15 @@ semantics.addOperation("astOfDeclaration", { semantics.addOperation("astOfStatement", { // TODO: process StatementBlock - StatementLet(_letKwd, id, _colon, type, _equals, expression, _semicolon) { + StatementLet_withType( + _letKwd, + id, + _colon, + type, + _equals, + expression, + _semicolon, + ) { checkVariableName(id.sourceString, createRef(id)); return createNode({ @@ -582,6 +590,16 @@ semantics.addOperation("astOfStatement", { ref: createRef(this), }); }, + StatementLet_withoutType(_arg0, arg1, _arg2, arg3, _arg4) { + checkVariableName(arg1.sourceString, createRef(arg1)); + + return createNode({ + kind: "statement_let_no_type", + name: arg1.sourceString, + expression: arg3.resolve_expression(), + ref: createRef(this), + }); + }, StatementReturn(_returnKwd, optExpression, _semicolon) { return createNode({ kind: "statement_return", @@ -830,16 +848,6 @@ semantics.addOperation("astOfType", { ref: createRef(this), }); }, - StatementLet_withoutType(arg0, arg1, arg2, arg3, arg4) { - checkVariableName(arg1.sourceString, createRef(arg1)); - - return createNode({ - kind: "statement_let_no_type", - name: arg1.sourceString, - expression: arg3.resolve_expression(), - ref: createRef(this), - }); - }, Type_regular(typeId) { return createNode({ kind: "type_ref_simple", diff --git a/src/test/e2e-emulated/local-type-inference.spec.ts b/src/test/e2e-emulated/local-type-inference.spec.ts index d44b95d52..4eb90af16 100644 --- a/src/test/e2e-emulated/local-type-inference.spec.ts +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -13,7 +13,6 @@ describe('feature-local-type-inference', () => { const system = await ContractSystem.create(); const treasure = system.treasure('treasure'); const contract = system.open(await LocalTypeInferenceTester.fromInit()); - const tracker = system.track(contract.address); await contract.send(treasure, { value: toNano('10') }, { $$type: 'Deploy', queryId: 0n }); await system.run(); From 637339fec0c071d2ea5b84ba7c9817bf0646694c Mon Sep 17 00:00:00 2001 From: Gusarich Date: Tue, 11 Jun 2024 20:12:47 +0300 Subject: [PATCH 04/11] fix --- src/grammar/grammar.ts | 2 +- src/types/resolveStatements.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index b3fdbdc17..4e5eab083 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -596,7 +596,7 @@ semantics.addOperation("astOfStatement", { return createNode({ kind: "statement_let_no_type", name: arg1.sourceString, - expression: arg3.resolve_expression(), + expression: arg3.astOfExpression(), ref: createRef(this), }); }, diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index 587a213e6..e82813a1b 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -193,7 +193,7 @@ function processStatements( const expressionType = getExpType(ctx, s.expression); // Add variable to statement context - if (sctx.vars[s.name]) { + if (sctx.vars.has(s.name)) { throwError(`Variable already exists: ${s.name}`, s.ref); } sctx = addVariable(s.name, expressionType, sctx); From 051319b69e554d8704714910d3bc6739aefedd2d Mon Sep 17 00:00:00 2001 From: Gusarich Date: Tue, 11 Jun 2024 20:31:47 +0300 Subject: [PATCH 05/11] remove separation between statement_let and statement_let_no_type --- src/generator/writers/writeFunction.ts | 30 +++-------------- src/grammar/ast.ts | 16 +-------- src/grammar/clone.ts | 6 ---- src/grammar/grammar.ohm | 3 +- src/grammar/grammar.ts | 22 ++----------- .../e2e-emulated/local-type-inference.spec.ts | 33 +++++++++++-------- src/types/resolveStatements.ts | 32 +++++++----------- 7 files changed, 41 insertions(+), 101 deletions(-) diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index 261cf89fa..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") { @@ -112,30 +116,6 @@ export function writeStatement( `${resolveFuncType(t, ctx)} ${id(f.name)} = ${writeCastedExpression(f.expression, t, ctx)};`, ); return; - } else if (f.kind === "statement_let_no_type") { - const varType = getExpType(ctx.ctx, f.expression); - - // Contract/struct case - if (varType.kind === "ref") { - const tt = getType(ctx.ctx, varType.name); - if (tt.kind === "contract" || tt.kind === "struct") { - if (varType.optional) { - ctx.append( - `tuple ${id(f.name)} = ${writeCastedExpression(f.expression, varType, ctx)};`, - ); - } else { - ctx.append( - `var ${resolveFuncTypeUnpack(varType, id(f.name), ctx)} = ${writeCastedExpression(f.expression, varType, ctx)};`, - ); - } - return; - } - } - - ctx.append( - `${resolveFuncType(varType, ctx)} ${id(f.name)} = ${writeCastedExpression(f.expression, varType, ctx)};`, - ); - return; } else if (f.kind === "statement_assign") { // Prepare lvalue const path = f.path diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 7b7d16d62..8cbef0f38 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -418,15 +418,7 @@ export type ASTStatementLet = { kind: "statement_let"; id: number; name: string; - type: ASTTypeRef; - expression: ASTExpression; - ref: ASTRef; -}; - -export type ASTStatementLetNoType = { - kind: "statement_let_no_type"; - id: number; - name: string; + type: ASTTypeRef | null; expression: ASTExpression; ref: ASTRef; }; @@ -538,7 +530,6 @@ export type ASTStatementForEach = { export type ASTStatement = | ASTStatementLet - | ASTStatementLetNoType | ASTStatementReturn | ASTStatementExpression | ASTStatementAssign @@ -559,7 +550,6 @@ export type ASTNode = | ASTFunction | ASTOpCall | ASTStatementLet - | ASTStatementLetNoType | ASTStatementReturn | ASTProgram | ASTPrimitive @@ -739,10 +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_let_no_type") { traverse(node.expression, callback); } if (node.kind === "statement_return") { diff --git a/src/grammar/clone.ts b/src/grammar/clone.ts index 3fb669f7e..2a2a6cf79 100644 --- a/src/grammar/clone.ts +++ b/src/grammar/clone.ts @@ -26,12 +26,6 @@ export function cloneNode(src: T): T { expression: cloneNode(src.expression), }); } else if (src.kind === "statement_let") { - return cloneASTNode({ - ...src, - type: cloneASTNode(src.type), - expression: cloneNode(src.expression), - }); - } else if (src.kind === "statement_let_no_type") { return cloneASTNode({ ...src, expression: cloneNode(src.expression), diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index a93383469..913812563 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -100,8 +100,7 @@ Tact { StatementBlock = "{" Statement* "}" - StatementLet = let id ":" Type "=" Expression ";" --withType - | let id "=" Expression ";" --withoutType + StatementLet = let id (":" Type)? "=" Expression ";" StatementReturn = return Expression? ";" diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index 4e5eab083..e2e97f7aa 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -571,35 +571,17 @@ semantics.addOperation("astOfDeclaration", { semantics.addOperation("astOfStatement", { // TODO: process StatementBlock - StatementLet_withType( - _letKwd, - id, - _colon, - type, - _equals, - expression, - _semicolon, - ) { + StatementLet(_letKwd, id, _colon, type, _equals, expression, _semicolon) { checkVariableName(id.sourceString, createRef(id)); return createNode({ kind: "statement_let", name: id.sourceString, - type: type.astOfType(), + type: unwrapOptNode(type, (t) => t.astOfType()), expression: expression.astOfExpression(), ref: createRef(this), }); }, - StatementLet_withoutType(_arg0, arg1, _arg2, arg3, _arg4) { - checkVariableName(arg1.sourceString, createRef(arg1)); - - return createNode({ - kind: "statement_let_no_type", - name: arg1.sourceString, - expression: arg3.astOfExpression(), - ref: createRef(this), - }); - }, StatementReturn(_returnKwd, optExpression, _semicolon) { return createNode({ kind: "statement_return", diff --git a/src/test/e2e-emulated/local-type-inference.spec.ts b/src/test/e2e-emulated/local-type-inference.spec.ts index 4eb90af16..b5007e7df 100644 --- a/src/test/e2e-emulated/local-type-inference.spec.ts +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -1,26 +1,33 @@ -import { toNano } from '@ton/core'; -import { ContractSystem } from '@tact-lang/emulator'; -import { __DANGER_resetNodeId } from '../grammar/ast'; -import { LocalTypeInferenceTester } from './features/output/local-type-inference_LocalTypeInferenceTester'; +import { toNano } from "@ton/core"; +import { ContractSystem } from "@tact-lang/emulator"; +import { __DANGER_resetNodeId } from "../grammar/ast"; +import { LocalTypeInferenceTester } from "./features/output/local-type-inference_LocalTypeInferenceTester"; -describe('feature-local-type-inference', () => { +describe("feature-local-type-inference", () => { beforeEach(() => { __DANGER_resetNodeId(); }); - it('should automatically set types for let statements', async () => { - + it("should automatically set types for let statements", async () => { // Init const system = await ContractSystem.create(); - const treasure = system.treasure('treasure'); + const treasure = system.treasure("treasure"); const contract = system.open(await LocalTypeInferenceTester.fromInit()); - await contract.send(treasure, { value: toNano('10') }, { $$type: 'Deploy', queryId: 0n }); + 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.getTest3()).toRawString()).toBe( + contract.address.toRawString(), + ); + expect((await contract.getTest4()).toRawString()).toBe( + contract.address.toRawString(), + ); expect(await contract.getTest5()).toStrictEqual(true); }); -}); \ No newline at end of file +}); diff --git a/src/types/resolveStatements.ts b/src/types/resolveStatements.ts index e82813a1b..ffebbd3fe 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -170,33 +170,25 @@ 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); - } else if (s.kind === "statement_let_no_type") { - // Process expression - ctx = resolveExpression(s.expression, sctx, ctx); // Check type const expressionType = getExpType(ctx, s.expression); - - // Add variable to statement context - if (sctx.vars.has(s.name)) { - throwError(`Variable already exists: ${s.name}`, s.ref); + 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 { + sctx = addVariable(s.name, expressionType, sctx); } - sctx = addVariable(s.name, expressionType, sctx); } else if (s.kind === "statement_assign") { // Process lvalue ctx = resolveLValueRef(s.path, sctx, ctx); From dd61f960fb332f8969637a507231bfddef80424d Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 12 Jun 2024 10:00:04 +0300 Subject: [PATCH 06/11] fix --- .../local-type-inference.spec.ts.snap | 318 ++++++++++++++++++ .../e2e-emulated/local-type-inference.spec.ts | 6 +- 2 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap 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..3793a9aaa --- /dev/null +++ b/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap @@ -0,0 +1,318 @@ +// 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", + }, + }, + ], + "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", + }, + ], +} +`; diff --git a/src/test/e2e-emulated/local-type-inference.spec.ts b/src/test/e2e-emulated/local-type-inference.spec.ts index b5007e7df..ee53fc386 100644 --- a/src/test/e2e-emulated/local-type-inference.spec.ts +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -1,9 +1,9 @@ import { toNano } from "@ton/core"; import { ContractSystem } from "@tact-lang/emulator"; -import { __DANGER_resetNodeId } from "../grammar/ast"; -import { LocalTypeInferenceTester } from "./features/output/local-type-inference_LocalTypeInferenceTester"; +import { __DANGER_resetNodeId } from "../../grammar/ast"; +import { LocalTypeInferenceTester } from "./contracts/output/local-type-inference_LocalTypeInferenceTester"; -describe("feature-local-type-inference", () => { +describe("local-type-inference", () => { beforeEach(() => { __DANGER_resetNodeId(); }); From 6a4b694aff9743b5233aa843a0eb4a22e58340c6 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 13 Jun 2024 18:27:04 +0300 Subject: [PATCH 07/11] add more tests --- .../local-type-inference.spec.ts.snap | 145 ++++++++++++++++++ .../contracts/local-type-inference.tact | 79 ++++++++++ .../e2e-emulated/local-type-inference.spec.ts | 35 ++++- 3 files changed, 258 insertions(+), 1 deletion(-) 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 index 3793a9aaa..c85ca25cd 100644 --- a/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap +++ b/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap @@ -124,6 +124,127 @@ exports[`local-type-inference should automatically set types for let statements "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", + }, + }, ], "receivers": [ { @@ -313,6 +434,30 @@ exports[`local-type-inference should automatically set types for let statements "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 index 56c20b63a..95c7032d6 100644 --- a/src/test/e2e-emulated/contracts/local-type-inference.tact +++ b/src/test/e2e-emulated/contracts/local-type-inference.tact @@ -1,5 +1,10 @@ import "@stdlib/deploy"; +struct MyStruct { + x: Int; + y: Int; +} + contract LocalTypeInferenceTester with Deployable { get fun test1(): Int { let x = 1; @@ -28,4 +33,78 @@ contract LocalTypeInferenceTester with Deployable { 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; + } } \ 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 index ee53fc386..e80f04148 100644 --- a/src/test/e2e-emulated/local-type-inference.spec.ts +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -1,4 +1,4 @@ -import { toNano } from "@ton/core"; +import { Dictionary, 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"; @@ -29,5 +29,38 @@ describe("local-type-inference", () => { 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); }); }); From ed78feaea2c4a0d58fea024c99c45537dc6c4936 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 13 Jun 2024 18:41:57 +0300 Subject: [PATCH 08/11] add throwing for `null` type inference attempt and cover that with tests --- .../local-type-inference.spec.ts.snap | 10 ++++ .../contracts/local-type-inference.tact | 6 +++ .../e2e-emulated/local-type-inference.spec.ts | 1 + .../resolveStatements.spec.ts.snap | 46 +++++++++++++++++++ src/types/resolveStatements.ts | 3 ++ .../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 ++++ 9 files changed, 104 insertions(+) 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/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap b/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap index c85ca25cd..f08d8ccac 100644 --- a/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap +++ b/src/test/e2e-emulated/__snapshots__/local-type-inference.spec.ts.snap @@ -245,6 +245,16 @@ exports[`local-type-inference should automatically set types for let statements "type": "int", }, }, + { + "arguments": [], + "name": "test19", + "returnType": { + "format": 257, + "kind": "simple", + "optional": true, + "type": "int", + }, + }, ], "receivers": [ { diff --git a/src/test/e2e-emulated/contracts/local-type-inference.tact b/src/test/e2e-emulated/contracts/local-type-inference.tact index 95c7032d6..dfa631f21 100644 --- a/src/test/e2e-emulated/contracts/local-type-inference.tact +++ b/src/test/e2e-emulated/contracts/local-type-inference.tact @@ -107,4 +107,10 @@ contract LocalTypeInferenceTester with Deployable { 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 index e80f04148..15a795517 100644 --- a/src/test/e2e-emulated/local-type-inference.spec.ts +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -62,5 +62,6 @@ describe("local-type-inference", () => { 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 ffebbd3fe..d5502fe1a 100644 --- a/src/types/resolveStatements.ts +++ b/src/types/resolveStatements.ts @@ -187,6 +187,9 @@ function processStatements( } 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") { 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 From d689d9cb440911192c0f6508fe1a0e433738d494 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 13 Jun 2024 18:42:59 +0300 Subject: [PATCH 09/11] resolve merge conflict in changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60fac4f19..b56610730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ 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) ### Fixed From f7357c4d9a83b1e7767b2981e072d21daa2f5be8 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 13 Jun 2024 18:48:21 +0300 Subject: [PATCH 10/11] fix eslint --- src/test/e2e-emulated/local-type-inference.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/e2e-emulated/local-type-inference.spec.ts b/src/test/e2e-emulated/local-type-inference.spec.ts index 15a795517..b6a986d07 100644 --- a/src/test/e2e-emulated/local-type-inference.spec.ts +++ b/src/test/e2e-emulated/local-type-inference.spec.ts @@ -1,4 +1,4 @@ -import { Dictionary, beginCell, toNano } from "@ton/core"; +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"; From aaf4bd184a8c13dcbb15c6ec6ba880e7cb985761 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Fri, 14 Jun 2024 10:09:24 +0300 Subject: [PATCH 11/11] fix naming for StatementLet and add ast type refs cloning --- src/grammar/clone.ts | 4 ++++ src/grammar/grammar.ts | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/grammar/clone.ts b/src/grammar/clone.ts index 2a2a6cf79..bcea020c8 100644 --- a/src/grammar/clone.ts +++ b/src/grammar/clone.ts @@ -28,6 +28,7 @@ export function cloneNode(src: T): T { } else if (src.kind === "statement_let") { return cloneASTNode({ ...src, + type: src.type ? cloneASTNode(src.type) : null, expression: cloneNode(src.expression), }); } else if (src.kind === "statement_condition") { @@ -131,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") { @@ -157,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.ts b/src/grammar/grammar.ts index e2e97f7aa..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: unwrapOptNode(type, (t) => t.astOfType()), + type: unwrapOptNode(optType, (t) => t.astOfType()), expression: expression.astOfExpression(), ref: createRef(this), });