Repository: tact-lang/tact

Issue:
Fix links in the CHANGELOG.md and within `context.tact`
https://github.com/tact-lang/tact/actions/runs/14323402844/job/40144456870

Hints from issue comments:

Solution patch:
diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md
index fb42439c..e6426dd4 100644
--- a/dev-docs/CHANGELOG.md
+++ b/dev-docs/CHANGELOG.md
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Optimized message deserialization with native loading of `Maybe Cell` fields: PR [#2661](https://github.com/tact-lang/tact/pull/2661)
 - [fix] Compiler now disallows `ton()` with empty or blank string: PR [#2681](https://github.com/tact-lang/tact/pull/2681)
+- [fix] Compiler now disallows `ton()` with invalid number value or negative numbers: PR [#2684](https://github.com/tact-lang/tact/pull/2684)
 
 ### Standard Library
 
@@ -81,7 +82,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Special thanks
 
-- [Ilya Aksakov](https://github.com/lexent3010) and the [RSquad](https://github.com/rsquad) team
+- [Ilya Aksakov](https://github.com/ilyaAksakov) and the [RSquad](https://github.com/rsquad) team
 
 ## [1.6.4] - 2025-03-18
 
diff --git a/src/optimizer/interpreter.ts b/src/optimizer/interpreter.ts
index bd07bae0..4e78c2e1 100644
--- a/src/optimizer/interpreter.ts
+++ b/src/optimizer/interpreter.ts
@@ -465,17 +465,17 @@ class EnvironmentStack {
 
     /*
     Sets a binding for "name" in the **current** environment of the stack.
-    If a binding for "name" already exists in the current environment, it 
+    If a binding for "name" already exists in the current environment, it
     overwrites the binding with the provided value.
     As a special case, name "_" is ignored.
 
-    Note that this method does not check if binding "name" already exists in 
+    Note that this method does not check if binding "name" already exists in
     a parent environment.
-    This means that if binding "name" already exists in a parent environment, 
+    This means that if binding "name" already exists in a parent environment,
     it will be shadowed by the provided value in the current environment.
     This shadowing behavior is useful for modelling recursive function calls.
-    For example, consider the recursive implementation of factorial 
-    (for simplification purposes, it returns 1 for the factorial of 
+    For example, consider the recursive implementation of factorial
+    (for simplification purposes, it returns 1 for the factorial of
     negative numbers):
 
     1  fun factorial(a: Int): Int {
@@ -496,7 +496,7 @@ class EnvironmentStack {
 
     When factorial(1) = 1 finishes execution, the environment at the top
     of the stack is popped:
-    
+
     a = 4 <------- a = 3 <-------- a = 2
 
     and execution resumes at line 5 in the environment where a = 2,
@@ -506,7 +506,7 @@ class EnvironmentStack {
 
     a = 4 <------- a = 3
 
-    so that the return at line 5 (now in the environment a = 3) will 
+    so that the return at line 5 (now in the environment a = 3) will
     produce 3 * 2 = 6, and so on.
     */
     public setNewBinding(name: string, val: Ast.Literal) {
@@ -517,7 +517,7 @@ class EnvironmentStack {
 
     /*
     Searches the binding "name" in the stack, starting at the current
-    environment and moving towards the parent environments. 
+    environment and moving towards the parent environments.
     If it finds the binding, it updates its value
     to "val". If it does not find "name", the stack is unchanged.
     As a special case, name "_" is always ignored.
@@ -533,7 +533,7 @@ class EnvironmentStack {
 
     /*
     Searches the binding "name" in the stack, starting at the current
-    environment and moving towards the parent environments. 
+    environment and moving towards the parent environments.
     If it finds "name", it returns its value.
     If it does not find "name", it returns undefined.
     As a special case, name "_" always returns undefined.
@@ -626,19 +626,19 @@ const defaultInterpreterConfig: InterpreterConfig = {
 };
 
 /*
-Interprets Tact AST trees. 
-The constructor receives an optional CompilerContext which includes 
+Interprets Tact AST trees.
+The constructor receives an optional CompilerContext which includes
 all external declarations that the interpreter will use during interpretation.
-If no CompilerContext is provided, the interpreter will use an empty 
+If no CompilerContext is provided, the interpreter will use an empty
 CompilerContext.
 
-**IMPORTANT**: if a custom CompilerContext is provided, it should be the 
-CompilerContext provided by the typechecker. 
+**IMPORTANT**: if a custom CompilerContext is provided, it should be the
+CompilerContext provided by the typechecker.
 
-The reason for requiring a CompilerContext is that the interpreter should work 
+The reason for requiring a CompilerContext is that the interpreter should work
 in the use case where the interpreter only knows part of the code.
-For example, consider the following code (I marked with brackets [ ] the places 
-where the interpreter gets called during expression simplification in the 
+For example, consider the following code (I marked with brackets [ ] the places
+where the interpreter gets called during expression simplification in the
 compilation phase):
 
 const C: Int = [1];
@@ -650,10 +650,10 @@ contract TestContract {
    }
 }
 
-When the interpreter gets called inside the brackets, it does not know what 
-other code is surrounding those brackets, because the interpreter did not execute the 
-code outside the brackets. Hence, it relies on the typechecker to receive the 
-CompilerContext that includes the declarations in the code 
+When the interpreter gets called inside the brackets, it does not know what
+other code is surrounding those brackets, because the interpreter did not execute the
+code outside the brackets. Hence, it relies on the typechecker to receive the
+CompilerContext that includes the declarations in the code
 (the constant C for example).
 
 Since the interpreter relies on the typechecker, it assumes that the given AST tree
@@ -1164,12 +1164,24 @@ export class Interpreter {
                         tons.loc,
                     );
                 }
+
+                const value = this.parseTonBuiltinValue(tons);
+                if (typeof value === "undefined") {
+                    throwCompilationError(
+                        "ton() function requires a valid number with no more than 10 digits after the decimal point",
+                        tons.loc,
+                    );
+                }
+                if (value < 0) {
+                    throwCompilationError(
+                        "ton() function requires a non-negative number",
+                        tons.loc,
+                    );
+                }
+
                 try {
                     return ensureInt(
-                        this.util.makeNumberLiteral(
-                            BigInt(toNano(tons.value).toString(10)),
-                            ast.loc,
-                        ),
+                        this.util.makeNumberLiteral(value, ast.loc),
                     );
                 } catch (e) {
                     if (e instanceof Error && e.message === "Invalid number") {
@@ -1491,6 +1503,15 @@ export class Interpreter {
         }
     }
 
+    private parseTonBuiltinValue(tons: Ast.String): bigint | undefined {
+        try {
+            const value = toNano(tons.value);
+            return BigInt(value.toString(10));
+        } catch {
+            return undefined;
+        }
+    }
+
     private evalStaticFunction(
         functionCode: Ast.FunctionDef,
         args: readonly Ast.Expression[],
diff --git a/src/stdlib/stdlib.ts b/src/stdlib/stdlib.ts
index 8d40e8eb..5cd6fa55 100644
--- a/src/stdlib/stdlib.ts
+++ b/src/stdlib/stdlib.ts
@@ -982,7 +982,7 @@ files["std/internal/context.tact"] =
     "ICAgIDYgMSBCTEtEUk9QMiAvLyBkcm9wIHRoZSBsb2FkZWQgdmFsdWVzIGFzIHdlbGwgYXMgdGhlIGZpcnN0IDMgZmllbGRzIG9mIENvbnRleHQKCiAgICBaRVJPICAg" +
     "ICAgICAgICAgICAvLyBub3QgbWFzdGVyY2hhaW4KICAgIEdFVE9SSUdJTkFMRldERkVFIC8vIGZsb29yKGZ3ZF9mZWUgKiAyXjE2IC8gKDJeMTYgLSBmaXJzdF9mcmFj" +
     "KSksIHdoZXJlCiAgICAgICAgICAgICAgICAgICAgICAvLyBmaXJzdF9mcmFjIGlzIGEgdmFsdWUgbGlzdGVkIGluIGNvbmZpZyBwYXJhbSAyNQogICAgICAgICAgICAg" +
-    "ICAgICAgICAgLy8gb2YgdGhlIGJsb2NrY2hhaW46IGh0dHBzOi8vdG9udmlld2VyLm9yZy9jb25maWcjMjUKICAgICAgICAgICAgICAgICAgICAgIC8vIHRoaXMgaW5z" +
+    "ICAgICAgICAgLy8gb2YgdGhlIGJsb2NrY2hhaW46IGh0dHBzOi8vdG9udmlld2VyLmNvbS9jb25maWcjMjUKICAgICAgICAgICAgICAgICAgICAgIC8vIHRoaXMgaW5z" +
     "dHJ1Y3Rpb24gZWZmZWN0aXZlbHkgbXVsdGlwbGllcyB0aGUgZndkX2ZlZSBieSAxLjUsCiAgICAgICAgICAgICAgICAgICAgICAvLyBhdCBsZWFzdCBmb3IgdGhlIGN1" +
     "cnJlbnQgdmFsdWUgb2YgZmlyc3RfZnJhYywgd2hpY2ggaXMgMjE4NDUKfQo=";
 files["std/internal/contract.tact"] =
diff --git a/src/stdlib/stdlib/std/internal/context.tact b/src/stdlib/stdlib/std/internal/context.tact
index 1a1c7946..b4534d06 100644
--- a/src/stdlib/stdlib/std/internal/context.tact
+++ b/src/stdlib/stdlib/std/internal/context.tact
@@ -115,7 +115,7 @@ asm extends fun readForwardFee(self: Context): Int {
     ZERO              // not masterchain
     GETORIGINALFWDFEE // floor(fwd_fee * 2^16 / (2^16 - first_frac)), where
                       // first_frac is a value listed in config param 25
-                      // of the blockchain: https://tonviewer.org/config#25
+                      // of the blockchain: https://tonviewer.com/config#25
                       // this instruction effectively multiplies the fwd_fee by 1.5,
                       // at least for the current value of first_frac, which is 21845
 }
diff --git a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap
index 848eb7b7..bc697602 100644
--- a/src/types/__snapshots__/resolveDescriptors.spec.ts.snap
+++ b/src/types/__snapshots__/resolveDescriptors.spec.ts.snap
@@ -951,6 +951,42 @@ exports[`resolveDescriptors should fail descriptors for ton-with-empty-string 1`
 "
 `;
 
+exports[`resolveDescriptors should fail descriptors for ton-with-invalid-string 1`] = `
+"<unknown>:3:22: ton() function requires a valid number with no more than 10 digits after the decimal point
+  2 | 
+> 3 | const FOO: Int = ton("45,67");
+                           ^~~~~~~
+  4 | 
+"
+`;
+
+exports[`resolveDescriptors should fail descriptors for ton-with-invalid-string-2 1`] = `
+"<unknown>:3:22: ton() function requires a valid number with no more than 10 digits after the decimal point
+  2 | 
+> 3 | const FOO: Int = ton("40+2");
+                           ^~~~~~
+  4 | 
+"
+`;
+
+exports[`resolveDescriptors should fail descriptors for ton-with-invalid-string-3 1`] = `
+"<unknown>:3:22: ton() function requires a valid number with no more than 10 digits after the decimal point
+  2 | 
+> 3 | const FOO: Int = ton("hello");
+                           ^~~~~~~
+  4 | 
+"
+`;
+
+exports[`resolveDescriptors should fail descriptors for ton-with-invalid-string-11-diigits-after-dot 1`] = `
+"<unknown>:3:22: ton() function requires a valid number with no more than 10 digits after the decimal point
+  2 | 
+> 3 | const FOO: Int = ton("1.12345678901");
+                           ^~~~~~~~~~~~~~~
+  4 | 
+"
+`;
+
 exports[`resolveDescriptors should fail descriptors for trait-circular-deps 1`] = `
 "<unknown>:3:1: Circular dependency detected for type "A"
   2 | 
diff --git a/src/types/test-failed/ton-with-invalid-string-11-diigits-after-dot.tact b/src/types/test-failed/ton-with-invalid-string-11-diigits-after-dot.tact
new file mode 100644
index 00000000..d1f80e10
--- /dev/null
+++ b/src/types/test-failed/ton-with-invalid-string-11-diigits-after-dot.tact
@@ -0,0 +1,3 @@
+primitive Int;
+
+const FOO: Int = ton("1.12345678901");
diff --git a/src/types/test-failed/ton-with-invalid-string-2.tact b/src/types/test-failed/ton-with-invalid-string-2.tact
new file mode 100644
index 00000000..e47a319d
--- /dev/null
+++ b/src/types/test-failed/ton-with-invalid-string-2.tact
@@ -0,0 +1,3 @@
+primitive Int;
+
+const FOO: Int = ton("40+2");
diff --git a/src/types/test-failed/ton-with-invalid-string-3.tact b/src/types/test-failed/ton-with-invalid-string-3.tact
new file mode 100644
index 00000000..1c66f3f9
--- /dev/null
+++ b/src/types/test-failed/ton-with-invalid-string-3.tact
@@ -0,0 +1,3 @@
+primitive Int;
+
+const FOO: Int = ton("hello");
diff --git a/src/types/test-failed/ton-with-invalid-string.tact b/src/types/test-failed/ton-with-invalid-string.tact
new file mode 100644
index 00000000..6808215b
--- /dev/null
+++ b/src/types/test-failed/ton-with-invalid-string.tact
@@ -0,0 +1,3 @@
+primitive Int;
+
+const FOO: Int = ton("45,67");



Test patch:
diff --git a/src/test/compilation-failed/const-eval-failed.spec.ts b/src/test/compilation-failed/const-eval-failed.spec.ts
index 2b89c98b..59be21a5 100644
--- a/src/test/compilation-failed/const-eval-failed.spec.ts
+++ b/src/test/compilation-failed/const-eval-failed.spec.ts
@@ -48,7 +48,7 @@ describe("fail-const-eval", () => {
     });
     itShouldNotCompile({
         testName: "const-eval-int-overflow-ton1",
-        errorMessage: `Cannot evaluate expression to a constant: invalid "ton" argument`,
+        errorMessage: `ton() function requires a valid number with no more than 10 digits after the decimal point`,
     });
     itShouldNotCompile({
         testName: "const-eval-int-overflow-ton2",