Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: do not allow variable width integer map key types #1276

Merged
merged 2 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 16 additions & 10 deletions src/abi/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
50 changes: 50 additions & 0 deletions src/types/__snapshots__/resolveDescriptors.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,56 @@ Line 6, col 29:
"
`;

exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-coins 1`] = `
"<unknown>:6:19: "coins" is invalid as-annotation for map key type "Int"
Line 6, col 19:
5 | contract Test {
> 6 | m: map<Int as coins, Address> = emptyMap();
^~~~~
7 | }
"
`;

exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-varint16 1`] = `
"<unknown>:6:19: "varint16" is invalid as-annotation for map key type "Int"
Line 6, col 19:
5 | contract Test {
> 6 | m: map<Int as varint16, Address> = emptyMap();
^~~~~~~~
7 | }
"
`;

exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-varint32 1`] = `
"<unknown>:6:19: "varint32" is invalid as-annotation for map key type "Int"
Line 6, col 19:
5 | contract Test {
> 6 | m: map<Int as varint32, Address> = emptyMap();
^~~~~~~~
7 | }
"
`;

exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-varuint16 1`] = `
"<unknown>:6:19: "varuint16" is invalid as-annotation for map key type "Int"
Line 6, col 19:
5 | contract Test {
> 6 | m: map<Int as varuint16, Address> = emptyMap();
^~~~~~~~~
7 | }
"
`;

exports[`resolveDescriptors should fail descriptors for wf-type-contract-incorrect-map-key-annotation-varuint32 1`] = `
"<unknown>:6:19: "varuint32" is invalid as-annotation for map key type "Int"
Line 6, col 19:
5 | contract Test {
> 6 | m: map<Int as varuint32, Address> = emptyMap();
^~~~~~~~~
7 | }
"
`;

exports[`resolveDescriptors should fail descriptors for wf-type-fun-param 1`] = `
"<unknown>:4:16: Invalid map type. Check https://docs.tact-lang.org/book/maps#allowed-types
Line 4, col 16:
Expand Down
37 changes: 24 additions & 13 deletions src/types/resolveABITypeRef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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" },
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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,
Expand All @@ -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`,
Expand Down
55 changes: 38 additions & 17 deletions src/types/resolveDescriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -62,17 +66,28 @@ const staticConstantsStore = createContextStore<ConstantDescription>();
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;
}
Expand All @@ -97,33 +112,39 @@ function verifyMapTypes(
typeId: AstTypeId,
asAnnotation: AstId | null,
allowedTypeNames: string[],
kind: "keyType" | "valType",
): void {
if (!allowedTypeNames.includes(idText(typeId))) {
throwCompilationError(
"Invalid map type. Check https://docs.tact-lang.org/book/maps#allowed-types",
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%%`;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
primitive Int;
primitive Address;
trait BaseTrait {}

contract Test {
m: map<Int as coins, Address> = emptyMap();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
primitive Int;
primitive Address;
trait BaseTrait {}

contract Test {
m: map<Int as varint16, Address> = emptyMap();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
primitive Int;
primitive Address;
trait BaseTrait {}

contract Test {
m: map<Int as varint32, Address> = emptyMap();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
primitive Int;
primitive Address;
trait BaseTrait {}

contract Test {
m: map<Int as varuint16, Address> = emptyMap();
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
primitive Int;
primitive Address;
trait BaseTrait {}

contract Test {
m: map<Int as varuint32, Address> = emptyMap();
}

Loading