Skip to content

Commit

Permalink
Merge branch 'main' into value-type
Browse files Browse the repository at this point in the history
  • Loading branch information
jeshecdom committed Jan 1, 2025
2 parents 910d36b + 06cb9f4 commit 6ef35d7
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 40 deletions.
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 @@ -46,7 +46,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 @@ -66,17 +70,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 @@ -101,33 +116,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();
}

0 comments on commit 6ef35d7

Please sign in to comment.