Skip to content

Commit c5bdc2d

Browse files
authored
fix: error message when key or value in map literal is out of serialization range (#3285)
1 parent 7f0e7b5 commit c5bdc2d

11 files changed

+210
-14
lines changed

dev-docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727

2828
### Language features
2929

30+
- [fix] Better error message when an integer key or value in a map literal is out of its serialization range: PR [#3285](https://github.com/tact-lang/tact/pull/3285)
3031
- [fix] Balanced quotation in error messages for out-of-project-root imports: PR [#3242](https://github.com/tact-lang/tact/pull/3242)
3132
- [fix] Disallow self-inheritance for contracts and traits: PR [#3094](https://github.com/tact-lang/tact/pull/3094)
3233
- [fix] Added fixed-bytes support to bounced message size calculations: PR [#3129](https://github.com/tact-lang/tact/pull/3129)

src/generator/writers/writeExpression.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ const writeMapLiteral =
835835
(node: Ast.MapLiteral) =>
836836
(_ctx: WriterContext): string => {
837837
throwCompilationError(
838-
"Only constant map literals are supported",
838+
"Invalid map literal: it either uses run-time values or unsupported features like structs, cells or asm functions",
839839
node.loc,
840840
);
841841
// NB! Intentionally left here for when we can distinguish which Ast.Id are variables

src/optimizer/interpreter.ts

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,94 @@ import {
5454
} from "@/bindings/typescript/serializers";
5555
import { getMapAbi } from "@/types/resolveABITypeRef";
5656

57+
const minSignedInt = (nBits: number): bigint => -(2n ** (BigInt(nBits) - 1n));
58+
59+
const maxSignedInt = (nBits: number): bigint => 2n ** (BigInt(nBits) - 1n) - 1n;
60+
61+
const minUnsignedInt = (_nBits: number): bigint => 0n;
62+
63+
const maxUnsignedInt = (nBits: number): bigint => 2n ** BigInt(nBits) - 1n;
64+
65+
const minVarInt = (length: number): bigint =>
66+
minSignedInt(8 * (2 ** length - 1));
67+
68+
const maxVarInt = (length: number): bigint =>
69+
maxSignedInt(8 * (2 ** length - 1));
70+
71+
const minVarUint = (_length: number): bigint => 0n;
72+
73+
const maxVarUint = (length: number): bigint =>
74+
maxUnsignedInt(8 * (2 ** length - 1));
75+
76+
type mapKeyOrValueIntFormat =
77+
| { kind: "int"; bits: number }
78+
| { kind: "uint"; bits: number }
79+
| { kind: "varint"; length: number }
80+
| { kind: "varuint"; length: number };
81+
82+
const ensureMapIntKeyOrValRange = (
83+
num: Ast.Number,
84+
intFormat: mapKeyOrValueIntFormat,
85+
): Ast.Number => {
86+
const val = num.value;
87+
switch (intFormat.kind) {
88+
case "int":
89+
if (
90+
minSignedInt(intFormat.bits) <= val &&
91+
val <= maxSignedInt(intFormat.bits)
92+
) {
93+
return num;
94+
}
95+
throwErrorConstEval(
96+
`integer '${prettyPrint(num)}' does not fit into ${intFormat.bits}-bit signed integer type`,
97+
num.loc,
98+
);
99+
break;
100+
case "uint":
101+
if (
102+
minUnsignedInt(intFormat.bits) <= val &&
103+
val <= maxUnsignedInt(intFormat.bits)
104+
) {
105+
return num;
106+
}
107+
throwErrorConstEval(
108+
`integer '${prettyPrint(num)}' does not fit into ${intFormat.bits}-bit unsigned integer type`,
109+
num.loc,
110+
);
111+
break;
112+
case "varint":
113+
if (
114+
minVarInt(intFormat.length) <= val &&
115+
val <= maxVarInt(intFormat.length)
116+
) {
117+
return num;
118+
}
119+
throwErrorConstEval(
120+
`integer '${prettyPrint(num)}' does not fit into variable-length signed integer type with ${intFormat.length}-bit length`,
121+
num.loc,
122+
);
123+
break;
124+
case "varuint":
125+
if (
126+
minVarUint(intFormat.length) <= val &&
127+
val <= maxVarUint(intFormat.length)
128+
) {
129+
return num;
130+
}
131+
throwErrorConstEval(
132+
`integer '${prettyPrint(num)}' does not fit into variable-length unsigned integer type with ${intFormat.length}-bit length`,
133+
num.loc,
134+
);
135+
}
136+
};
137+
57138
// TVM integers are signed 257-bit integers
58-
const minTvmInt: bigint = -(2n ** 256n);
59-
const maxTvmInt: bigint = 2n ** 256n - 1n;
139+
const minTvmInt: bigint = minSignedInt(257);
140+
const maxTvmInt: bigint = maxSignedInt(257);
60141

61142
// Range allowed in repeat statements
62-
const minRepeatStatement: bigint = -(2n ** 256n); // Note it is the same as minimum for TVM
63-
const maxRepeatStatement: bigint = 2n ** 31n - 1n;
143+
const minRepeatStatement: bigint = minTvmInt; // Note it is the same as minimum for TVM
144+
const maxRepeatStatement: bigint = maxSignedInt(32);
64145

65146
// Util factory methods
66147
// FIXME: pass util as argument
@@ -1179,15 +1260,27 @@ export class Interpreter {
11791260
}
11801261
let dict = Dictionary.empty(keyType, valueType);
11811262
for (const { key: keyExpr, value: valueExpr } of ast.fields) {
1182-
const keyValue = parseKey(
1183-
this.interpretExpressionInternal(keyExpr),
1184-
keyExpr.loc,
1185-
);
1186-
const valValue = parseValue(
1187-
this.interpretExpressionInternal(valueExpr),
1188-
valueExpr.loc,
1189-
);
1190-
dict = dict.set(keyValue, valValue);
1263+
const keyValue = this.interpretExpressionInternal(keyExpr);
1264+
if (keyValue.kind === "number") {
1265+
if (res.key.kind === "int" || res.key.kind === "uint") {
1266+
ensureMapIntKeyOrValRange(keyValue, res.key);
1267+
}
1268+
}
1269+
const dictKeyValue = parseKey(keyValue, keyExpr.loc);
1270+
const valValue =
1271+
this.interpretExpressionInternal(valueExpr);
1272+
if (valValue.kind === "number") {
1273+
if (
1274+
res.value.kind === "int" ||
1275+
res.value.kind === "uint" ||
1276+
res.value.kind === "varint" ||
1277+
res.value.kind === "varuint"
1278+
) {
1279+
ensureMapIntKeyOrValRange(valValue, res.value);
1280+
}
1281+
}
1282+
const dictValValue = parseValue(valValue, valueExpr.loc);
1283+
dict = dict.set(dictKeyValue, dictValValue);
11911284
}
11921285
return beginCell()
11931286
.storeDictDirect(dict)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract C {
2+
get fun test(): Int {
3+
let m: map<Int as int8, Int> = map<Int as int8, Int> {
4+
-129: 0,
5+
};
6+
return m.get(2)!!;
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract C {
2+
get fun test(): Int {
3+
let m: map<Int, Int as int4> = map<Int, Int as int4> {
4+
0: 8,
5+
};
6+
return m.get(2)!!;
7+
}
8+
}

src/test/compilation-failed/contracts/map-literal-structs.tact

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
struct Foo {
2+
x: Int;
3+
y: Int;
4+
}
5+
16
contract MapLiteralStructs {
27
get fun structs(): map<Int as uint16, Foo> {
38
return map<Int as uint16, Foo> {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract C {
2+
get fun test(): Int {
3+
let m: map<Int as uint1, Int> = map<Int as uint1, Int> {
4+
2: 0, // key 2 is out of range for uint1 (0..1)
5+
};
6+
return m.get(2)!!;
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract C {
2+
get fun test(): Int {
3+
let m: map<Int, Int as uint4> = map<Int, Int as uint4> {
4+
2: 16,
5+
};
6+
return m.get(2)!!;
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract C {
2+
get fun test(): Int {
3+
let m: map<Int, Int as varint32> = map<Int, Int as varint32> {
4+
2: -pow2(31 * 8 - 1) - 1,
5+
};
6+
return m.get(2)!!;
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
contract C {
2+
get fun test(): Int {
3+
let m: map<Int, Int as coins> = map<Int, Int as coins> {
4+
2: pow2(120),
5+
};
6+
return m.get(2)!!;
7+
}
8+
}

0 commit comments

Comments
 (0)