From 5f496cd36cf738e64c65bd0acdc0ee78d6ecf8b3 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Tue, 3 Sep 2024 08:51:03 +0300 Subject: [PATCH 01/19] feat: implement `slice`, `rawSlice`, `ascii` and `crc32` --- package.json | 1 + src/abi/global.ts | 189 +++++++++++++++++- src/generator/writers/writeConstant.ts | 12 +- src/generator/writers/writeExpression.ts | 8 +- src/interpreter.ts | 68 ++++++- .../e2e-emulated/contracts/intrinsics.tact | 36 ++++ src/test/e2e-emulated/intrinsics.spec.ts | 40 ++++ src/types/types.ts | 5 +- yarn.lock | 5 + 9 files changed, 357 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 22b857fac..105fc75be 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@ton/crypto": "^3.2.0", "blockstore-core": "1.0.5", "change-case": "^4.1.2", + "crc-32": "1.2.2", "ipfs-unixfs-importer": "9.0.10", "json-bigint": "^1.0.0", "meow": "^13.2.0", diff --git a/src/abi/global.ts b/src/abi/global.ts index 46a46a587..82f3f4520 100644 --- a/src/abi/global.ts +++ b/src/abi/global.ts @@ -1,6 +1,10 @@ -import { Address, Cell, toNano } from "@ton/core"; +import { Address, beginCell, Cell, toNano } from "@ton/core"; import { enabledDebug, enabledMasterchain } from "../config/features"; -import { writeAddress, writeCell } from "../generator/writers/writeConstant"; +import { + writeAddress, + writeCell, + writeSlice, +} from "../generator/writers/writeConstant"; import { writeExpression, writeValue, @@ -398,4 +402,185 @@ export const GlobalFunctions: Map = new Map([ }, }, ], + [ + "slice", + { + name: "slice", + resolve: (ctx, args, ref) => { + if (args.length !== 1) { + throwCompilationError("slice() expects one argument", ref); + } + const arg0 = args[0]!; + if (arg0.kind !== "ref") { + throwCompilationError( + "slice() expects string argument", + ref, + ); + } + if (arg0.name !== "String") { + throwCompilationError( + "slice() expects string argument", + ref, + ); + } + return { kind: "ref", name: "Slice", optional: false }; + }, + generate: (ctx, args, resolved, ref) => { + if (resolved.length !== 1) { + throwCompilationError("slice() expects one argument", ref); + } + + // Load slice data + const str = evalConstantExpression( + resolved[0]!, + ctx.ctx, + ) as string; + let c: Cell; + try { + c = Cell.fromBase64(str); + } catch (e) { + throwCompilationError(`Invalid slice ${str}`, ref); + } + + const res = writeSlice(c.asSlice(), ctx); + ctx.used(res); + return `${res}()`; + }, + }, + ], + [ + "rawSlice", + { + name: "rawSlice", + resolve: (ctx, args, ref) => { + if (args.length !== 1) { + throwCompilationError( + "rawSlice() expects one argument", + ref, + ); + } + const arg0 = args[0]!; + if (arg0.kind !== "ref") { + throwCompilationError( + "rawSlice() expects string argument", + ref, + ); + } + if (arg0.name !== "String") { + throwCompilationError( + "rawSlice() expects string argument", + ref, + ); + } + return { kind: "ref", name: "Slice", optional: false }; + }, + generate: (ctx, args, resolved, ref) => { + if (resolved.length !== 1) { + throwCompilationError( + "rawSlice() expects one argument", + ref, + ); + } + + // Load slice data + const str = evalConstantExpression( + resolved[0]!, + ctx.ctx, + ) as string; + let c: Cell; + try { + c = beginCell().storeBuffer(Buffer.from(str)).endCell(); + } catch (e) { + throwCompilationError(`Invalid slice data ${str}`, ref); + } + + const res = writeSlice(c.asSlice(), ctx); + ctx.used(res); + return `${res}()`; + }, + }, + ], + [ + "ascii", + { + name: "ascii", + resolve: (ctx, args, ref) => { + if (args.length !== 1) { + throwCompilationError("ascii() expects one argument", ref); + } + const arg0 = args[0]!; + if (arg0.kind !== "ref") { + throwCompilationError( + "ascii() expects string argument", + ref, + ); + } + if (arg0.name !== "String") { + throwCompilationError( + "ascii() expects string argument", + ref, + ); + } + return { kind: "ref", name: "Int", optional: false }; + }, + generate: (ctx, args, resolved, ref) => { + if (resolved.length !== 1) { + throwCompilationError("ascii() expects one argument", ref); + } + + // Load slice data + const str = evalConstantExpression( + resolved[0]!, + ctx.ctx, + ) as string; + + if (str.length > 32) { + throwCompilationError( + `ascii() expects string argument with length <= 32`, + ref, + ); + } + + return `"${str}"u`; + }, + }, + ], + [ + "crc32", + { + name: "crc32", + resolve: (ctx, args, ref) => { + if (args.length !== 1) { + throwCompilationError("crc32() expects one argument", ref); + } + const arg0 = args[0]!; + if (arg0.kind !== "ref") { + throwCompilationError( + "crc32() expects string argument", + ref, + ); + } + if (arg0.name !== "String") { + throwCompilationError( + "crc32() expects string argument", + ref, + ); + } + return { kind: "ref", name: "Int", optional: false }; + }, + generate: (ctx, args, resolved, ref) => { + if (resolved.length !== 1) { + throwCompilationError("crc32() expects one argument", ref); + } + + // Load slice data + const str = evalConstantExpression( + resolved[0]!, + ctx.ctx, + ) as string; + + return `"${str}"c`; + }, + }, + ], ]); diff --git a/src/generator/writers/writeConstant.ts b/src/generator/writers/writeConstant.ts index d4489001e..7b45bdee6 100644 --- a/src/generator/writers/writeConstant.ts +++ b/src/generator/writers/writeConstant.ts @@ -1,4 +1,4 @@ -import { Address, beginCell, Cell } from "@ton/core"; +import { Address, beginCell, Cell, Slice } from "@ton/core"; import { WriterContext } from "../Writer"; export function writeString(str: string, ctx: WriterContext) { @@ -29,6 +29,16 @@ export function writeCell(cell: Cell, ctx: WriterContext) { ); } +export function writeSlice(slice: Slice, ctx: WriterContext) { + const cell = slice.asCell(); + return writeRawSlice( + "slice", + "Slice " + cell.hash().toString("base64"), + cell, + ctx, + ); +} + function writeRawSlice( prefix: string, comment: string, diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index 700c7a8ce..937d3144c 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -31,11 +31,12 @@ import { GlobalFunctions } from "../../abi/global"; import { funcIdOf } from "./id"; import { StructFunctions } from "../../abi/struct"; import { resolveFuncType } from "./resolveFuncType"; -import { Address, Cell } from "@ton/core"; +import { Address, Cell, Slice } from "@ton/core"; import { writeAddress, writeCell, writeComment, + writeSlice, writeString, } from "./writeConstant"; import { ops } from "./ops"; @@ -119,6 +120,11 @@ export function writeValue(val: Value, wCtx: WriterContext): string { wCtx.used(res); return `${res}()`; } + if (val instanceof Slice) { + const res = writeSlice(val, wCtx); + wCtx.used(res); + return `${res}()`; + } if (val === null) { return "null()"; } diff --git a/src/interpreter.ts b/src/interpreter.ts index a6b654529..b4e429612 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1,4 +1,5 @@ -import { Address, Cell, toNano } from "@ton/core"; +import { Address, beginCell, Cell, toNano } from "@ton/core"; +import * as crc32 from "crc-32"; import { evalConstantExpression } from "./constEval"; import { CompilerContext } from "./context"; import { @@ -1070,6 +1071,71 @@ export class Interpreter { } } break; + case "slice": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString( + this.interpretExpression(ast.args[0]!), + ast.args[0]!.loc, + ); + try { + return Cell.fromBase64(str).asSlice(); + } catch (_) { + throwErrorConstEval( + `invalid base64 encoding for a cell: ${str}`, + ast.loc, + ); + } + } + break; + case "rawSlice": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString( + this.interpretExpression(ast.args[0]!), + ast.args[0]!.loc, + ); + try { + return beginCell() + .storeBuffer(Buffer.from(str)) + .endCell() + .asSlice(); + } catch (_) { + throwErrorConstEval( + `invalid slice data: ${str}`, + ast.loc, + ); + } + } + break; + case "ascii": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString( + this.interpretExpression(ast.args[0]!), + ast.args[0]!.loc, + ); + if (str.length > 32) { + throwErrorConstEval( + `ascii string is too long, expected up to 32 characters, got ${str.length}`, + ast.loc, + ); + } + return BigInt( + "0x" + Buffer.from(str, "ascii").toString("hex"), + ); + } + break; + case "crc32": + { + ensureFunArity(1, ast.args, ast.loc); + const str = ensureString( + this.interpretExpression(ast.args[0]!), + ast.args[0]!.loc, + ); + return BigInt(crc32.str(str)); + } + break; case "address": { ensureFunArity(1, ast.args, ast.loc); diff --git a/src/test/e2e-emulated/contracts/intrinsics.tact b/src/test/e2e-emulated/contracts/intrinsics.tact index 75817ab88..0de691970 100644 --- a/src/test/e2e-emulated/contracts/intrinsics.tact +++ b/src/test/e2e-emulated/contracts/intrinsics.tact @@ -6,6 +6,10 @@ contract IntrinsicsTester { d: Cell = cell("te6cckEBAQEADgAAGEhlbGxvIHdvcmxkIXgtxbw="); e: Int = pow(2, 9); f: Int = sha256("hello world"); + g: Slice = slice("te6cckEBAQEADgAAGEhlbGxvIHdvcmxkIXgtxbw="); + h: Slice = rawSlice("hello world"); + i: Int = ascii("hello world"); + j: Int = crc32("hello world"); init() { @@ -78,4 +82,36 @@ contract IntrinsicsTester { receive("emit_1") { emit("Hello world".asComment()); } + + get fun getSlice(): Slice { + return slice("te6cckEBAQEADgAAGEhlbGxvIHdvcmxkIXgtxbw="); + } + + get fun getSlice2(): Slice { + return self.g; + } + + get fun getRawSlice(): Slice { + return rawSlice("hello world"); + } + + get fun getRawSlice2(): Slice { + return self.h; + } + + get fun getAscii(): Int { + return ascii("hello world"); + } + + get fun getAscii2(): Int { + return self.i; + } + + get fun getCrc32(): Int { + return crc32("hello world"); + } + + get fun getCrc32_2(): Int { + return self.j; + } } \ No newline at end of file diff --git a/src/test/e2e-emulated/intrinsics.spec.ts b/src/test/e2e-emulated/intrinsics.spec.ts index 33a378367..d3e1734fc 100644 --- a/src/test/e2e-emulated/intrinsics.spec.ts +++ b/src/test/e2e-emulated/intrinsics.spec.ts @@ -104,5 +104,45 @@ describe("intrinsics", () => { ), ).toBe(sha256("sometest")); expect(await contract.getGetHash4("wallet")).toBe(sha256("wallet")); + + // Check `slice` + expect( + (await contract.getGetSlice()) + .asCell() + .equals( + Cell.fromBase64("te6cckEBAQEADgAAGEhlbGxvIHdvcmxkIXgtxbw="), + ), + ).toBe(true); + expect( + (await contract.getGetSlice2()) + .asCell() + .equals( + Cell.fromBase64("te6cckEBAQEADgAAGEhlbGxvIHdvcmxkIXgtxbw="), + ), + ).toBe(true); + + // Check `rawSlice` + expect( + (await contract.getGetRawSlice()) + .asCell() + .equals(beginCell().storeStringTail("hello world").endCell()), + ).toBe(true); + expect( + (await contract.getGetRawSlice2()) + .asCell() + .equals(beginCell().storeStringTail("hello world").endCell()), + ).toBe(true); + + // Check `ascii` + expect(await contract.getGetAscii()).toBe( + BigInt("0x68656c6c6f20776f726c64"), + ); + expect(await contract.getGetAscii2()).toBe( + BigInt("0x68656c6c6f20776f726c64"), + ); + + // Check `crc32` + expect(await contract.getGetCrc32()).toBe(BigInt(0xd4a1185)); + expect(await contract.getGetCrc32_2()).toBe(BigInt(0xd4a1185)); }); }); diff --git a/src/types/types.ts b/src/types/types.ts index 35256e3ff..e7f6860e4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,4 +1,4 @@ -import { ABIField, Address, Cell } from "@ton/core"; +import { ABIField, Address, Cell, Slice } from "@ton/core"; import { throwInternalCompilerError } from "../errors"; import { AstConstantDef, @@ -78,6 +78,7 @@ export type Value = | string | Address | Cell + | Slice | null | CommentValue | StructValue; @@ -91,7 +92,7 @@ export function showValue(val: Value): string { return val ? "true" : "false"; } else if (Address.isAddress(val)) { return val.toRawString(); - } else if (val instanceof Cell) { + } else if (val instanceof Cell || val instanceof Slice) { return val.toString(); } else if (val === null) { return "null"; diff --git a/yarn.lock b/yarn.lock index 241e26ee2..17a523d0f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2243,6 +2243,11 @@ cosmiconfig@9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" +crc-32@1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + create-jest@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz" From 0d519327484b4ed9337ea03e27aef9c2b8424dcb Mon Sep 17 00:00:00 2001 From: Gusarich Date: Tue, 3 Sep 2024 08:52:07 +0300 Subject: [PATCH 02/19] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 702ff0911..7f18af388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `deepEquals` method for the `Map` type: PR [#637](https://github.com/tact-lang/tact/pull/637) - `asm` bodies for module-level functions: PR [#769](https://github.com/tact-lang/tact/pull/769) - Corresponding stdlib functions for new TVM instructions from 2023.07 and 2024.04 upgrades: PR [#331](https://github.com/tact-lang/tact/pull/331) +- `slice`, `rawSlcie`, `ascii` and `crc32` built-in functions: PR [#787](https://github.com/tact-lang/tact/pull/787) ### Changed From 54a3be42e03750628f3013e4279fe54a63b44eb7 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 18:53:03 +0300 Subject: [PATCH 03/19] fix: return unsigned integer from crc32 --- src/interpreter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index b4e429612..60b5fd719 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1133,7 +1133,8 @@ export class Interpreter { this.interpretExpression(ast.args[0]!), ast.args[0]!.loc, ); - return BigInt(crc32.str(str)); + const c = BigInt(crc32.str(str)); + return c < 0 ? c + 4294967296n : c; } break; case "address": From afcb96cbf4d7d8a816fc61b5af1df5d6b4f1350d Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 18:53:17 +0300 Subject: [PATCH 04/19] chore: add compilation-error test for ascii string length overflow --- src/test/compilation-failed/const-eval-failed.spec.ts | 5 +++++ .../contracts/const-eval-ascii-overflow.tact | 5 +++++ src/test/compilation-failed/tact.config.json | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 src/test/compilation-failed/contracts/const-eval-ascii-overflow.tact diff --git a/src/test/compilation-failed/const-eval-failed.spec.ts b/src/test/compilation-failed/const-eval-failed.spec.ts index 75cee5bbe..a2a7a17c8 100644 --- a/src/test/compilation-failed/const-eval-failed.spec.ts +++ b/src/test/compilation-failed/const-eval-failed.spec.ts @@ -163,4 +163,9 @@ describe("fail-const-eval", () => { errorMessage: "Cannot evaluate expression to a constant: repeat argument must be a number between -2^256 (inclusive) and 2^31 - 1 (inclusive)", }); + itShouldNotCompile({ + testName: "const-eval-ascii-overflow", + errorMessage: + "Cannot evaluate expression to a constant: ascii string is too long, expected up to 32 characters, got 33", + }); }); diff --git a/src/test/compilation-failed/contracts/const-eval-ascii-overflow.tact b/src/test/compilation-failed/contracts/const-eval-ascii-overflow.tact new file mode 100644 index 000000000..c0ff86223 --- /dev/null +++ b/src/test/compilation-failed/contracts/const-eval-ascii-overflow.tact @@ -0,0 +1,5 @@ +contract AsciiOverflow { + get fun getAscii_fail(): Int { + return ascii("000000000000000000000000000000000"); + } +} \ No newline at end of file diff --git a/src/test/compilation-failed/tact.config.json b/src/test/compilation-failed/tact.config.json index cf8132e8f..9939b581f 100644 --- a/src/test/compilation-failed/tact.config.json +++ b/src/test/compilation-failed/tact.config.json @@ -187,6 +187,11 @@ "path": "./contracts/const-eval-repeat-upper-bound.tact", "output": "./contracts/output" }, + { + "name": "const-eval-ascii-overflow", + "path": "./contracts/const-eval-ascii-overflow.tact", + "output": "./contracts/output" + }, { "name": "scope-const-shadows-stdlib-ident", "path": "./contracts/scope-const-shadows-stdlib-ident.tact", From 50a6d22778347af2b814574de1f85e5ab0a991fd Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 18:53:31 +0300 Subject: [PATCH 05/19] chore: change the crc32 testcase to match the one from docs --- src/test/e2e-emulated/contracts/intrinsics.tact | 4 ++-- src/test/e2e-emulated/intrinsics.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/e2e-emulated/contracts/intrinsics.tact b/src/test/e2e-emulated/contracts/intrinsics.tact index 0de691970..399a5ddbf 100644 --- a/src/test/e2e-emulated/contracts/intrinsics.tact +++ b/src/test/e2e-emulated/contracts/intrinsics.tact @@ -9,7 +9,7 @@ contract IntrinsicsTester { g: Slice = slice("te6cckEBAQEADgAAGEhlbGxvIHdvcmxkIXgtxbw="); h: Slice = rawSlice("hello world"); i: Int = ascii("hello world"); - j: Int = crc32("hello world"); + j: Int = crc32("transfer(slice, int)"); init() { @@ -108,7 +108,7 @@ contract IntrinsicsTester { } get fun getCrc32(): Int { - return crc32("hello world"); + return crc32("transfer(slice, int)"); } get fun getCrc32_2(): Int { diff --git a/src/test/e2e-emulated/intrinsics.spec.ts b/src/test/e2e-emulated/intrinsics.spec.ts index d3e1734fc..48a6cbf0c 100644 --- a/src/test/e2e-emulated/intrinsics.spec.ts +++ b/src/test/e2e-emulated/intrinsics.spec.ts @@ -142,7 +142,7 @@ describe("intrinsics", () => { ); // Check `crc32` - expect(await contract.getGetCrc32()).toBe(BigInt(0xd4a1185)); - expect(await contract.getGetCrc32_2()).toBe(BigInt(0xd4a1185)); + expect(await contract.getGetCrc32()).toBe(BigInt(2235694568)); + expect(await contract.getGetCrc32_2()).toBe(BigInt(2235694568)); }); }); From c2b3f52e5b19c4dd7494fbc9f4117d38911aecfb Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 19:11:30 +0300 Subject: [PATCH 06/19] feat: make `rawSlice` use the hex encoding to match the `s` string literal type from FunC --- src/interpreter.ts | 16 +++++++++++++++- .../compilation-failed/const-eval-failed.spec.ts | 5 +++++ .../contracts/const-eval-rawslice-not-hex.tact | 5 +++++ src/test/compilation-failed/tact.config.json | 5 +++++ src/test/e2e-emulated/contracts/intrinsics.tact | 4 ++-- src/test/e2e-emulated/intrinsics.spec.ts | 12 ++++++++++-- 6 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/test/compilation-failed/contracts/const-eval-rawslice-not-hex.tact diff --git a/src/interpreter.ts b/src/interpreter.ts index 60b5fd719..da10cd7d4 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1095,9 +1095,23 @@ export class Interpreter { this.interpretExpression(ast.args[0]!), ast.args[0]!.loc, ); + + if (str.length > 255) { + throwErrorConstEval( + `hex string is too long, expected up to 255 characters, got ${str.length}`, + ast.loc, + ); + } + if (!/^[0-9a-fA-F]*$/.test(str)) { + throwErrorConstEval( + `invalid hex string: ${str}`, + ast.loc, + ); + } + try { return beginCell() - .storeBuffer(Buffer.from(str)) + .storeBuffer(Buffer.from(str, "hex")) .endCell() .asSlice(); } catch (_) { diff --git a/src/test/compilation-failed/const-eval-failed.spec.ts b/src/test/compilation-failed/const-eval-failed.spec.ts index a2a7a17c8..12f59c810 100644 --- a/src/test/compilation-failed/const-eval-failed.spec.ts +++ b/src/test/compilation-failed/const-eval-failed.spec.ts @@ -168,4 +168,9 @@ describe("fail-const-eval", () => { errorMessage: "Cannot evaluate expression to a constant: ascii string is too long, expected up to 32 characters, got 33", }); + itShouldNotCompile({ + testName: "const-eval-rawslice-not-hex", + errorMessage: + "Cannot evaluate expression to a constant: invalid hex string: hello world", + }); }); diff --git a/src/test/compilation-failed/contracts/const-eval-rawslice-not-hex.tact b/src/test/compilation-failed/contracts/const-eval-rawslice-not-hex.tact new file mode 100644 index 000000000..600ab84e0 --- /dev/null +++ b/src/test/compilation-failed/contracts/const-eval-rawslice-not-hex.tact @@ -0,0 +1,5 @@ +contract AsciiOverflow { + get fun getRawSlice_fail(): Slice { + return rawSlice("hello world"); + } +} \ No newline at end of file diff --git a/src/test/compilation-failed/tact.config.json b/src/test/compilation-failed/tact.config.json index 9939b581f..651784aac 100644 --- a/src/test/compilation-failed/tact.config.json +++ b/src/test/compilation-failed/tact.config.json @@ -192,6 +192,11 @@ "path": "./contracts/const-eval-ascii-overflow.tact", "output": "./contracts/output" }, + { + "name": "const-eval-rawslice-not-hex", + "path": "./contracts/const-eval-rawslice-not-hex.tact", + "output": "./contracts/output" + }, { "name": "scope-const-shadows-stdlib-ident", "path": "./contracts/scope-const-shadows-stdlib-ident.tact", diff --git a/src/test/e2e-emulated/contracts/intrinsics.tact b/src/test/e2e-emulated/contracts/intrinsics.tact index 399a5ddbf..d42551e7f 100644 --- a/src/test/e2e-emulated/contracts/intrinsics.tact +++ b/src/test/e2e-emulated/contracts/intrinsics.tact @@ -7,7 +7,7 @@ contract IntrinsicsTester { e: Int = pow(2, 9); f: Int = sha256("hello world"); g: Slice = slice("te6cckEBAQEADgAAGEhlbGxvIHdvcmxkIXgtxbw="); - h: Slice = rawSlice("hello world"); + h: Slice = rawSlice("abcdef"); i: Int = ascii("hello world"); j: Int = crc32("transfer(slice, int)"); @@ -92,7 +92,7 @@ contract IntrinsicsTester { } get fun getRawSlice(): Slice { - return rawSlice("hello world"); + return rawSlice("abcdef"); } get fun getRawSlice2(): Slice { diff --git a/src/test/e2e-emulated/intrinsics.spec.ts b/src/test/e2e-emulated/intrinsics.spec.ts index 48a6cbf0c..1c89ba6db 100644 --- a/src/test/e2e-emulated/intrinsics.spec.ts +++ b/src/test/e2e-emulated/intrinsics.spec.ts @@ -125,12 +125,20 @@ describe("intrinsics", () => { expect( (await contract.getGetRawSlice()) .asCell() - .equals(beginCell().storeStringTail("hello world").endCell()), + .equals( + beginCell() + .storeBuffer(Buffer.from("abcdef", "hex")) + .endCell(), + ), ).toBe(true); expect( (await contract.getGetRawSlice2()) .asCell() - .equals(beginCell().storeStringTail("hello world").endCell()), + .equals( + beginCell() + .storeBuffer(Buffer.from("abcdef", "hex")) + .endCell(), + ), ).toBe(true); // Check `ascii` From c1107e0631662af66ce5d7726ccb707ec04e65e8 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 19:29:38 +0300 Subject: [PATCH 07/19] chore: fix formatting --- src/test/compilation-failed/tact.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/compilation-failed/tact.config.json b/src/test/compilation-failed/tact.config.json index 651784aac..f82528826 100644 --- a/src/test/compilation-failed/tact.config.json +++ b/src/test/compilation-failed/tact.config.json @@ -196,7 +196,7 @@ "name": "const-eval-rawslice-not-hex", "path": "./contracts/const-eval-rawslice-not-hex.tact", "output": "./contracts/output" - }, + }, { "name": "scope-const-shadows-stdlib-ident", "path": "./contracts/scope-const-shadows-stdlib-ident.tact", From 5f3abf3bcb271ae1546bf36443206b05f2caf75b Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 19:56:56 +0300 Subject: [PATCH 08/19] chore: add `rawslice` to cspell ignorelist --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index e6506e475..14545fe94 100644 --- a/cspell.json +++ b/cspell.json @@ -102,6 +102,7 @@ "RANDU", "rangle", "RAWRESERVE", + "rawslice", "renamer", "rparen", "rugpull", From 005bb1f4f87fa9dbf0f0251e1f9f2e7e38731ca4 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 20:19:36 +0300 Subject: [PATCH 09/19] feat: throw in case of empty string in `ascii()` --- src/interpreter.ts | 6 ++++++ src/test/compilation-failed/const-eval-failed.spec.ts | 5 +++++ .../contracts/const-eval-ascii-empty.tact | 5 +++++ src/test/compilation-failed/tact.config.json | 5 +++++ 4 files changed, 21 insertions(+) create mode 100644 src/test/compilation-failed/contracts/const-eval-ascii-empty.tact diff --git a/src/interpreter.ts b/src/interpreter.ts index da10cd7d4..7ecfe8954 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1135,6 +1135,12 @@ export class Interpreter { ast.loc, ); } + if (str.length == 0) { + throwErrorConstEval( + `ascii string cannot be empty`, + ast.loc, + ); + } return BigInt( "0x" + Buffer.from(str, "ascii").toString("hex"), ); diff --git a/src/test/compilation-failed/const-eval-failed.spec.ts b/src/test/compilation-failed/const-eval-failed.spec.ts index 12f59c810..7bcad63f5 100644 --- a/src/test/compilation-failed/const-eval-failed.spec.ts +++ b/src/test/compilation-failed/const-eval-failed.spec.ts @@ -173,4 +173,9 @@ describe("fail-const-eval", () => { errorMessage: "Cannot evaluate expression to a constant: invalid hex string: hello world", }); + itShouldNotCompile({ + testName: "const-eval-ascii-empty", + errorMessage: + "Cannot evaluate expression to a constant: ascii string cannot be empty", + }); }); diff --git a/src/test/compilation-failed/contracts/const-eval-ascii-empty.tact b/src/test/compilation-failed/contracts/const-eval-ascii-empty.tact new file mode 100644 index 000000000..3f1a61f1b --- /dev/null +++ b/src/test/compilation-failed/contracts/const-eval-ascii-empty.tact @@ -0,0 +1,5 @@ +contract AsciiOverflow { + get fun getAscii_fail(): Int { + return ascii(""); + } +} \ No newline at end of file diff --git a/src/test/compilation-failed/tact.config.json b/src/test/compilation-failed/tact.config.json index f82528826..771fae752 100644 --- a/src/test/compilation-failed/tact.config.json +++ b/src/test/compilation-failed/tact.config.json @@ -197,6 +197,11 @@ "path": "./contracts/const-eval-rawslice-not-hex.tact", "output": "./contracts/output" }, + { + "name": "const-eval-ascii-empty", + "path": "./contracts/const-eval-ascii-empty.tact", + "output": "./contracts/output" + }, { "name": "scope-const-shadows-stdlib-ident", "path": "./contracts/scope-const-shadows-stdlib-ident.tact", From e41fac9787799e52565b8d1fc47640fa3f748bf0 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 20:29:27 +0300 Subject: [PATCH 10/19] fix: use `utf-8` instead of `ascii` for `Buffer.from` in `ascii()` --- src/interpreter.ts | 11 +++++------ src/test/compilation-failed/const-eval-failed.spec.ts | 7 ++++++- .../contracts/const-eval-ascii-overflow-2.tact | 5 +++++ src/test/compilation-failed/tact.config.json | 5 +++++ 4 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 src/test/compilation-failed/contracts/const-eval-ascii-overflow-2.tact diff --git a/src/interpreter.ts b/src/interpreter.ts index 7ecfe8954..538ee2909 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1129,21 +1129,20 @@ export class Interpreter { this.interpretExpression(ast.args[0]!), ast.args[0]!.loc, ); - if (str.length > 32) { + const hex = Buffer.from(str).toString("hex"); + if (hex.length > 32) { throwErrorConstEval( - `ascii string is too long, expected up to 32 characters, got ${str.length}`, + `ascii string is too long, expected up to 32 bytes, got ${Math.floor(hex.length / 2)}`, ast.loc, ); } - if (str.length == 0) { + if (hex.length == 0) { throwErrorConstEval( `ascii string cannot be empty`, ast.loc, ); } - return BigInt( - "0x" + Buffer.from(str, "ascii").toString("hex"), - ); + return BigInt("0x" + hex); } break; case "crc32": diff --git a/src/test/compilation-failed/const-eval-failed.spec.ts b/src/test/compilation-failed/const-eval-failed.spec.ts index 7bcad63f5..dc300df02 100644 --- a/src/test/compilation-failed/const-eval-failed.spec.ts +++ b/src/test/compilation-failed/const-eval-failed.spec.ts @@ -166,7 +166,12 @@ describe("fail-const-eval", () => { itShouldNotCompile({ testName: "const-eval-ascii-overflow", errorMessage: - "Cannot evaluate expression to a constant: ascii string is too long, expected up to 32 characters, got 33", + "Cannot evaluate expression to a constant: ascii string is too long, expected up to 32 bytes, got 33", + }); + itShouldNotCompile({ + testName: "const-eval-ascii-overflow-2", + errorMessage: + "Cannot evaluate expression to a constant: ascii string is too long, expected up to 32 bytes, got 33", }); itShouldNotCompile({ testName: "const-eval-rawslice-not-hex", diff --git a/src/test/compilation-failed/contracts/const-eval-ascii-overflow-2.tact b/src/test/compilation-failed/contracts/const-eval-ascii-overflow-2.tact new file mode 100644 index 000000000..b7f5788f3 --- /dev/null +++ b/src/test/compilation-failed/contracts/const-eval-ascii-overflow-2.tact @@ -0,0 +1,5 @@ +contract AsciiOverflow { + get fun getAscii_fail(): Int { + return ascii("⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡"); + } +} \ No newline at end of file diff --git a/src/test/compilation-failed/tact.config.json b/src/test/compilation-failed/tact.config.json index 771fae752..80438b28c 100644 --- a/src/test/compilation-failed/tact.config.json +++ b/src/test/compilation-failed/tact.config.json @@ -192,6 +192,11 @@ "path": "./contracts/const-eval-ascii-overflow.tact", "output": "./contracts/output" }, + { + "name": "const-eval-ascii-overflow-2", + "path": "./contracts/const-eval-ascii-overflow-2.tact", + "output": "./contracts/output" + }, { "name": "const-eval-rawslice-not-hex", "path": "./contracts/const-eval-rawslice-not-hex.tact", From b580258c3cf77509e96df095057a51bee265b3ae Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 20:29:40 +0300 Subject: [PATCH 11/19] chore: add more intrinsics test cases --- .../e2e-emulated/contracts/intrinsics.tact | 18 ++++++++++++++++++ src/test/e2e-emulated/intrinsics.spec.ts | 12 ++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/test/e2e-emulated/contracts/intrinsics.tact b/src/test/e2e-emulated/contracts/intrinsics.tact index d42551e7f..15fbec4f8 100644 --- a/src/test/e2e-emulated/contracts/intrinsics.tact +++ b/src/test/e2e-emulated/contracts/intrinsics.tact @@ -10,6 +10,8 @@ contract IntrinsicsTester { h: Slice = rawSlice("abcdef"); i: Int = ascii("hello world"); j: Int = crc32("transfer(slice, int)"); + k: Int = crc32(""); + l: Int = ascii("⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡"); init() { @@ -107,6 +109,14 @@ contract IntrinsicsTester { return self.i; } + get fun getAscii3(): Int { + return ascii("⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡"); + } + + get fun getAscii4(): Int { + return self.l; + } + get fun getCrc32(): Int { return crc32("transfer(slice, int)"); } @@ -114,4 +124,12 @@ contract IntrinsicsTester { get fun getCrc32_2(): Int { return self.j; } + + get fun getCrc32_3(): Int { + return self.k; + } + + get fun getCrc32_4(): Int { + return crc32(""); + } } \ No newline at end of file diff --git a/src/test/e2e-emulated/intrinsics.spec.ts b/src/test/e2e-emulated/intrinsics.spec.ts index 1c89ba6db..becc66595 100644 --- a/src/test/e2e-emulated/intrinsics.spec.ts +++ b/src/test/e2e-emulated/intrinsics.spec.ts @@ -148,9 +148,21 @@ describe("intrinsics", () => { expect(await contract.getGetAscii2()).toBe( BigInt("0x68656c6c6f20776f726c64"), ); + expect(await contract.getGetAscii3()).toBe( + BigInt( + "1563963554659859369353828835329962428465513941646011501275668087180532385", + ), + ); + expect(await contract.getGetAscii4()).toBe( + BigInt( + "1563963554659859369353828835329962428465513941646011501275668087180532385", + ), + ); // Check `crc32` expect(await contract.getGetCrc32()).toBe(BigInt(2235694568)); expect(await contract.getGetCrc32_2()).toBe(BigInt(2235694568)); + expect(await contract.getGetCrc32_3()).toBe(0n); + expect(await contract.getGetCrc32_4()).toBe(0n); }); }); From bcfc64ad6c93810bd1cda87298e84f4ae19fbf5a Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 20:30:01 +0300 Subject: [PATCH 12/19] chore: fix formatting --- src/test/compilation-failed/tact.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/compilation-failed/tact.config.json b/src/test/compilation-failed/tact.config.json index 80438b28c..3c5740529 100644 --- a/src/test/compilation-failed/tact.config.json +++ b/src/test/compilation-failed/tact.config.json @@ -206,7 +206,7 @@ "name": "const-eval-ascii-empty", "path": "./contracts/const-eval-ascii-empty.tact", "output": "./contracts/output" - }, + }, { "name": "scope-const-shadows-stdlib-ident", "path": "./contracts/scope-const-shadows-stdlib-ident.tact", From 29f582d23a07980e1c7d3737257c6024109d8acf Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 20:33:45 +0300 Subject: [PATCH 13/19] fix: `64` hex digits instead of `32` for overflow check --- src/interpreter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index 538ee2909..e44bdd731 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1130,7 +1130,7 @@ export class Interpreter { ast.args[0]!.loc, ); const hex = Buffer.from(str).toString("hex"); - if (hex.length > 32) { + if (hex.length > 64) { throwErrorConstEval( `ascii string is too long, expected up to 32 bytes, got ${Math.floor(hex.length / 2)}`, ast.loc, From f16386eee18b27d2035bb504cf360d86a352c757 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 20:37:26 +0300 Subject: [PATCH 14/19] chore: negative test case for `rawSlice` overflow --- src/test/compilation-failed/const-eval-failed.spec.ts | 5 +++++ .../contracts/const-eval-rawslice-overflow.tact | 5 +++++ src/test/compilation-failed/tact.config.json | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 src/test/compilation-failed/contracts/const-eval-rawslice-overflow.tact diff --git a/src/test/compilation-failed/const-eval-failed.spec.ts b/src/test/compilation-failed/const-eval-failed.spec.ts index dc300df02..a04aa4257 100644 --- a/src/test/compilation-failed/const-eval-failed.spec.ts +++ b/src/test/compilation-failed/const-eval-failed.spec.ts @@ -178,6 +178,11 @@ describe("fail-const-eval", () => { errorMessage: "Cannot evaluate expression to a constant: invalid hex string: hello world", }); + itShouldNotCompile({ + testName: "const-eval-rawslice-overflow", + errorMessage: + "Cannot evaluate expression to a constant: hex string is too long, expected up to 255 characters, got 256", + }); itShouldNotCompile({ testName: "const-eval-ascii-empty", errorMessage: diff --git a/src/test/compilation-failed/contracts/const-eval-rawslice-overflow.tact b/src/test/compilation-failed/contracts/const-eval-rawslice-overflow.tact new file mode 100644 index 000000000..188e7ec57 --- /dev/null +++ b/src/test/compilation-failed/contracts/const-eval-rawslice-overflow.tact @@ -0,0 +1,5 @@ +contract AsciiOverflow { + get fun getRawSlice_fail(): Slice { + return rawSlice("abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"); + } +} \ No newline at end of file diff --git a/src/test/compilation-failed/tact.config.json b/src/test/compilation-failed/tact.config.json index 3c5740529..a3b546e2d 100644 --- a/src/test/compilation-failed/tact.config.json +++ b/src/test/compilation-failed/tact.config.json @@ -202,6 +202,11 @@ "path": "./contracts/const-eval-rawslice-not-hex.tact", "output": "./contracts/output" }, + { + "name": "const-eval-rawslice-overflow", + "path": "./contracts/const-eval-rawslice-overflow.tact", + "output": "./contracts/output" +}, { "name": "const-eval-ascii-empty", "path": "./contracts/const-eval-ascii-empty.tact", From f398057c9e364ae8db8451646b26f6101785effa Mon Sep 17 00:00:00 2001 From: Gusarich Date: Wed, 4 Sep 2024 20:43:59 +0300 Subject: [PATCH 15/19] chore: add `rawSlice("")` test --- src/test/e2e-emulated/contracts/intrinsics.tact | 9 +++++++++ src/test/e2e-emulated/intrinsics.spec.ts | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/test/e2e-emulated/contracts/intrinsics.tact b/src/test/e2e-emulated/contracts/intrinsics.tact index 15fbec4f8..c4b8743cb 100644 --- a/src/test/e2e-emulated/contracts/intrinsics.tact +++ b/src/test/e2e-emulated/contracts/intrinsics.tact @@ -12,6 +12,7 @@ contract IntrinsicsTester { j: Int = crc32("transfer(slice, int)"); k: Int = crc32(""); l: Int = ascii("⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡"); + m: Slice = rawSlice(""); init() { @@ -101,6 +102,14 @@ contract IntrinsicsTester { return self.h; } + get fun getRawSlice3(): Slice { + return rawSlice(""); + } + + get fun getRawSlice4(): Slice { + return self.m; + } + get fun getAscii(): Int { return ascii("hello world"); } diff --git a/src/test/e2e-emulated/intrinsics.spec.ts b/src/test/e2e-emulated/intrinsics.spec.ts index becc66595..a2ced3ad0 100644 --- a/src/test/e2e-emulated/intrinsics.spec.ts +++ b/src/test/e2e-emulated/intrinsics.spec.ts @@ -140,6 +140,12 @@ describe("intrinsics", () => { .endCell(), ), ).toBe(true); + expect( + (await contract.getGetRawSlice3()).asCell().equals(Cell.EMPTY), + ).toBe(true); + expect( + (await contract.getGetRawSlice4()).asCell().equals(Cell.EMPTY), + ).toBe(true); // Check `ascii` expect(await contract.getGetAscii()).toBe( From d6b1e1b8ff9ec26fd1b5f18ac8c1e470eee2b683 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 5 Sep 2024 09:48:13 +0300 Subject: [PATCH 16/19] chore: make crc32 convertion to unsigned more clear --- src/interpreter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/interpreter.ts b/src/interpreter.ts index e44bdd731..e24d86b6b 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -1152,8 +1152,7 @@ export class Interpreter { this.interpretExpression(ast.args[0]!), ast.args[0]!.loc, ); - const c = BigInt(crc32.str(str)); - return c < 0 ? c + 4294967296n : c; + return BigInt(crc32.str(str) >>> 0); // >>> 0 converts to unsigned } break; case "address": From 816068a08bcfc84768633cc381e75ccd0291262a Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 5 Sep 2024 09:48:41 +0300 Subject: [PATCH 17/19] chore: fix formatting --- src/test/compilation-failed/tact.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/compilation-failed/tact.config.json b/src/test/compilation-failed/tact.config.json index a3b546e2d..31b35a1db 100644 --- a/src/test/compilation-failed/tact.config.json +++ b/src/test/compilation-failed/tact.config.json @@ -206,7 +206,7 @@ "name": "const-eval-rawslice-overflow", "path": "./contracts/const-eval-rawslice-overflow.tact", "output": "./contracts/output" -}, + }, { "name": "const-eval-ascii-empty", "path": "./contracts/const-eval-ascii-empty.tact", From 70946f713837b2350b3c4d42a58ed23a9003d716 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 5 Sep 2024 09:49:26 +0300 Subject: [PATCH 18/19] chore: add test string to cspell ignorelist --- cspell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index 14545fe94..a070f6ee0 100644 --- a/cspell.json +++ b/cspell.json @@ -145,7 +145,8 @@ "unixfs", "workchain", "xffff", - "привет" + "привет", + "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd" ], "flagWords": [], "ignorePaths": [ From a9fadbdcb44a1d047b03842a977ec83704110782 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 5 Sep 2024 09:55:28 +0300 Subject: [PATCH 19/19] chore: fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f18af388..a2ce0495d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `deepEquals` method for the `Map` type: PR [#637](https://github.com/tact-lang/tact/pull/637) - `asm` bodies for module-level functions: PR [#769](https://github.com/tact-lang/tact/pull/769) - Corresponding stdlib functions for new TVM instructions from 2023.07 and 2024.04 upgrades: PR [#331](https://github.com/tact-lang/tact/pull/331) -- `slice`, `rawSlcie`, `ascii` and `crc32` built-in functions: PR [#787](https://github.com/tact-lang/tact/pull/787) +- `slice`, `rawSlice`, `ascii` and `crc32` built-in functions: PR [#787](https://github.com/tact-lang/tact/pull/787) ### Changed