From 1202ded43248611cb711c9b7d79a3703826e2894 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 5 Sep 2024 11:22:50 +0300 Subject: [PATCH 1/6] feat: grammar --- .../__snapshots__/grammar.spec.ts.snap | 189 ++++++++++++++++++ src/grammar/ast.ts | 9 + src/grammar/grammar.ohm | 3 + src/grammar/grammar.ts | 8 + src/grammar/test/exotic-cells.tact | 9 + src/prettyPrinter.ts | 7 + src/types/resolveDescriptors.ts | 35 ++++ src/types/resolveExpression.ts | 1 + src/types/types.ts | 13 ++ 9 files changed, 274 insertions(+) create mode 100644 src/grammar/test/exotic-cells.tact diff --git a/src/grammar/__snapshots__/grammar.spec.ts.snap b/src/grammar/__snapshots__/grammar.spec.ts.snap index 0d66af2a2..b89612e14 100644 --- a/src/grammar/__snapshots__/grammar.spec.ts.snap +++ b/src/grammar/__snapshots__/grammar.spec.ts.snap @@ -1665,6 +1665,195 @@ exports[`grammar should parse contract-with-trait-string-literal 1`] = ` } `; +exports[`grammar should parse exotic-cells 1`] = ` +{ + "id": 29, + "imports": [], + "items": [ + { + "fields": [ + { + "as": { + "id": 4, + "kind": "id", + "loc": uint32, + "text": "uint32", + }, + "id": 5, + "initializer": null, + "kind": "field_decl", + "loc": x: Int as uint32, + "name": { + "id": 2, + "kind": "id", + "loc": x, + "text": "x", + }, + "type": { + "id": 3, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + { + "as": null, + "id": 8, + "initializer": null, + "kind": "field_decl", + "loc": y: Int, + "name": { + "id": 6, + "kind": "id", + "loc": y, + "text": "y", + }, + "type": { + "id": 7, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + { + "as": null, + "id": 13, + "initializer": null, + "kind": "field_decl", + "loc": z: map, + "name": { + "id": 9, + "kind": "id", + "loc": z, + "text": "z", + }, + "type": { + "id": 12, + "keyStorageType": null, + "keyType": { + "id": 10, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + "kind": "map_type", + "loc": map, + "valueStorageType": null, + "valueType": { + "id": 11, + "kind": "type_id", + "loc": Int, + "text": "Int", + }, + }, + }, + ], + "id": 14, + "kind": "struct_decl", + "loc": struct S { + x: Int as uint32; + y: Int; + z: map; +}, + "name": { + "id": 1, + "kind": "type_id", + "loc": S, + "text": "S", + }, + }, + { + "attributes": [], + "id": 28, + "kind": "function_def", + "loc": fun test(p: merkleProof, u: merkleUpdate, l: libraryReference) { + +}, + "name": { + "id": 15, + "kind": "id", + "loc": test, + "text": "test", + }, + "params": [ + { + "id": 19, + "kind": "typed_parameter", + "loc": p: merkleProof, + "name": { + "id": 16, + "kind": "id", + "loc": p, + "text": "p", + }, + "type": { + "id": 18, + "kind": "exotic_type", + "loc": merkleProof, + "name": "merkleProof", + "struct": { + "id": 17, + "kind": "type_id", + "loc": S, + "text": "S", + }, + }, + }, + { + "id": 23, + "kind": "typed_parameter", + "loc": u: merkleUpdate, + "name": { + "id": 20, + "kind": "id", + "loc": u, + "text": "u", + }, + "type": { + "id": 22, + "kind": "exotic_type", + "loc": merkleUpdate, + "name": "merkleUpdate", + "struct": { + "id": 21, + "kind": "type_id", + "loc": S, + "text": "S", + }, + }, + }, + { + "id": 27, + "kind": "typed_parameter", + "loc": l: libraryReference, + "name": { + "id": 24, + "kind": "id", + "loc": l, + "text": "l", + }, + "type": { + "id": 26, + "kind": "exotic_type", + "loc": libraryReference, + "name": "libraryReference", + "struct": { + "id": 25, + "kind": "type_id", + "loc": S, + "text": "S", + }, + }, + }, + ], + "return": null, + "statements": [], + }, + ], + "kind": "module", +} +`; + exports[`grammar should parse expr-arith 1`] = ` { "id": 12, diff --git a/src/grammar/ast.ts b/src/grammar/ast.ts index 13bc91ff4..b2f162532 100644 --- a/src/grammar/ast.ts +++ b/src/grammar/ast.ts @@ -324,6 +324,7 @@ export type AstType = | AstTypeId | AstOptionalType | AstMapType + | AstExoticType | AstBouncedMessageType; export type AstTypeId = { @@ -350,6 +351,14 @@ export type AstMapType = { loc: SrcInfo; }; +export type AstExoticType = { + kind: "exotic_type"; + name: string; + struct: AstTypeId; + id: number; + loc: SrcInfo; +}; + export type AstBouncedMessageType = { kind: "bounced_message_type"; messageType: AstTypeId; diff --git a/src/grammar/grammar.ohm b/src/grammar/grammar.ohm index e2c821e05..7de2f1c43 100644 --- a/src/grammar/grammar.ohm +++ b/src/grammar/grammar.ohm @@ -31,7 +31,10 @@ Tact { Type = typeId "?" --optional | typeId --regular | map "<" typeId (as id)? "," typeId (as id)? ">" --map + | exoticType "<" typeId ">" --exotic | "bounced" "<" typeId ">" --bounced + + exoticType = "merkleProof" | "merkleUpdate" | "libraryReference" FieldDecl = id ":" Type (as id)? ("=" Expression)? diff --git a/src/grammar/grammar.ts b/src/grammar/grammar.ts index 657cd81ec..d634b1d6c 100644 --- a/src/grammar/grammar.ts +++ b/src/grammar/grammar.ts @@ -935,6 +935,14 @@ semantics.addOperation("astOfType", { loc: createRef(this), }); }, + Type_exotic(name, _langle, structName, _rangle) { + return createAstNode({ + kind: "exotic_type", + name: name.sourceString, + struct: structName.astOfType(), + loc: createRef(this), + }); + }, Type_bounced(_bouncedKwd, _langle, typeId, _rangle) { return createAstNode({ kind: "bounced_message_type", diff --git a/src/grammar/test/exotic-cells.tact b/src/grammar/test/exotic-cells.tact new file mode 100644 index 000000000..a309bb9b0 --- /dev/null +++ b/src/grammar/test/exotic-cells.tact @@ -0,0 +1,9 @@ +struct S { + x: Int as uint32; + y: Int; + z: map; +} + +fun test(p: merkleProof, u: merkleUpdate, l: libraryReference) { + +} \ No newline at end of file diff --git a/src/prettyPrinter.ts b/src/prettyPrinter.ts index b70e69e01..baf6cefb1 100644 --- a/src/prettyPrinter.ts +++ b/src/prettyPrinter.ts @@ -47,6 +47,7 @@ import { AstAsmInstruction, AstAsmShuffle, astNumToString, + AstExoticType, } from "./grammar/ast"; import { throwInternalCompilerError } from "./errors"; import JSONbig from "json-bigint"; @@ -94,6 +95,8 @@ export class PrettyPrinter { return this.ppAstBouncedMessageType(typeRef); case "optional_type": return this.ppAstOptionalType(typeRef); + case "exotic_type": + return this.ppAstExoticType(typeRef); } } @@ -115,6 +118,10 @@ export class PrettyPrinter { return `map<${this.ppAstTypeId(typeRef.keyType)}${keyAlias}, ${this.ppAstTypeId(typeRef.valueType)}${valueAlias}>`; } + ppAstExoticType(typeRef: AstExoticType): string { + return `${typeRef.name}<${this.ppAstTypeId(typeRef.struct)}>`; + } + ppAstBouncedMessageType(typeRef: AstBouncedMessageType): string { return `bounced<${this.ppAstTypeId(typeRef.messageType)}>`; } diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index dc55b0dea..dc17b7255 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -176,6 +176,21 @@ export function resolveTypeRef(ctx: CompilerContext, type: AstType): TypeRef { name: t.name, }; } + case "exotic_type": { + const s = getType(ctx, idText(src.struct)); + if (src.name !== "merkleProof") { + // Should never happen because of grammar + throwCompilationError( + `Unknown exotic type ${src.name}`, + src.loc, + ); + } + return { + kind: "exotic", + name: src.name, + struct: s.name, + }; + } } } @@ -251,6 +266,26 @@ function buildTypeRef( name: idText(type.messageType), }; } + case "exotic_type": { + if (!types.has(idText(src.struct))) { + throwCompilationError( + `Type ${idTextErr(src.struct)} not found`, + src.loc, + ); + } + if (src.name !== "merkleProof") { + // Should never happen because of grammar + throwCompilationError( + `Unknown exotic type ${src.name}`, + src.loc, + ); + } + return { + kind: "exotic", + name: src.name, + struct: idText(src.struct), + }; + } } } diff --git a/src/types/resolveExpression.ts b/src/types/resolveExpression.ts index 95190249c..d77baf32e 100644 --- a/src/types/resolveExpression.ts +++ b/src/types/resolveExpression.ts @@ -315,6 +315,7 @@ function isEqualityType(ctx: CompilerContext, ty: TypeRef): boolean { } case "null": case "map": + case "exotic": return true; case "void": case "ref_bounced": diff --git a/src/types/types.ts b/src/types/types.ts index e7f6860e4..aa8246ea5 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -50,6 +50,17 @@ export type TypeRef = value: string; valueAs: string | null; } + | { + kind: "exotic"; + name: "merkleProof"; + struct: string; + } + // TODO: + // | { + // kind: "exotic"; + // name: "merkleUpdate"; + // struct: string; + // } | { kind: "ref_bounced"; name: string; @@ -262,6 +273,8 @@ export function printTypeRef(src: TypeRef): string { return `${src.name}${src.optional ? "?" : ""}`; case "map": return `map<${src.key + (src.keyAs ? " as " + src.keyAs : "")}, ${src.value + (src.valueAs ? " as " + src.valueAs : "")}>`; + case "exotic": + return `${src.name}<${src.struct}>`; case "void": return ""; case "null": From 64cd7b8e03ccc703893fdcfe03fc8f19f50ff2c3 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Thu, 12 Sep 2024 10:48:24 +0300 Subject: [PATCH 2/6] chore: add airdrop e2e test --- src/test/e2e-emulated/contracts/exotics.tact | 52 ++++++++++++++++++++ tact.config.json | 5 ++ 2 files changed, 57 insertions(+) create mode 100644 src/test/e2e-emulated/contracts/exotics.tact diff --git a/src/test/e2e-emulated/contracts/exotics.tact b/src/test/e2e-emulated/contracts/exotics.tact new file mode 100644 index 000000000..cee4d02ce --- /dev/null +++ b/src/test/e2e-emulated/contracts/exotics.tact @@ -0,0 +1,52 @@ +import "@stdlib/deploy"; + +struct AirdropEntry { + address: Address; + amount: Int as coins; +} + +struct ProvableData { + justForTestPurposes: Int; + entries: map; + anoterOne: Int as uint32; +} + +message ProcessClaim { + queryId: Int as uint64; + proof: merkleProof; + index: Int as uint256; +} + +message TestResponse { + queryId: Int as uint64; + rootHash: Int as uint256; + depth: Int as uint16; + data: ProvableData; + entry: AirdropEntry; +} + +contract MerkleTreesTestContract with Deployable { + rootHash: Int; + + init(rootHash: Int) { + self.rootHash = rootHash; + } + + receive(msg: ProcessClaim) { + require(msg.proof.rootHash == self.rootHash, "wrong merkle root"); + let entry: AirdropEntry = msg.proof.data.entries.get(msg.index)!!; + + send(SendParameters{ + to: entry.address, + value: 0, + mode: SendIgnoreErrors, + body: TestResponse { + queryId: msg.queryId, + rootHash: msg.proof.rootHash, + depth: msg.proof.depth, + data: msg.proof.data, + entry: entry + }.toCell() + }); + } +} \ No newline at end of file diff --git a/tact.config.json b/tact.config.json index 187785edf..e04d56f2e 100644 --- a/tact.config.json +++ b/tact.config.json @@ -410,6 +410,11 @@ "name": "semantics", "path": "./src/test/e2e-emulated/contracts/semantics.tact", "output": "./src/test/e2e-emulated/contracts/output" + }, + { + "name": "exotics", + "path": "./src/test/e2e-emulated/contracts/exotics.tact", + "output": "./src/test/e2e-emulated/contracts/output" } ] } From 435ac214899af1b0d8ade52893f39bd5852838b9 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Fri, 13 Sep 2024 14:08:12 +0300 Subject: [PATCH 3/6] wip --- src/abi/errors.ts | 8 +++++ src/generator/writers/resolveFuncFlatPack.ts | 2 ++ src/generator/writers/resolveFuncFlatTypes.ts | 2 ++ src/generator/writers/resolveFuncType.ts | 12 +++++++ .../writers/resolveFuncTypeFromAbi.ts | 12 +++++++ .../writers/resolveFuncTypeFromAbiUnpack.ts | 2 ++ .../writers/resolveFuncTypeUnpack.ts | 17 ++++++++++ src/generator/writers/writeExpression.ts | 17 +++++++++- src/generator/writers/writeSerialization.ts | 25 ++++++++++++-- src/imports/stdlib.ts | 3 +- src/storage/allocator.ts | 19 +++++++++++ src/storage/operation.ts | 4 +++ src/types/resolveABITypeRef.ts | 34 +++++++++++++++++++ src/types/resolveDescriptors.ts | 32 ++++++++++------- src/types/resolveExpression.ts | 33 +++++++++++++++++- src/types/resolveSignatures.ts | 7 ++++ stdlib/stdlib.fc | 2 ++ 17 files changed, 213 insertions(+), 18 deletions(-) diff --git a/src/abi/errors.ts b/src/abi/errors.ts index ee04f2fc8..598cf7748 100644 --- a/src/abi/errors.ts +++ b/src/abi/errors.ts @@ -14,4 +14,12 @@ export const contractErrors = { id: 137, message: "Masterchain support is not enabled for this contract", }, + expectedExoticCell: { + id: 138, + message: "Expected exotic cell, got regular cell", + }, + invalidExoticCellType: { + id: 139, + message: "Invalid exotic cell type", + }, }; diff --git a/src/generator/writers/resolveFuncFlatPack.ts b/src/generator/writers/resolveFuncFlatPack.ts index f0480bc5a..d69004563 100644 --- a/src/generator/writers/resolveFuncFlatPack.ts +++ b/src/generator/writers/resolveFuncFlatPack.ts @@ -51,6 +51,8 @@ export function resolveFuncFlatPack( resolveFuncFlatPack(v.type, name + `'` + v.name, ctx), ); } + } else if (descriptor.kind === "exotic") { + return ["int", "int", "cell"]; } // Unreachable diff --git a/src/generator/writers/resolveFuncFlatTypes.ts b/src/generator/writers/resolveFuncFlatTypes.ts index 2f829a2ba..87d1565d1 100644 --- a/src/generator/writers/resolveFuncFlatTypes.ts +++ b/src/generator/writers/resolveFuncFlatTypes.ts @@ -50,6 +50,8 @@ export function resolveFuncFlatTypes( resolveFuncFlatTypes(v.type, ctx), ); } + } else if (descriptor.kind === "exotic") { + return ["int", "int", "cell"]; } // Unreachable diff --git a/src/generator/writers/resolveFuncType.ts b/src/generator/writers/resolveFuncType.ts index a5b920636..b28e39189 100644 --- a/src/generator/writers/resolveFuncType.ts +++ b/src/generator/writers/resolveFuncType.ts @@ -1,3 +1,4 @@ +import { deserialize } from "node:v8"; import { getType } from "../../types/resolveDescriptors"; import { TypeDescription, TypeRef } from "../../types/types"; import { WriterContext } from "../Writer"; @@ -94,6 +95,17 @@ export function resolveFuncType( ")" ); } + } else if (descriptor.kind === "exotic") { + const t = getType(ctx.ctx, descriptor.struct); + return ( + "(int, int, " + + t.fields + .map((v) => + resolveFuncType(v.type, ctx, false, usePartialFields), + ) + .join(", ") + + ")" + ); } // Unreachable diff --git a/src/generator/writers/resolveFuncTypeFromAbi.ts b/src/generator/writers/resolveFuncTypeFromAbi.ts index c45b7b5da..b6281a346 100644 --- a/src/generator/writers/resolveFuncTypeFromAbi.ts +++ b/src/generator/writers/resolveFuncTypeFromAbi.ts @@ -1,6 +1,7 @@ import { ABITypeRef } from "@ton/core"; import { getType } from "../../types/resolveDescriptors"; import { WriterContext } from "../Writer"; +import { throwInternalCompilerError } from "../../errors"; export function resolveFuncTypeFromAbi( fields: ABITypeRef[], @@ -36,6 +37,17 @@ export function resolveFuncTypeFromAbi( res.push("slice"); } else if (f.type === "string") { res.push("slice"); + } else if (f.type === "merkleProof") { + if (typeof f.format !== "string") { + throwInternalCompilerError( + "Expected string format for merkleProof", + ); // should never happen + } + const t = getType(ctx.ctx, f.format); + res.push("int"); + res.push("int"); + const loaded = t.fields.map((v) => v.abi.type); + res.push(resolveFuncTypeFromAbi(loaded, ctx)); } else { const t = getType(ctx.ctx, f.type); if (t.kind !== "struct") { diff --git a/src/generator/writers/resolveFuncTypeFromAbiUnpack.ts b/src/generator/writers/resolveFuncTypeFromAbiUnpack.ts index 4652570d7..1b9f6f5a2 100644 --- a/src/generator/writers/resolveFuncTypeFromAbiUnpack.ts +++ b/src/generator/writers/resolveFuncTypeFromAbiUnpack.ts @@ -38,6 +38,8 @@ export function resolveFuncTypeFromAbiUnpack( res.push(`${name}'${f.name}`); } else if (f.type.type === "string") { res.push(`${name}'${f.name}`); + } else if (f.type.type === "merkleProof") { + res.push(`${name}'${f.name}`); } else { const t = getType(ctx.ctx, f.type.type); if (f.type.optional ?? t.fields.length === 0) { diff --git a/src/generator/writers/resolveFuncTypeUnpack.ts b/src/generator/writers/resolveFuncTypeUnpack.ts index dd088d8b0..65b17b77a 100644 --- a/src/generator/writers/resolveFuncTypeUnpack.ts +++ b/src/generator/writers/resolveFuncTypeUnpack.ts @@ -92,6 +92,23 @@ export function resolveFuncTypeUnpack( ")" ); } + } else if (descriptor.kind === "exotic") { + const t = getType(ctx.ctx, descriptor.struct); + return ( + "(int, int, " + + t.fields + .map((v) => + resolveFuncTypeUnpack( + v.type, + name + `'` + v.name, + ctx, + false, + usePartialFields, + ), + ) + .join(", ") + + ")" + ); } // Unreachable diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index 937d3144c..de6dde0dc 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -432,13 +432,28 @@ export function writeExpression(f: AstExpression, wCtx: WriterContext): string { const src = getExpType(wCtx.ctx, f.aggregate); if ( (src.kind !== "ref" || src.optional) && - src.kind !== "ref_bounced" + src.kind !== "ref_bounced" && + src.kind !== "exotic" ) { throwCompilationError( `Cannot access field of non-struct type: "${printTypeRef(src)}"`, f.loc, ); } + + if (src.kind === "exotic") { + // exotics have fields `rootHash` (int), `depth` (int) and `data` (arbitrary struct) + // they are accessed exactly the same as struct fields + const path = tryExtractPath(f); + if (path) { + return writePathExpression(path); + } + throwCompilationError( + `Cannot access field of exotic type: "${printTypeRef(src)}"`, + f.loc, + ); + } + const srcT = getType(wCtx.ctx, src.name); // Resolve field diff --git a/src/generator/writers/writeSerialization.ts b/src/generator/writers/writeSerialization.ts index aca186622..b38c6a19b 100644 --- a/src/generator/writers/writeSerialization.ts +++ b/src/generator/writers/writeSerialization.ts @@ -305,9 +305,11 @@ function writeSerializerField( } return; } + case "merkle-proof": { + ctx.append(`build_${gen} = build_${gen}.store_ref(${fieldName});`); + return; + } } - - throwInternalCompilerError(`Unsupported field kind`, dummySrcInfo); } // @@ -643,5 +645,24 @@ function writeFieldParser( } return; } + case "merkle-proof": { + const name = `v'${f.name}`; + ctx.append( + `var (${name}, exotic?) = sc_${gen}~load_ref().begin_parse_exotic();`, + ); + ctx.append( + `throw_unless(${contractErrors.expectedExoticCell.id}, exotic?);`, + ); + ctx.append( + `throw_unless(${contractErrors.invalidExoticCellType.id}, ${name}~load_uint(8) == 3);`, + ); + ctx.append(`var ${name}'rootHash = ${name}~load_uint(256);`); + ctx.append(`var ${name}'depth = ${name}~load_uint(16);`); + ctx.append(`var sc = ${name}~load_ref().begin_parse();`); + ctx.append( + `var ${name}'data = sc~${ops.reader(op.struct, ctx)}();`, + ); + return; + } } } diff --git a/src/imports/stdlib.ts b/src/imports/stdlib.ts index 91df41b6a..41cf0a218 100644 --- a/src/imports/stdlib.ts +++ b/src/imports/stdlib.ts @@ -680,7 +680,8 @@ files['stdlib.fc'] = 'CmludCBidWlsZGVyX251bGw/KGJ1aWxkZXIgYikgYXNtICJJU05VTEwiOwo7OzsgQ29uY2F0ZW5hdGVzIHR3byBidWlsZGVycwpidWlsZGVyIHN0b3JlX2J1aWxkZXIo' + 'YnVpbGRlciB0bywgYnVpbGRlciBmcm9tKSBhc20gIlNUQlIiOwoKOzsgQ1VTVE9NOgoKOzsgVFZNIFVQR1JBREUgMjAyMy0wNyBodHRwczovL2RvY3MudG9uLm9yZy9s' + 'ZWFybi90dm0taW5zdHJ1Y3Rpb25zL3R2bS11cGdyYWRlLTIwMjMtMDcKOzsgSW4gbWFpbm5ldCBzaW5jZSAyMCBEZWMgMjAyMyBodHRwczovL3QubWUvdG9uYmxvY2tj' + - 'aGFpbi8yMjYKCjs7OyBSZXRyaWV2ZXMgY29kZSBvZiBzbWFydC1jb250cmFjdCBmcm9tIGM3CmNlbGwgbXlfY29kZSgpIGFzbSAiTVlDT0RFIjsK'; + 'aGFpbi8yMjYKCjs7OyBSZXRyaWV2ZXMgY29kZSBvZiBzbWFydC1jb250cmFjdCBmcm9tIGM3CmNlbGwgbXlfY29kZSgpIGFzbSAiTVlDT0RFIjsKCihzbGljZSwgaW50' + + 'KSBiZWdpbl9wYXJzZV9leG90aWMoY2VsbCBjKSBhc20gIlhDVE9TIjs='; files['stdlib.tact'] = 'aW1wb3J0ICIuL3N0ZC9wcmltaXRpdmVzIjsKaW1wb3J0ICIuL3N0ZC9jZWxscyI7CmltcG9ydCAiLi9zdGQvY3J5cHRvIjsKaW1wb3J0ICIuL3N0ZC90ZXh0IjsKaW1w' + 'b3J0ICIuL3N0ZC9tYXRoIjsKaW1wb3J0ICIuL3N0ZC9jb250cmFjdCI7CmltcG9ydCAiLi9zdGQvZGVidWciOwppbXBvcnQgIi4vc3RkL2NvbnRleHQiOwppbXBvcnQg' + diff --git a/src/storage/allocator.ts b/src/storage/allocator.ts index 27ab63d54..f2f14fcbf 100644 --- a/src/storage/allocator.ts +++ b/src/storage/allocator.ts @@ -65,6 +65,9 @@ function getOperationSize(src: AllocationOperationType): { case "fixed-bytes": { return { bits: src.bytes * 8 + (src.optional ? 1 : 0), refs: 0 }; } + case "merkle-proof": { + return { bits: 0, refs: 1 }; + } } } @@ -222,6 +225,22 @@ export function getAllocationOperationFromField( optional: src.optional ? src.optional : false, }; } + if (src.type === "merkleProof") { + if (src.optional) { + throwInternalCompilerError( + "Merkle proof cannot be optional", + ); + } + if (typeof src.format !== "string") { + throwInternalCompilerError( + "Expected string format for merkleProof", + ); + } + return { + kind: "merkle-proof", + struct: src.format, + }; + } // Struct types const size = structLoader(src.type); diff --git a/src/storage/operation.ts b/src/storage/operation.ts index 7b18fb466..147236781 100644 --- a/src/storage/operation.ts +++ b/src/storage/operation.ts @@ -63,4 +63,8 @@ export type AllocationOperationType = kind: "fixed-bytes"; bytes: number; optional: boolean; + } + | { + kind: "merkle-proof"; + struct: string; }; diff --git a/src/types/resolveABITypeRef.ts b/src/types/resolveABITypeRef.ts index dc049e101..5d3f86d84 100644 --- a/src/types/resolveABITypeRef.ts +++ b/src/types/resolveABITypeRef.ts @@ -357,6 +357,25 @@ export function resolveABIType(src: AstFieldDecl): ABITypeRef { return { kind: "dict", key, keyFormat, value, valueFormat }; } + // + // Exotics + // + + if (src.type.kind === "exotic_type") { + switch (src.type.name) { + case "merkleProof": + return { + kind: "simple", + type: "merkleProof", + format: src.type.struct.text, + }; + } + throwCompilationError( + `Unsupported exotic type '${idTextErr(src.type.name)}'`, + src.loc, + ); // should never happen + } + throwCompilationError(`Unsupported type`, src.loc); } @@ -511,5 +530,20 @@ export function createABITypeRefFromTypeRef( throwInternalCompilerError("Unexpected bounced reference"); } + // + // Exotics + // + + if (src.kind === "exotic") { + switch (src.name) { + case "merkleProof": + return { + kind: "simple", + type: "merkleProof", + format: src.struct, + }; + } + } + throwInternalCompilerError(`Unsupported type`); } diff --git a/src/types/resolveDescriptors.ts b/src/types/resolveDescriptors.ts index dc17b7255..ab7642aa9 100644 --- a/src/types/resolveDescriptors.ts +++ b/src/types/resolveDescriptors.ts @@ -177,17 +177,17 @@ export function resolveTypeRef(ctx: CompilerContext, type: AstType): TypeRef { }; } case "exotic_type": { - const s = getType(ctx, idText(src.struct)); - if (src.name !== "merkleProof") { + const s = getType(ctx, idText(type.struct)); + if (type.name !== "merkleProof") { // Should never happen because of grammar throwCompilationError( - `Unknown exotic type ${src.name}`, - src.loc, + `Unknown exotic type ${type.name}`, + type.loc, ); } return { kind: "exotic", - name: src.name, + name: type.name, struct: s.name, }; } @@ -267,23 +267,23 @@ function buildTypeRef( }; } case "exotic_type": { - if (!types.has(idText(src.struct))) { + if (!types.has(idText(type.struct))) { throwCompilationError( - `Type ${idTextErr(src.struct)} not found`, - src.loc, + `Type ${idTextErr(type.struct)} not found`, + type.loc, ); } - if (src.name !== "merkleProof") { + if (type.name !== "merkleProof") { // Should never happen because of grammar throwCompilationError( - `Unknown exotic type ${src.name}`, - src.loc, + `Unknown exotic type ${type.name}`, + type.loc, ); } return { kind: "exotic", - name: src.name, - struct: idText(src.struct), + name: type.name, + struct: idText(type.struct), }; } } @@ -969,6 +969,9 @@ export function resolveDescriptors(ctx: CompilerContext) { } } break; + case "exotic": + retTupleSize = 1; + break; case "null": case "map": retTupleSize = 1; @@ -2257,6 +2260,9 @@ function checkRecursiveTypes(ctx: CompilerContext): void { case "map": processPossibleSuccessor(field.type.value); break; + case "exotic": + processPossibleSuccessor(field.type.struct); + break; // do nothing case "void": case "null": diff --git a/src/types/resolveExpression.ts b/src/types/resolveExpression.ts index d77baf32e..72d585376 100644 --- a/src/types/resolveExpression.ts +++ b/src/types/resolveExpression.ts @@ -394,7 +394,11 @@ function resolveFieldAccess( // Find target type and check for type const src = getExpType(ctx, exp.aggregate); - if ((src.kind !== "ref" || src.optional) && src.kind !== "ref_bounced") { + if ( + (src.kind !== "ref" || src.optional) && + src.kind !== "ref_bounced" && + src.kind !== "exotic" + ) { throwCompilationError( `Invalid type "${printTypeRef(src)}" for field access`, exp.loc, @@ -415,6 +419,33 @@ function resolveFieldAccess( } } + if (src.kind === "exotic") { + if (eqNames(exp.field, "rootHash")) { + return registerExpType(ctx, exp, { + kind: "ref", + name: "Int", + optional: false, + }); + } + if (eqNames(exp.field, "depth")) { + return registerExpType(ctx, exp, { + kind: "ref", + name: "Int", + optional: false, + }); + } + if (eqNames(exp.field, "data")) { + return registerExpType(ctx, exp, { + kind: "ref", + name: src.struct, + optional: false, + }); + } + throwCompilationError( + `Type ${src.name} does not have a field named ${idTextErr(exp.field)}`, + ); + } + // Find field let fields: FieldDescription[]; diff --git a/src/types/resolveSignatures.ts b/src/types/resolveSignatures.ts index a8b9ffb5a..01ba3dc5f 100644 --- a/src/types/resolveSignatures.ts +++ b/src/types/resolveSignatures.ts @@ -106,6 +106,13 @@ export function resolveSignatures(ctx: CompilerContext) { ); } throwInternalCompilerError("Missing fixed-bytes format"); + } else if (type === "merkleProof") { + if (typeof format !== "string") { + throwInternalCompilerError( + `Unsupported merkleProof format: ${format}`, + ); + } + return `^merkle_proof<${format}>`; } // Struct types diff --git a/stdlib/stdlib.fc b/stdlib/stdlib.fc index ac05cbc7c..4638761b1 100644 --- a/stdlib/stdlib.fc +++ b/stdlib/stdlib.fc @@ -645,3 +645,5 @@ builder store_builder(builder to, builder from) asm "STBR"; ;;; Retrieves code of smart-contract from c7 cell my_code() asm "MYCODE"; + +(slice, int) begin_parse_exotic(cell c) asm "XCTOS"; \ No newline at end of file From 43f9f8df5eb8825ac9a750ffd4c3dad5660b74da Mon Sep 17 00:00:00 2001 From: Gusarich Date: Sun, 15 Sep 2024 00:32:50 +0300 Subject: [PATCH 4/6] wip: compilable version --- src/bindings/typescript/serializers.ts | 18 ++++++++++++------ src/generator/writers/resolveFuncType.ts | 2 +- .../writers/resolveFuncTypeUnpack.ts | 12 ++++++------ src/generator/writers/writeExpression.ts | 19 +++++++++++++++++++ src/generator/writers/writeSerialization.ts | 10 +++++++++- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/bindings/typescript/serializers.ts b/src/bindings/typescript/serializers.ts index 7e91e2878..55e8e10c6 100644 --- a/src/bindings/typescript/serializers.ts +++ b/src/bindings/typescript/serializers.ts @@ -281,14 +281,18 @@ const addressSerializer: Serializer<{ optional: boolean }> = { }; function getCellLikeTsType(v: { - kind: "cell" | "slice" | "builder"; + kind: "cell" | "slice" | "builder" | "merkleProof"; optional?: boolean; }) { - return v.kind == "cell" ? "Cell" : v.kind == "slice" ? "Slice" : "Builder"; + return v.kind === "cell" || v.kind === "merkleProof" + ? "Cell" + : v.kind === "slice" + ? "Slice" + : "Builder"; } function getCellLikeTsAsMethod(v: { - kind: "cell" | "slice" | "builder"; + kind: "cell" | "slice" | "builder" | "merkleProof"; optional?: boolean; }) { if (v.optional) { @@ -299,7 +303,7 @@ function getCellLikeTsAsMethod(v: { } const cellSerializer: Serializer<{ - kind: "cell" | "slice" | "builder"; + kind: "cell" | "slice" | "builder" | "merkleProof"; optional: boolean; }> = { tsType(v) { @@ -358,12 +362,14 @@ const cellSerializer: Serializer<{ if ( src.type === "cell" || src.type === "slice" || - src.type === "builder" + src.type === "builder" || + src.type === "merkleProof" ) { if ( src.format === null || src.format === undefined || - src.format === "ref" + src.format === "ref" || + src.type === "merkleProof" ) { return { optional: src.optional ? src.optional : false, diff --git a/src/generator/writers/resolveFuncType.ts b/src/generator/writers/resolveFuncType.ts index b28e39189..52bf89ba1 100644 --- a/src/generator/writers/resolveFuncType.ts +++ b/src/generator/writers/resolveFuncType.ts @@ -98,7 +98,7 @@ export function resolveFuncType( } else if (descriptor.kind === "exotic") { const t = getType(ctx.ctx, descriptor.struct); return ( - "(int, int, " + + "int, int, (" + t.fields .map((v) => resolveFuncType(v.type, ctx, false, usePartialFields), diff --git a/src/generator/writers/resolveFuncTypeUnpack.ts b/src/generator/writers/resolveFuncTypeUnpack.ts index 65b17b77a..b59b2f3a3 100644 --- a/src/generator/writers/resolveFuncTypeUnpack.ts +++ b/src/generator/writers/resolveFuncTypeUnpack.ts @@ -95,17 +95,17 @@ export function resolveFuncTypeUnpack( } else if (descriptor.kind === "exotic") { const t = getType(ctx.ctx, descriptor.struct); return ( - "(int, int, " + + `${name}'rootHash, ${name}'depth, (` + t.fields - .map((v) => - resolveFuncTypeUnpack( + .map((v) => { + return resolveFuncTypeUnpack( v.type, - name + `'` + v.name, + name + `'data'` + v.name, ctx, false, usePartialFields, - ), - ) + ); + }) .join(", ") + ")" ); diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index de6dde0dc..710651d43 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -446,6 +446,25 @@ export function writeExpression(f: AstExpression, wCtx: WriterContext): string { // they are accessed exactly the same as struct fields const path = tryExtractPath(f); if (path) { + // if we are accessing `data` field, we need to unpack the struct + if (path[path.length - 1]?.text === "data") { + const idd = writePathExpression(path); + const t = getType(wCtx.ctx, src.struct); + return ( + "(" + + t.fields + .map((v) => + resolveFuncTypeUnpack( + v.type, + `${idd}'${v.name}`, + wCtx, + false, + ), + ) + .join(", ") + + ")" + ); + } return writePathExpression(path); } throwCompilationError( diff --git a/src/generator/writers/writeSerialization.ts b/src/generator/writers/writeSerialization.ts index b38c6a19b..305ec01be 100644 --- a/src/generator/writers/writeSerialization.ts +++ b/src/generator/writers/writeSerialization.ts @@ -352,7 +352,15 @@ export function writeParser( ctx.append(`return (sc_0, null());`); } else { ctx.append( - `return (sc_0, (${allocation.ops.map((v) => `v'${v.name}`).join(", ")}));`, + `return (sc_0, (${allocation.ops + .map((v) => { + if (v.op.kind == "merkle-proof") { + return `v'${v.name}'rootHash, v'${v.name}'depth, v'${v.name}'data`; + } else { + return `v'${v.name}`; + } + }) + .join(", ")}));`, ); } }); From fae295c9900c3482f7e837d579bdeee43c6cfab7 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Sun, 15 Sep 2024 00:49:54 +0300 Subject: [PATCH 5/6] wip --- src/bindings/typescript/serializers.ts | 26 +++--- src/test/e2e-emulated/contracts/exotics.tact | 2 +- src/test/e2e-emulated/exotics.spec.ts | 84 ++++++++++++++++++++ 3 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 src/test/e2e-emulated/exotics.spec.ts diff --git a/src/bindings/typescript/serializers.ts b/src/bindings/typescript/serializers.ts index 55e8e10c6..a27c1caf6 100644 --- a/src/bindings/typescript/serializers.ts +++ b/src/bindings/typescript/serializers.ts @@ -280,11 +280,15 @@ const addressSerializer: Serializer<{ optional: boolean }> = { }, }; +function isCellType(vKind: string) { + return vKind === "cell" || vKind === "merkleProof"; +} + function getCellLikeTsType(v: { kind: "cell" | "slice" | "builder" | "merkleProof"; optional?: boolean; }) { - return v.kind === "cell" || v.kind === "merkleProof" + return isCellType(v.kind) ? "Cell" : v.kind === "slice" ? "Slice" @@ -316,44 +320,44 @@ const cellSerializer: Serializer<{ tsLoad(v, slice, field, w) { if (v.optional) { w.append( - `let ${field} = ${slice}.loadBit() ? ${slice}.loadRef()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""} : null;`, + `let ${field} = ${slice}.loadBit() ? ${slice}.loadRef()${!isCellType(v.kind) ? getCellLikeTsAsMethod(v) : ""} : null;`, ); } else { w.append( - `let ${field} = ${slice}.loadRef()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""};`, + `let ${field} = ${slice}.loadRef()${!isCellType(v.kind) ? getCellLikeTsAsMethod(v) : ""};`, ); } }, tsLoadTuple(v, reader, field, w) { if (v.optional) { w.append( - `let ${field} = ${reader}.readCellOpt()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""};`, + `let ${field} = ${reader}.readCellOpt()${!isCellType(v.kind) ? getCellLikeTsAsMethod(v) : ""};`, ); } else { w.append( - `let ${field} = ${reader}.readCell()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""};`, + `let ${field} = ${reader}.readCell()${!isCellType(v.kind) ? getCellLikeTsAsMethod(v) : ""};`, ); } }, tsStore(v, builder, field, w) { if (v.optional) { w.append( - `if (${field} !== null && ${field} !== undefined) { ${builder}.storeBit(true).storeRef(${field}${v.kind !== "cell" ? ".asCell()" : ""}); } else { ${builder}.storeBit(false); }`, + `if (${field} !== null && ${field} !== undefined) { ${builder}.storeBit(true).storeRef(${field}${!isCellType(v.kind) ? ".asCell()" : ""}); } else { ${builder}.storeBit(false); }`, ); } else { w.append( - `${builder}.storeRef(${field}${v.kind !== "cell" ? ".asCell()" : ""});`, + `${builder}.storeRef(${field}${!isCellType(v.kind) ? ".asCell()" : ""});`, ); } }, tsStoreTuple(v, to, field, w) { if (v.optional) { w.append( - `${to}.write${getCellLikeTsType(v)}(${field}${v.kind !== "cell" ? "?.asCell()" : ""});`, + `${to}.write${getCellLikeTsType(v)}(${field}${!isCellType(v.kind) ? "?.asCell()" : ""});`, ); } else { w.append( - `${to}.write${getCellLikeTsType(v)}(${field}${v.kind !== "cell" ? ".asCell()" : ""});`, + `${to}.write${getCellLikeTsType(v)}(${field}${!isCellType(v.kind) ? ".asCell()" : ""});`, ); } }, @@ -394,7 +398,7 @@ const remainderSerializer: Serializer<{ kind: "cell" | "slice" | "builder" }> = }, tsLoadTuple(v, reader, field, w) { w.append( - `let ${field} = ${reader}.readCell()${v.kind !== "cell" ? getCellLikeTsAsMethod(v) : ""};`, + `let ${field} = ${reader}.readCell()${!isCellType(v.kind) ? getCellLikeTsAsMethod(v) : ""};`, ); }, tsStore(v, builder, field, w) { @@ -404,7 +408,7 @@ const remainderSerializer: Serializer<{ kind: "cell" | "slice" | "builder" }> = }, tsStoreTuple(v, to, field, w) { w.append( - `${to}.write${getCellLikeTsType(v)}(${field}${v.kind !== "cell" ? ".asCell()" : ""});`, + `${to}.write${getCellLikeTsType(v)}(${field}${!isCellType(v.kind) ? ".asCell()" : ""});`, ); }, abiMatcher(src) { diff --git a/src/test/e2e-emulated/contracts/exotics.tact b/src/test/e2e-emulated/contracts/exotics.tact index cee4d02ce..f305dd953 100644 --- a/src/test/e2e-emulated/contracts/exotics.tact +++ b/src/test/e2e-emulated/contracts/exotics.tact @@ -7,7 +7,7 @@ struct AirdropEntry { struct ProvableData { justForTestPurposes: Int; - entries: map; + entries: map; anoterOne: Int as uint32; } diff --git a/src/test/e2e-emulated/exotics.spec.ts b/src/test/e2e-emulated/exotics.spec.ts new file mode 100644 index 000000000..e338b8e3e --- /dev/null +++ b/src/test/e2e-emulated/exotics.spec.ts @@ -0,0 +1,84 @@ +import { beginCell, Builder, Dictionary, Slice, toNano } from "@ton/core"; +import { + Blockchain, + printTransactionFees, + SandboxContract, + TreasuryContract, +} from "@ton/sandbox"; +import { + AirdropEntry, + MerkleTreesTestContract, +} from "./contracts/output/exotics_MerkleTreesTestContract"; +import "@ton/test-utils"; +import { randomAddress } from "@ton/test-utils"; + +const airdropEntryValue = { + serialize: (src: AirdropEntry, buidler: Builder) => { + buidler.storeAddress(src.address).storeCoins(src.amount); + }, + parse: (src: Slice) => { + return { + $$type: "AirdropEntry" as const, + address: src.loadAddress(), + amount: src.loadCoins(), + }; + }, +}; + +describe("exotics", () => { + let blockchain: Blockchain; + let treasure: SandboxContract; + let contract: SandboxContract; + let dict: Dictionary; + + beforeAll(() => { + dict = Dictionary.empty(Dictionary.Keys.Uint(16), airdropEntryValue); + dict.set(0, { + $$type: "AirdropEntry", + address: randomAddress(), + amount: toNano("10"), + }); + dict.set(1, { + $$type: "AirdropEntry", + address: randomAddress(), + amount: toNano("20"), + }); + dict.set(2, { + $$type: "AirdropEntry", + address: randomAddress(), + amount: toNano("30"), + }); + }); + + beforeEach(async () => { + blockchain = await Blockchain.create(); + blockchain.verbosity.print = false; + treasure = await blockchain.treasury("treasure"); + + const dictCell = beginCell().storeDictDirect(dict).endCell(); + + contract = blockchain.openContract( + await MerkleTreesTestContract.fromInit( + BigInt("0x" + dictCell.hash().toString("hex")), + ), + ); + }); + + it("should check merkle proofs", async () => { + const proof = dict.generateMerkleProof([0]); + const result = await contract.send( + treasure.getSender(), + { + value: toNano("10"), + }, + { + $$type: "ProcessClaim", + index: 0n, + queryId: 123n, + proof, + }, + ); + printTransactionFees(result.transactions); + console.log(result.transactions[1].vmLogs); + }); +}); From 5b27c35d3a7fb4d98ef2afbcb5b02e60ef744388 Mon Sep 17 00:00:00 2001 From: Gusarich Date: Sun, 15 Sep 2024 00:53:25 +0300 Subject: [PATCH 6/6] wip: fix eslint and cspell --- cspell.json | 1 + src/generator/writers/resolveFuncType.ts | 1 - src/generator/writers/writeSerialization.ts | 3 +-- src/grammar/iterators.ts | 3 +++ src/test/e2e-emulated/contracts/exotics.tact | 2 +- src/test/e2e-emulated/exotics.spec.ts | 4 ++-- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/cspell.json b/cspell.json index e5b433ec2..3b77e8d2d 100644 --- a/cspell.json +++ b/cspell.json @@ -75,6 +75,7 @@ "lvalues", "masterchain", "maxint", + "merkle", "minmax", "mintable", "mktemp", diff --git a/src/generator/writers/resolveFuncType.ts b/src/generator/writers/resolveFuncType.ts index 52bf89ba1..07cf6b651 100644 --- a/src/generator/writers/resolveFuncType.ts +++ b/src/generator/writers/resolveFuncType.ts @@ -1,4 +1,3 @@ -import { deserialize } from "node:v8"; import { getType } from "../../types/resolveDescriptors"; import { TypeDescription, TypeRef } from "../../types/types"; import { WriterContext } from "../Writer"; diff --git a/src/generator/writers/writeSerialization.ts b/src/generator/writers/writeSerialization.ts index 305ec01be..2f6e126ad 100644 --- a/src/generator/writers/writeSerialization.ts +++ b/src/generator/writers/writeSerialization.ts @@ -1,6 +1,5 @@ import { contractErrors } from "../../abi/errors"; -import { throwInternalCompilerError } from "../../errors"; -import { dummySrcInfo, ItemOrigin } from "../../grammar/grammar"; +import { ItemOrigin } from "../../grammar/grammar"; import { AllocationCell, AllocationOperation } from "../../storage/operation"; import { StorageAllocation } from "../../storage/StorageAllocation"; import { getType } from "../../types/resolveDescriptors"; diff --git a/src/grammar/iterators.ts b/src/grammar/iterators.ts index 4a8501470..19cbe8d31 100644 --- a/src/grammar/iterators.ts +++ b/src/grammar/iterators.ts @@ -244,5 +244,8 @@ export function traverse(node: AstNode, callback: (node: AstNode) => void) { traverse(node.name, callback); traverse(node.type, callback); break; + case "exotic_type": + traverse(node.struct, callback); + break; } } diff --git a/src/test/e2e-emulated/contracts/exotics.tact b/src/test/e2e-emulated/contracts/exotics.tact index f305dd953..edb62b328 100644 --- a/src/test/e2e-emulated/contracts/exotics.tact +++ b/src/test/e2e-emulated/contracts/exotics.tact @@ -8,7 +8,7 @@ struct AirdropEntry { struct ProvableData { justForTestPurposes: Int; entries: map; - anoterOne: Int as uint32; + anotherOne: Int as uint32; } message ProcessClaim { diff --git a/src/test/e2e-emulated/exotics.spec.ts b/src/test/e2e-emulated/exotics.spec.ts index e338b8e3e..34e99864f 100644 --- a/src/test/e2e-emulated/exotics.spec.ts +++ b/src/test/e2e-emulated/exotics.spec.ts @@ -13,8 +13,8 @@ import "@ton/test-utils"; import { randomAddress } from "@ton/test-utils"; const airdropEntryValue = { - serialize: (src: AirdropEntry, buidler: Builder) => { - buidler.storeAddress(src.address).storeCoins(src.amount); + serialize: (src: AirdropEntry, builder: Builder) => { + builder.storeAddress(src.address).storeCoins(src.amount); }, parse: (src: Slice) => { return {