diff --git a/CHANGELOG.md b/CHANGELOG.md index 31897fa99..f4073ddb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Returning `self` from getters is now allowed: PR [#666](https://github.com/tact-lang/tact/pull/666) - Remainder fields in the middle of a struct are now forbidden: PR [#697](https://github.com/tact-lang/tact/pull/697) - Defining two native functions from the same FunC function now does not fail compilation: PR [#699](https://github.com/tact-lang/tact/pull/699) +- Map types are checked for well-formedness in all type ascriptions: PR [#704](https://github.com/tact-lang/tact/pull/704) ## [1.4.3] - 2024-08-16 diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index ba5ec2543..6042ee405 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -486,6 +486,56 @@ Line 7, col 1: " `; +exports[`resolveDescriptors should fail descriptors for wf-type-const 1`] = ` +":4:19: Invalid key type for map. Check https://docs.tact-lang.org/book/maps#allowed-types +Line 4, col 19: + 3 | +> 4 | const m: map = null; + ^~~~~~ + 5 | +" +`; + +exports[`resolveDescriptors should fail descriptors for wf-type-contract-const 1`] = ` +":6:23: Invalid key type for map. Check https://docs.tact-lang.org/book/maps#allowed-types +Line 6, col 23: + 5 | contract Test { +> 6 | const m: map = null; + ^~~~~~ + 7 | } +" +`; + +exports[`resolveDescriptors should fail descriptors for wf-type-contract-const-incorrect-annotation 1`] = ` +":6:34: "Address" type cannot have as-annotation +Line 6, col 34: + 5 | contract Test { +> 6 | const m: map = null; + ^~~~~~ + 7 | } +" +`; + +exports[`resolveDescriptors should fail descriptors for wf-type-contract-getter 1`] = ` +":6:29: Invalid key type for map. Check https://docs.tact-lang.org/book/maps#allowed-types +Line 6, col 29: + 5 | contract Test { +> 6 | get fun foo(): map { + ^~~~~~ + 7 | let m: map = null; +" +`; + +exports[`resolveDescriptors should fail descriptors for wf-type-fun-param 1`] = ` +":4:16: Invalid key type for map. Check https://docs.tact-lang.org/book/maps#allowed-types +Line 4, col 16: + 3 | +> 4 | fun foo(m: map) { } + ^~~~ + 5 | +" +`; + exports[`resolveDescriptors should resolve descriptors for const-decl-struct-with-default-field 1`] = ` { "BaseTrait": { diff --git a/src/types/__snapshots__/resolveStatements.spec.ts.snap b/src/types/__snapshots__/resolveStatements.spec.ts.snap index 361d7ba90..67b8e3082 100644 --- a/src/types/__snapshots__/resolveStatements.spec.ts.snap +++ b/src/types/__snapshots__/resolveStatements.spec.ts.snap @@ -1205,6 +1205,16 @@ Line 9, col 12: " `; +exports[`resolveStatements should fail statements for wf-type-let 1`] = ` +":5:16: Invalid key type for map. Check https://docs.tact-lang.org/book/maps#allowed-types +Line 5, col 16: + 4 | fun foo() { +> 5 | let m: map = null; + ^~~~~~ + 6 | } +" +`; + exports[`resolveStatements should resolve statements for assign-self-mutating-method 1`] = ` [ [ diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index d7d5b7161..13faf3f0d 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -4,7 +4,6 @@ import { AstContractInit, AstNativeFunctionDecl, AstNode, - SrcInfo, AstType, createAstNode, idText, @@ -16,6 +15,8 @@ import { AstFunctionDecl, AstConstantDecl, AstExpression, + AstMapType, + AstTypeId, } from "../grammar/ast"; import { traverse } from "../grammar/iterators"; import { @@ -55,21 +56,15 @@ const store = createContextStore(); const staticFunctionsStore = createContextStore(); const staticConstantsStore = createContextStore(); -function verifyMapType( - key: string, - keyAs: AstId | null, - value: string, - valueAs: AstId | null, - loc: SrcInfo, -) { - if (!keyAs && !valueAs) { - return; - } - - // keyAs - if (keyAs) { - if (key === "Int") { +// this function does not handle the case of structs +function verifyMapAsAnnotationsForPrimitiveTypes( + type: AstTypeId, + asAnnotation: AstId | null, +): void { + switch (idText(type)) { + case "Int": { if ( + asAnnotation !== null && ![ "int8", "int16", @@ -84,50 +79,72 @@ function verifyMapType( "uint64", "uint128", "uint256", - ].includes(idText(keyAs)) + "coins", + ].includes(idText(asAnnotation)) ) { - throwCompilationError("Invalid key type for map", loc); + throwCompilationError( + 'Invalid `as`-annotation for type "Int" type', + asAnnotation.loc, + ); } - } else { - throwCompilationError("Invalid key type for map", loc); + return; } - } - - // valueAs - if (valueAs) { - if (value === "Int") { - if ( - ![ - "int8", - "int16", - "int32", - "int64", - "int128", - "int256", - "int257", - "uint8", - "uint16", - "uint32", - "uint64", - "uint128", - "uint256", - "coins", - ].includes(idText(valueAs)) - ) { - throwCompilationError("Invalid value type for map", loc); + case "Address": + case "Bool": + case "Cell": { + if (asAnnotation !== null) { + throwCompilationError( + `${idTextErr(type)} type cannot have as-annotation`, + asAnnotation.loc, + ); } - } else { - throwCompilationError("Invalid value type for map", loc); + return; + } + default: { + throwInternalCompilerError("Unsupported map type", type.loc); } } } +function verifyMapTypes( + typeId: AstTypeId, + asAnnotation: AstId | null, + allowedTypeNames: string[], +): void { + if (!allowedTypeNames.includes(idText(typeId))) { + throwCompilationError( + "Invalid key type for map. Check https://docs.tact-lang.org/book/maps#allowed-types", + typeId.loc, + ); + } + verifyMapAsAnnotationsForPrimitiveTypes(typeId, asAnnotation); +} + +function verifyMapType(mapTy: AstMapType, isValTypeStruct: boolean) { + // optional and other compound key and value types are disallowed at the level of grammar + + // check allowed key types + verifyMapTypes(mapTy.keyType, mapTy.keyStorageType, ["Int", "Address"]); + + // check allowed value types + if (isValTypeStruct && mapTy.valueStorageType === null) { + return; + } + // the case for struct/message is already checked + verifyMapTypes(mapTy.valueType, mapTy.valueStorageType, [ + "Int", + "Address", + "Bool", + "Cell", + ]); +} + export const toBounced = (type: string) => `${type}%%BOUNCED%%`; -export function resolveTypeRef(ctx: CompilerContext, src: AstType): TypeRef { - switch (src.kind) { +export function resolveTypeRef(ctx: CompilerContext, type: AstType): TypeRef { + switch (type.kind) { case "type_id": { - const t = getType(ctx, idText(src)); + const t = getType(ctx, idText(type)); return { kind: "ref", name: t.name, @@ -135,13 +152,13 @@ export function resolveTypeRef(ctx: CompilerContext, src: AstType): TypeRef { }; } case "optional_type": { - if (src.typeArg.kind !== "type_id") { + if (type.typeArg.kind !== "type_id") { throwInternalCompilerError( "Only optional type identifiers are supported now", - src.typeArg.loc, + type.typeArg.loc, ); } - const t = getType(ctx, idText(src.typeArg)); + const t = getType(ctx, idText(type.typeArg)); return { kind: "ref", name: t.name, @@ -149,31 +166,25 @@ export function resolveTypeRef(ctx: CompilerContext, src: AstType): TypeRef { }; } case "map_type": { - const k = getType(ctx, idText(src.keyType)).name; - const v = getType(ctx, idText(src.valueType)).name; - verifyMapType( - k, - src.keyStorageType, - v, - src.valueStorageType, - src.loc, - ); + const keyTy = getType(ctx, idText(type.keyType)); + const valTy = getType(ctx, idText(type.valueType)); + verifyMapType(type, valTy.kind === "struct"); return { kind: "map", - key: k, + key: keyTy.name, keyAs: - src.keyStorageType !== null - ? idText(src.keyStorageType) + type.keyStorageType !== null + ? idText(type.keyStorageType) : null, - value: v, + value: valTy.name, valueAs: - src.valueStorageType !== null - ? idText(src.valueStorageType) + type.valueStorageType !== null + ? idText(type.valueStorageType) : null, }; } case "bounced_message_type": { - const t = getType(ctx, idText(src.messageType)); + const t = getType(ctx, idText(type.messageType)); return { kind: "ref_bounced", name: t.name, @@ -183,73 +194,75 @@ export function resolveTypeRef(ctx: CompilerContext, src: AstType): TypeRef { } function buildTypeRef( - src: AstType, + type: AstType, types: Map, ): TypeRef { - switch (src.kind) { + switch (type.kind) { case "type_id": { - if (!types.has(idText(src))) { + if (!types.has(idText(type))) { throwCompilationError( - `Type ${idTextErr(src)} not found`, - src.loc, + `Type ${idTextErr(type)} not found`, + type.loc, ); } return { kind: "ref", - name: idText(src), + name: idText(type), optional: false, }; } case "optional_type": { - if (src.typeArg.kind !== "type_id") { + if (type.typeArg.kind !== "type_id") { throwInternalCompilerError( "Only optional type identifiers are supported now", - src.typeArg.loc, + type.typeArg.loc, ); } - if (!types.has(idText(src.typeArg))) { + if (!types.has(idText(type.typeArg))) { throwCompilationError( - `Type ${idTextErr(src.typeArg)} not found`, - src.loc, + `Type ${idTextErr(type.typeArg)} not found`, + type.loc, ); } return { kind: "ref", - name: idText(src.typeArg), + name: idText(type.typeArg), optional: true, }; } case "map_type": { - if (!types.has(idText(src.keyType))) { + if (!types.has(idText(type.keyType))) { throwCompilationError( - `Type ${idTextErr(src.keyType)} not found`, - src.loc, + `Type ${idTextErr(type.keyType)} not found`, + type.loc, ); } - if (!types.has(idText(src.valueType))) { + if (!types.has(idText(type.valueType))) { throwCompilationError( - `Type ${idTextErr(src.valueType)} not found`, - src.loc, + `Type ${idTextErr(type.valueType)} not found`, + type.loc, ); } + const valTy = types.get(idText(type.valueType))!; + verifyMapType(type, valTy.kind === "struct"); return { kind: "map", - key: idText(src.keyType), + key: idText(type.keyType), keyAs: - src.keyStorageType !== null - ? idText(src.keyStorageType) + type.keyStorageType !== null + ? idText(type.keyStorageType) : null, - value: idText(src.valueType), + value: idText(type.valueType), valueAs: - src.valueStorageType !== null - ? idText(src.valueStorageType) + type.valueStorageType !== null + ? idText(type.valueStorageType) : null, }; } case "bounced_message_type": { return { kind: "ref_bounced", - name: idText(src.messageType), + name: idText(type.messageType), }; } } @@ -1815,7 +1828,7 @@ export function resolveDescriptors(ctx: CompilerContext) { export function getType( ctx: CompilerContext, - ident: AstId | string, + ident: AstId | AstTypeId | string, ): TypeDescription { const name = typeof ident === "string" ? ident : idText(ident); const r = store.get(ctx, name); diff --git a/src/types/stmts-failed/wf-type-let.tact b/src/types/stmts-failed/wf-type-let.tact new file mode 100644 index 000000000..98bb00982 --- /dev/null +++ b/src/types/stmts-failed/wf-type-let.tact @@ -0,0 +1,6 @@ +primitive String; +primitive Int; + +fun foo() { + let m: map = null; +} diff --git a/src/types/test-failed/wf-type-const.tact b/src/types/test-failed/wf-type-const.tact new file mode 100644 index 000000000..f10321c29 --- /dev/null +++ b/src/types/test-failed/wf-type-const.tact @@ -0,0 +1,5 @@ +primitive Int; +primitive String; + +const m: map = null; + diff --git a/src/types/test-failed/wf-type-contract-const-incorrect-annotation.tact b/src/types/test-failed/wf-type-contract-const-incorrect-annotation.tact new file mode 100644 index 000000000..a99e36c5b --- /dev/null +++ b/src/types/test-failed/wf-type-contract-const-incorrect-annotation.tact @@ -0,0 +1,8 @@ +primitive Int; +primitive Address; +trait BaseTrait {} + +contract Test { + const m: map = null; +} + diff --git a/src/types/test-failed/wf-type-contract-const.tact b/src/types/test-failed/wf-type-contract-const.tact new file mode 100644 index 000000000..a14765ee5 --- /dev/null +++ b/src/types/test-failed/wf-type-contract-const.tact @@ -0,0 +1,8 @@ +primitive Int; +primitive String; +trait BaseTrait {} + +contract Test { + const m: map = null; +} + diff --git a/src/types/test-failed/wf-type-contract-getter.tact b/src/types/test-failed/wf-type-contract-getter.tact new file mode 100644 index 000000000..354019b74 --- /dev/null +++ b/src/types/test-failed/wf-type-contract-getter.tact @@ -0,0 +1,11 @@ +primitive Int; +primitive String; +trait BaseTrait {} + +contract Test { + get fun foo(): map { + let m: map = null; + return m; + } +} + diff --git a/src/types/test-failed/wf-type-fun-param.tact b/src/types/test-failed/wf-type-fun-param.tact new file mode 100644 index 000000000..2a2c5dfd1 --- /dev/null +++ b/src/types/test-failed/wf-type-fun-param.tact @@ -0,0 +1,5 @@ +primitive Bool; +primitive Int; + +fun foo(m: map) { } +