diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9378fa4..53788537f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `sha256()` function no longer throws on statically known strings of any length: PR [#907](https://github.com/tact-lang/tact/pull/907) - TypeScript wrappers generation for messages with single quote: PR [#1106](https://github.com/tact-lang/tact/pull/1106) - `foreach` loops now properly handle `as coins` map value serialization type: PR [#1186](https://github.com/tact-lang/tact/pull/1186) +- The typechecker now rejects integer map key types with variable width (`coins`, `varint16`, `varint32`, `varuint16`, `varuint32`): PR [#1276](https://github.com/tact-lang/tact/pull/1276) ### Docs diff --git a/src/abi/map.ts b/src/abi/map.ts index 2c0332b1f..d2c52c783 100644 --- a/src/abi/map.ts +++ b/src/abi/map.ts @@ -74,21 +74,27 @@ function checkValueType( } function resolveMapKeyBits( - self: { key: string; keyAs: string | null }, - ref: SrcInfo, + type: { key: string; keyAs: string | null }, + loc: SrcInfo, ): { bits: number; kind: string } { - if (self.key === "Int") { - if (self.keyAs?.startsWith("int")) { - return { bits: parseInt(self.keyAs.slice(3), 10), kind: "int" }; + if (type.key === "Int") { + if (type.keyAs === null) { + return { bits: 257, kind: "int" }; // Default for "Int" keys + } + if (type.keyAs.startsWith("int")) { + return { bits: parseInt(type.keyAs.slice(3), 10), kind: "int" }; } - if (self.keyAs?.startsWith("uint")) { - return { bits: parseInt(self.keyAs.slice(4), 10), kind: "uint" }; + if (type.keyAs.startsWith("uint")) { + return { bits: parseInt(type.keyAs.slice(4), 10), kind: "uint" }; } - return { bits: 257, kind: "int" }; // Default for "Int" keys - } else if (self.key === "Address") { + throwCompilationError( + `Unsupported integer map key type storage annotation: ${type.keyAs}`, + loc, + ); + } else if (type.key === "Address") { return { bits: 267, kind: "slice" }; } - throwCompilationError(`Unsupported key type: ${self.key}`, ref); + throwCompilationError(`Unsupported key type: ${type.key}`, loc); } function handleStructOrOtherValue( diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap index 1b1cd6331..976d617ec 100644 --- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap +++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap @@ -672,6 +672,56 @@ Line 6, col 29: " `; +exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-coins 1`] = ` +":6:19: "coins" is invalid as-annotation for map key type "Int" +Line 6, col 19: + 5 | contract Test { +> 6 | m: map = emptyMap(); + ^~~~~ + 7 | } +" +`; + +exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-varint16 1`] = ` +":6:19: "varint16" is invalid as-annotation for map key type "Int" +Line 6, col 19: + 5 | contract Test { +> 6 | m: map = emptyMap(); + ^~~~~~~~ + 7 | } +" +`; + +exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-varint32 1`] = ` +":6:19: "varint32" is invalid as-annotation for map key type "Int" +Line 6, col 19: + 5 | contract Test { +> 6 | m: map = emptyMap(); + ^~~~~~~~ + 7 | } +" +`; + +exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-varuint16 1`] = ` +":6:19: "varuint16" is invalid as-annotation for map key type "Int" +Line 6, col 19: + 5 | contract Test { +> 6 | m: map = emptyMap(); + ^~~~~~~~~ + 7 | } +" +`; + +exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-varuint32 1`] = ` +":6:19: "varuint32" is invalid as-annotation for map key type "Int" +Line 6, col 19: + 5 | contract Test { +> 6 | m: map = emptyMap(); + ^~~~~~~~~ + 7 | } +" +`; + exports[`resolveDescriptors should fail descriptors for wf-type-fun-param 1`] = ` ":4:16: Invalid map type. Check https://docs.tact-lang.org/book/maps#allowed-types Line 4, col 16: diff --git a/src/types/resolveABITypeRef.ts b/src/types/resolveABITypeRef.ts index b343e421f..b2cf8e731 100644 --- a/src/types/resolveABITypeRef.ts +++ b/src/types/resolveABITypeRef.ts @@ -29,15 +29,17 @@ type FormatDef = Record< >; const uintOptions: FormatDef = Object.fromEntries( - [...Array(257).keys()] - .slice(1) - .map((key) => [`uint${key}`, { type: "uint", format: key }]), + [...Array(256).keys()].map((key) => [ + `uint${key + 1}`, + { type: "uint", format: key + 1 }, + ]), ); const intOptions: FormatDef = Object.fromEntries( - [...Array(257).keys()] - .slice(1) - .map((key) => [`int${key}`, { type: "int", format: key }]), + [...Array(256).keys()].map((key) => [ + `int${key + 1}`, + { type: "int", format: key + 1 }, + ]), ); const intFormats: FormatDef = { @@ -51,7 +53,14 @@ const intFormats: FormatDef = { varuint32: { type: "uint", format: "varuint32" }, }; -export const intMapFormats: FormatDef = { ...intFormats }; +// only fixed-width integer map keys are supported +export const intMapKeyFormats: FormatDef = { + ...uintOptions, + ...intOptions, + int257: { type: "int", format: 257 }, +}; + +export const intMapValFormats: FormatDef = { ...intFormats }; const cellFormats: FormatDef = { remaining: { type: "cell", format: "remainder" }, @@ -259,8 +268,9 @@ export function resolveABIType(src: AstFieldDecl): ABITypeRef { if (isInt(src.type.keyType)) { key = "int"; if (src.type.keyStorageType) { - const format = intMapFormats[idText(src.type.keyStorageType)]; - if (!format || format.format === "coins") { + const format = + intMapKeyFormats[idText(src.type.keyStorageType)]; + if (!format) { throwCompilationError( `Unsupported format ${idTextErr(src.type.keyStorageType)} for map key`, src.loc, @@ -288,7 +298,8 @@ export function resolveABIType(src: AstFieldDecl): ABITypeRef { if (isInt(src.type.valueType)) { value = "int"; if (src.type.valueStorageType) { - const format = intMapFormats[idText(src.type.valueStorageType)]; + const format = + intMapValFormats[idText(src.type.valueStorageType)]; if (!format) { throwCompilationError( `Unsupported format ${idText(src.type.valueStorageType)} for map value`, @@ -424,8 +435,8 @@ export function createABITypeRefFromTypeRef( if (src.key === "Int") { key = "int"; if (src.keyAs) { - const format = intMapFormats[src.keyAs]; - if (!format || src.keyAs === "coins") { + const format = intMapKeyFormats[src.keyAs]; + if (!format) { throwCompilationError( `Unsupported format ${src.keyAs} for map key`, loc, @@ -450,7 +461,7 @@ export function createABITypeRefFromTypeRef( if (src.value === "Int") { value = "int"; if (src.valueAs) { - const format = intMapFormats[src.valueAs]; + const format = intMapValFormats[src.valueAs]; if (!format) { throwCompilationError( `Unsupported format ${src.valueAs} for map value`, diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index bd5976596..2f01f4c9e 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -45,7 +45,11 @@ import { cloneNode } from "../grammar/clone"; import { crc16 } from "../utils/crc16"; import { isSubsetOf } from "../utils/isSubsetOf"; import { evalConstantExpression } from "../constEval"; -import { resolveABIType, intMapFormats } from "./resolveABITypeRef"; +import { + resolveABIType, + intMapKeyFormats, + intMapValFormats, +} from "./resolveABITypeRef"; import { enabledExternals } from "../config/features"; import { isRuntimeType } from "./isRuntimeType"; import { GlobalFunctions } from "../abi/global"; @@ -62,17 +66,28 @@ const staticConstantsStore = createContextStore(); function verifyMapAsAnnotationsForPrimitiveTypes( type: AstTypeId, asAnnotation: AstId | null, + kind: "keyType" | "valType", ): void { switch (idText(type)) { case "Int": { - if ( - asAnnotation !== null && - !Object.keys(intMapFormats).includes(idText(asAnnotation)) - ) { - throwCompilationError( - 'Invalid `as`-annotation for type "Int" type', - asAnnotation.loc, - ); + if (asAnnotation === null) return; + const ann = idText(asAnnotation); + switch (kind) { + case "keyType": + if (!Object.keys(intMapKeyFormats).includes(ann)) { + throwCompilationError( + `"${ann}" is invalid as-annotation for map key type "Int"`, + asAnnotation.loc, + ); + } + return; + case "valType": + if (!Object.keys(intMapValFormats).includes(ann)) { + throwCompilationError( + `"${ann}" is invalid as-annotation for map value type "Int"`, + asAnnotation.loc, + ); + } } return; } @@ -97,6 +112,7 @@ function verifyMapTypes( typeId: AstTypeId, asAnnotation: AstId | null, allowedTypeNames: string[], + kind: "keyType" | "valType", ): void { if (!allowedTypeNames.includes(idText(typeId))) { throwCompilationError( @@ -104,26 +120,31 @@ function verifyMapTypes( typeId.loc, ); } - verifyMapAsAnnotationsForPrimitiveTypes(typeId, asAnnotation); + verifyMapAsAnnotationsForPrimitiveTypes(typeId, asAnnotation, kind); } 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"]); + verifyMapTypes( + mapTy.keyType, + mapTy.keyStorageType, + ["Int", "Address"], + "keyType", + ); // 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", - ]); + verifyMapTypes( + mapTy.valueType, + mapTy.valueStorageType, + ["Int", "Address", "Bool", "Cell"], + "valType", + ); } export const toBounced = (type: string) => `${type}%%BOUNCED%%`; diff --git a/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-coins.tact b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-coins.tact new file mode 100644 index 000000000..e309de586 --- /dev/null +++ b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-coins.tact @@ -0,0 +1,8 @@ +primitive Int; +primitive Address; +trait BaseTrait {} + +contract Test { + m: map = emptyMap(); +} + diff --git a/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varint16.tact b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varint16.tact new file mode 100644 index 000000000..80f11f809 --- /dev/null +++ b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varint16.tact @@ -0,0 +1,8 @@ +primitive Int; +primitive Address; +trait BaseTrait {} + +contract Test { + m: map = emptyMap(); +} + diff --git a/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varint32.tact b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varint32.tact new file mode 100644 index 000000000..d5ea502d0 --- /dev/null +++ b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varint32.tact @@ -0,0 +1,8 @@ +primitive Int; +primitive Address; +trait BaseTrait {} + +contract Test { + m: map = emptyMap(); +} + diff --git a/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varuint16.tact b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varuint16.tact new file mode 100644 index 000000000..56830db68 --- /dev/null +++ b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varuint16.tact @@ -0,0 +1,8 @@ +primitive Int; +primitive Address; +trait BaseTrait {} + +contract Test { + m: map = emptyMap(); +} + diff --git a/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varuint32.tact b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varuint32.tact new file mode 100644 index 000000000..ead6afbb7 --- /dev/null +++ b/src/types/test-failed/wf-type-contract-incorrect-map-key-annotation-varuint32.tact @@ -0,0 +1,8 @@ +primitive Int; +primitive Address; +trait BaseTrait {} + +contract Test { + m: map = emptyMap(); +} +