Repository: tact-lang/tact

Issue:
Unstructured @ton/core error when key or value in map literal is out of serialization range
**Reproduction:**
```tact
contract C {
    get fun test(): Int {
        let m: map<Int as uint1, Int> = map<Int as uint1, Int> {
            2: 0,    // key 2 is out of range for uint1 (0..1)
        };
        return m.get(2)!!;
    }
}
```

**Actual Output:**
```
Tact compilation failed
Error: bitLength is too small for a value 2. Got 1
    at BitBuilder.writeUint (...)
    at Builder.storeUint (...)
    at Object.serialize (...)
    at Dictionary.storeDirect (...)
    at Builder.storeDictDirect (...)
    at .../interpreter.js:853:22
    ... [stack trace continues]
```

**Expected Behavior:**  
Compiler should emit a structured, user-facing error with source context, e.g.:
```
Error: key expression `2` is too large for `uint1` (allowed range 0..1)
```

**Summary:**  
This is a compiler issue: invalid map literals with out-of-range keys/values currently produce an unstructured error from `@ton/core`, rather than a proper diagnostic from the compiler. This breaks expected UX.

---

LLM Fuzzing discovery (see #2490)

Hints from issue comments:

Solution patch:
diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md
index 7f22e7fc..fd167e52 100644
--- a/dev-docs/CHANGELOG.md
+++ b/dev-docs/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - [fix] Compiler now doesn't require explicit re-declaration for abstract methods and fields in traits: PR [#3272](https://github.com/tact-lang/tact/pull/3272)
 - Compiler now allows `toCell()` and `toSlice()` methods on contract types: PR [#3274](https://github.com/tact-lang/tact/pull/3274)
 - Compiler now generates message loading in place for better performance: PR [#2993](https://github.com/tact-lang/tact/pull/2993)
+- Compiler now shows a more informative error message if two `require()` calls have the same generated error code: PR [#3286](https://github.com/tact-lang/tact/pull/3286)
 
 ### Docs
 
@@ -26,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Language features
 
+- [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)
 - [fix] Balanced quotation in error messages for out-of-project-root imports: PR [#3242](https://github.com/tact-lang/tact/pull/3242)
 - [fix] Disallow self-inheritance for contracts and traits: PR [#3094](https://github.com/tact-lang/tact/pull/3094)
 - [fix] Added fixed-bytes support to bounced message size calculations: PR [#3129](https://github.com/tact-lang/tact/pull/3129)
diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts
index 12984564..b3d03336 100644
--- a/src/generator/writers/writeExpression.ts
+++ b/src/generator/writers/writeExpression.ts
@@ -835,7 +835,7 @@ const writeMapLiteral =
     (node: Ast.MapLiteral) =>
     (_ctx: WriterContext): string => {
         throwCompilationError(
-            "Only constant map literals are supported",
+            "Invalid map literal: it either uses run-time values or unsupported features like structs, cells or asm functions",
             node.loc,
         );
         // NB! Intentionally left here for when we can distinguish which Ast.Id are variables
diff --git a/src/optimizer/interpreter.ts b/src/optimizer/interpreter.ts
index 8561c045..25d38cc9 100644
--- a/src/optimizer/interpreter.ts
+++ b/src/optimizer/interpreter.ts
@@ -54,13 +54,94 @@ import {
 } from "@/bindings/typescript/serializers";
 import { getMapAbi } from "@/types/resolveABITypeRef";
 
+const minSignedInt = (nBits: number): bigint => -(2n ** (BigInt(nBits) - 1n));
+
+const maxSignedInt = (nBits: number): bigint => 2n ** (BigInt(nBits) - 1n) - 1n;
+
+const minUnsignedInt = (_nBits: number): bigint => 0n;
+
+const maxUnsignedInt = (nBits: number): bigint => 2n ** BigInt(nBits) - 1n;
+
+const minVarInt = (length: number): bigint =>
+    minSignedInt(8 * (2 ** length - 1));
+
+const maxVarInt = (length: number): bigint =>
+    maxSignedInt(8 * (2 ** length - 1));
+
+const minVarUint = (_length: number): bigint => 0n;
+
+const maxVarUint = (length: number): bigint =>
+    maxUnsignedInt(8 * (2 ** length - 1));
+
+type mapKeyOrValueIntFormat =
+    | { kind: "int"; bits: number }
+    | { kind: "uint"; bits: number }
+    | { kind: "varint"; length: number }
+    | { kind: "varuint"; length: number };
+
+const ensureMapIntKeyOrValRange = (
+    num: Ast.Number,
+    intFormat: mapKeyOrValueIntFormat,
+): Ast.Number => {
+    const val = num.value;
+    switch (intFormat.kind) {
+        case "int":
+            if (
+                minSignedInt(intFormat.bits) <= val &&
+                val <= maxSignedInt(intFormat.bits)
+            ) {
+                return num;
+            }
+            throwErrorConstEval(
+                `integer '${prettyPrint(num)}' does not fit into ${intFormat.bits}-bit signed integer type`,
+                num.loc,
+            );
+            break;
+        case "uint":
+            if (
+                minUnsignedInt(intFormat.bits) <= val &&
+                val <= maxUnsignedInt(intFormat.bits)
+            ) {
+                return num;
+            }
+            throwErrorConstEval(
+                `integer '${prettyPrint(num)}' does not fit into ${intFormat.bits}-bit unsigned integer type`,
+                num.loc,
+            );
+            break;
+        case "varint":
+            if (
+                minVarInt(intFormat.length) <= val &&
+                val <= maxVarInt(intFormat.length)
+            ) {
+                return num;
+            }
+            throwErrorConstEval(
+                `integer '${prettyPrint(num)}' does not fit into variable-length signed integer type with ${intFormat.length}-bit length`,
+                num.loc,
+            );
+            break;
+        case "varuint":
+            if (
+                minVarUint(intFormat.length) <= val &&
+                val <= maxVarUint(intFormat.length)
+            ) {
+                return num;
+            }
+            throwErrorConstEval(
+                `integer '${prettyPrint(num)}' does not fit into variable-length unsigned integer type with ${intFormat.length}-bit length`,
+                num.loc,
+            );
+    }
+};
+
 // TVM integers are signed 257-bit integers
-const minTvmInt: bigint = -(2n ** 256n);
-const maxTvmInt: bigint = 2n ** 256n - 1n;
+const minTvmInt: bigint = minSignedInt(257);
+const maxTvmInt: bigint = maxSignedInt(257);
 
 // Range allowed in repeat statements
-const minRepeatStatement: bigint = -(2n ** 256n); // Note it is the same as minimum for TVM
-const maxRepeatStatement: bigint = 2n ** 31n - 1n;
+const minRepeatStatement: bigint = minTvmInt; // Note it is the same as minimum for TVM
+const maxRepeatStatement: bigint = maxSignedInt(32);
 
 // Util factory methods
 // FIXME: pass util as argument
@@ -1179,15 +1260,27 @@ export class Interpreter {
                 }
                 let dict = Dictionary.empty(keyType, valueType);
                 for (const { key: keyExpr, value: valueExpr } of ast.fields) {
-                    const keyValue = parseKey(
-                        this.interpretExpressionInternal(keyExpr),
-                        keyExpr.loc,
-                    );
-                    const valValue = parseValue(
-                        this.interpretExpressionInternal(valueExpr),
-                        valueExpr.loc,
-                    );
-                    dict = dict.set(keyValue, valValue);
+                    const keyValue = this.interpretExpressionInternal(keyExpr);
+                    if (keyValue.kind === "number") {
+                        if (res.key.kind === "int" || res.key.kind === "uint") {
+                            ensureMapIntKeyOrValRange(keyValue, res.key);
+                        }
+                    }
+                    const dictKeyValue = parseKey(keyValue, keyExpr.loc);
+                    const valValue =
+                        this.interpretExpressionInternal(valueExpr);
+                    if (valValue.kind === "number") {
+                        if (
+                            res.value.kind === "int" ||
+                            res.value.kind === "uint" ||
+                            res.value.kind === "varint" ||
+                            res.value.kind === "varuint"
+                        ) {
+                            ensureMapIntKeyOrValRange(valValue, res.value);
+                        }
+                    }
+                    const dictValValue = parseValue(valValue, valueExpr.loc);
+                    dict = dict.set(dictKeyValue, dictValValue);
                 }
                 return beginCell()
                     .storeDictDirect(dict)
diff --git a/src/storage/resolveErrors.spec.ts b/src/storage/resolveErrors.spec.ts
index 549de85b..3e423ca7 100644
--- a/src/storage/resolveErrors.spec.ts
+++ b/src/storage/resolveErrors.spec.ts
@@ -49,4 +49,45 @@ contract Test {
         ctx = resolveStatements(ctx);
         expect(() => resolveErrors(ctx, ast)).toThrow(expectedErrors);
     });
+
+    it("should throw on duplicate error code", () => {
+        const src = `
+trait BaseTrait {}
+
+contract Test {
+    get fun foo(): Int {
+        require(false, "113");
+        return 0;
+    }
+
+    get fun bar(): Int {
+        require(false, "241");
+        return 0;
+    }
+}
+`;
+
+        const expectedErrors = `<unknown>:11:24: Error message "241" maps to error code 38048, which is already assigned to "113", change the message to resolve this conflict
+  10 |     get fun bar(): Int {
+> 11 |         require(false, "241");
+                              ^~~~~
+  12 |         return 0;
+`;
+
+        const ast = getAstFactory();
+        const sources: Source[] = [
+            { code: stdlib, path: primitivesPath, origin: "stdlib" },
+            { code: src, path: "<unknown>", origin: "user" },
+        ];
+        let ctx = openContext(
+            new CompilerContext(),
+            sources,
+            [],
+            parseModules(sources, getParser(ast)),
+        );
+        ctx = resolveDescriptors(ctx, ast);
+        ctx = resolveSignatures(ctx, ast);
+        ctx = resolveStatements(ctx);
+        expect(() => resolveErrors(ctx, ast)).toThrow(expectedErrors);
+    });
 });
diff --git a/src/types/resolveErrors.ts b/src/types/resolveErrors.ts
index 8da52862..6cd14d17 100644
--- a/src/types/resolveErrors.ts
+++ b/src/types/resolveErrors.ts
@@ -40,21 +40,21 @@ function resolveStringsInAST(
 ) {
     traverse(ast, (node) => {
         if (node.kind === "static_call" && isRequire(node.function)) {
-            if (node.args.length !== 2) {
+            const [, messageArg] = node.args;
+            if (typeof messageArg === "undefined") {
                 return;
             }
             const resolved = ensureString(
-                evaluateRequireErrorString(node.args[1]!, ctx, util),
+                evaluateRequireErrorString(messageArg, ctx, util),
             ).value;
             if (!exceptions.get(ctx, resolved)) {
                 const id = exceptionId(resolved);
-                if (
-                    Array.from(exceptions.all(ctx).values()).find(
-                        (v) => v.id === id,
-                    )
-                ) {
-                    throwInternalCompilerError(
-                        `Duplicate error id: "${resolved}"`,
+                const exceptionsArray = [...exceptions.all(ctx).values()];
+                const prev = exceptionsArray.find((v) => v.id === id);
+                if (typeof prev !== "undefined") {
+                    throwCompilationError(
+                        `Error message "${resolved}" maps to error code ${id}, which is already assigned to "${prev.value}", change the message to resolve this conflict`,
+                        messageArg.loc,
                     );
                 }
                 ctx = exceptions.set(ctx, resolved, { value: resolved, id });



Test patch:
diff --git a/src/test/compilation-failed/contracts/map-literal-int-key-out-of-range.tact b/src/test/compilation-failed/contracts/map-literal-int-key-out-of-range.tact
new file mode 100644
index 00000000..fc82dcfd
--- /dev/null
+++ b/src/test/compilation-failed/contracts/map-literal-int-key-out-of-range.tact
@@ -0,0 +1,8 @@
+contract C {
+    get fun test(): Int {
+        let m: map<Int as int8, Int> = map<Int as int8, Int> {
+            -129: 0,
+        };
+        return m.get(2)!!;
+    }
+}
diff --git a/src/test/compilation-failed/contracts/map-literal-int-val-out-of-range.tact b/src/test/compilation-failed/contracts/map-literal-int-val-out-of-range.tact
new file mode 100644
index 00000000..5de2a1a1
--- /dev/null
+++ b/src/test/compilation-failed/contracts/map-literal-int-val-out-of-range.tact
@@ -0,0 +1,8 @@
+contract C {
+    get fun test(): Int {
+        let m: map<Int, Int as int4> = map<Int, Int as int4> {
+            0: 8,
+        };
+        return m.get(2)!!;
+    }
+}
diff --git a/src/test/compilation-failed/contracts/map-literal-structs.tact b/src/test/compilation-failed/contracts/map-literal-structs.tact
index f8f59d8f..0be76e66 100644
--- a/src/test/compilation-failed/contracts/map-literal-structs.tact
+++ b/src/test/compilation-failed/contracts/map-literal-structs.tact
@@ -1,3 +1,8 @@
+struct Foo {
+    x: Int;
+    y: Int;
+}
+
 contract MapLiteralStructs {
     get fun structs(): map<Int as uint16, Foo> {
         return map<Int as uint16, Foo> {
diff --git a/src/test/compilation-failed/contracts/map-literal-uint-key-out-of-range.tact b/src/test/compilation-failed/contracts/map-literal-uint-key-out-of-range.tact
new file mode 100644
index 00000000..0428cb02
--- /dev/null
+++ b/src/test/compilation-failed/contracts/map-literal-uint-key-out-of-range.tact
@@ -0,0 +1,8 @@
+contract C {
+    get fun test(): Int {
+        let m: map<Int as uint1, Int> = map<Int as uint1, Int> {
+            2: 0,    // key 2 is out of range for uint1 (0..1)
+        };
+        return m.get(2)!!;
+    }
+}
diff --git a/src/test/compilation-failed/contracts/map-literal-uint-val-out-of-range.tact b/src/test/compilation-failed/contracts/map-literal-uint-val-out-of-range.tact
new file mode 100644
index 00000000..6df6382b
--- /dev/null
+++ b/src/test/compilation-failed/contracts/map-literal-uint-val-out-of-range.tact
@@ -0,0 +1,8 @@
+contract C {
+    get fun test(): Int {
+        let m: map<Int, Int as uint4> = map<Int, Int as uint4> {
+            2: 16,
+        };
+        return m.get(2)!!;
+    }
+}
diff --git a/src/test/compilation-failed/contracts/map-literal-varint-val-out-of-range.tact b/src/test/compilation-failed/contracts/map-literal-varint-val-out-of-range.tact
new file mode 100644
index 00000000..2edea599
--- /dev/null
+++ b/src/test/compilation-failed/contracts/map-literal-varint-val-out-of-range.tact
@@ -0,0 +1,8 @@
+contract C {
+    get fun test(): Int {
+        let m: map<Int, Int as varint32> = map<Int, Int as varint32> {
+            2: -pow2(31 * 8 - 1) - 1,
+        };
+        return m.get(2)!!;
+    }
+}
diff --git a/src/test/compilation-failed/contracts/map-literal-varuint-val-out-of-range.tact b/src/test/compilation-failed/contracts/map-literal-varuint-val-out-of-range.tact
new file mode 100644
index 00000000..582f6a86
--- /dev/null
+++ b/src/test/compilation-failed/contracts/map-literal-varuint-val-out-of-range.tact
@@ -0,0 +1,8 @@
+contract C {
+    get fun test(): Int {
+        let m: map<Int, Int as coins> = map<Int, Int as coins> {
+            2: pow2(120),
+        };
+        return m.get(2)!!;
+    }
+}
diff --git a/src/test/compilation-failed/map-literal-errors.spec.ts b/src/test/compilation-failed/map-literal-errors.spec.ts
new file mode 100644
index 00000000..e39a4e78
--- /dev/null
+++ b/src/test/compilation-failed/map-literal-errors.spec.ts
@@ -0,0 +1,49 @@
+import { itShouldNotCompile } from "@/test/compilation-failed/util";
+
+describe("map-literal-errors", () => {
+    itShouldNotCompile({
+        testName: "map-literal-cell",
+        errorMessage:
+            "Invalid map literal: it either uses run-time values or unsupported features like structs, cells or asm functions",
+    });
+    itShouldNotCompile({
+        testName: "map-literal-runtime",
+        errorMessage:
+            "Invalid map literal: it either uses run-time values or unsupported features like structs, cells or asm functions",
+    });
+    itShouldNotCompile({
+        testName: "map-literal-structs",
+        errorMessage:
+            "Invalid map literal: it either uses run-time values or unsupported features like structs, cells or asm functions",
+    });
+    itShouldNotCompile({
+        testName: "map-literal-uint-key-out-of-range",
+        errorMessage:
+            "Cannot evaluate expression to a constant: integer '2' does not fit into 1-bit unsigned integer type",
+    });
+    itShouldNotCompile({
+        testName: "map-literal-int-key-out-of-range",
+        errorMessage:
+            "Cannot evaluate expression to a constant: integer '-129' does not fit into 8-bit signed integer type",
+    });
+    itShouldNotCompile({
+        testName: "map-literal-int-val-out-of-range",
+        errorMessage:
+            "Cannot evaluate expression to a constant: integer '8' does not fit into 4-bit signed integer type",
+    });
+    itShouldNotCompile({
+        testName: "map-literal-uint-val-out-of-range",
+        errorMessage:
+            "Cannot evaluate expression to a constant: integer '16' does not fit into 4-bit unsigned integer type",
+    });
+    itShouldNotCompile({
+        testName: "map-literal-varuint-val-out-of-range",
+        errorMessage:
+            "Cannot evaluate expression to a constant: integer '1329227995784915872903807060280344576' does not fit into variable-length unsigned integer type with 4-bit length",
+    });
+    itShouldNotCompile({
+        testName: "map-literal-varint-val-out-of-range",
+        errorMessage:
+            "Cannot evaluate expression to a constant: integer '-226156424291633194186662080095093570025917938800079226639565593765455331329' does not fit into variable-length signed integer type with 5-bit length",
+    });
+});