Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: slice, rawSlice, ascii and crc32 built-in functions #787

Merged
merged 19 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`, `rawSlice`, `ascii` and `crc32` built-in functions: PR [#787](https://github.com/tact-lang/tact/pull/787)

### Changed

Expand Down
4 changes: 3 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"RANDU",
"rangle",
"RAWRESERVE",
"rawslice",
"renamer",
"rparen",
"rugpull",
Expand Down Expand Up @@ -144,7 +145,8 @@
"unixfs",
"workchain",
"xffff",
"привет"
"привет",
"abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"
],
"flagWords": [],
"ignorePaths": [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
189 changes: 187 additions & 2 deletions src/abi/global.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -398,4 +402,185 @@ export const GlobalFunctions: Map<string, AbiFunction> = 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`;
},
},
],
]);
12 changes: 11 additions & 1 deletion src/generator/writers/writeConstant.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion src/generator/writers/writeExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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()";
}
Expand Down
87 changes: 86 additions & 1 deletion src/interpreter.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -1070,6 +1071,90 @@ 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,
);

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, "hex"))
.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,
);
const hex = Buffer.from(str).toString("hex");
if (hex.length > 64) {
throwErrorConstEval(
`ascii string is too long, expected up to 32 bytes, got ${Math.floor(hex.length / 2)}`,
ast.loc,
);
}
if (hex.length == 0) {
throwErrorConstEval(
`ascii string cannot be empty`,
ast.loc,
);
}
return BigInt("0x" + 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) >>> 0); // >>> 0 converts to unsigned
}
break;
case "address":
{
ensureFunArity(1, ast.args, ast.loc);
Expand Down
Loading
Loading