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/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/bindings/typescript/serializers.ts b/src/bindings/typescript/serializers.ts index 7e91e2878..a27c1caf6 100644 --- a/src/bindings/typescript/serializers.ts +++ b/src/bindings/typescript/serializers.ts @@ -280,15 +280,23 @@ const addressSerializer: Serializer<{ optional: boolean }> = { }, }; +function isCellType(vKind: string) { + return vKind === "cell" || vKind === "merkleProof"; +} + 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 isCellType(v.kind) + ? "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 +307,7 @@ function getCellLikeTsAsMethod(v: { } const cellSerializer: Serializer<{ - kind: "cell" | "slice" | "builder"; + kind: "cell" | "slice" | "builder" | "merkleProof"; optional: boolean; }> = { tsType(v) { @@ -312,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()" : ""});`, ); } }, @@ -358,12 +366,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, @@ -388,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) { @@ -398,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/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..07cf6b651 100644 --- a/src/generator/writers/resolveFuncType.ts +++ b/src/generator/writers/resolveFuncType.ts @@ -94,6 +94,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..b59b2f3a3 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 ( + `${name}'rootHash, ${name}'depth, (` + + t.fields + .map((v) => { + return resolveFuncTypeUnpack( + v.type, + name + `'data'` + v.name, + ctx, + false, + usePartialFields, + ); + }) + .join(", ") + + ")" + ); } // Unreachable diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index 937d3144c..710651d43 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -432,13 +432,47 @@ 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) { + // 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( + `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..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"; @@ -305,9 +304,11 @@ function writeSerializerField( } return; } + case "merkle-proof": { + ctx.append(`build_${gen} = build_${gen}.store_ref(${fieldName});`); + return; + } } - - throwInternalCompilerError(`Unsupported field kind`, dummySrcInfo); } // @@ -350,7 +351,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(", ")}));`, ); } }); @@ -643,5 +652,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/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/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/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/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/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/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/test/e2e-emulated/contracts/exotics.tact b/src/test/e2e-emulated/contracts/exotics.tact new file mode 100644 index 000000000..edb62b328 --- /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; + anotherOne: 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/src/test/e2e-emulated/exotics.spec.ts b/src/test/e2e-emulated/exotics.spec.ts new file mode 100644 index 000000000..34e99864f --- /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, builder: Builder) => { + builder.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); + }); +}); 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 dc55b0dea..ab7642aa9 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(type.struct)); + if (type.name !== "merkleProof") { + // Should never happen because of grammar + throwCompilationError( + `Unknown exotic type ${type.name}`, + type.loc, + ); + } + return { + kind: "exotic", + name: type.name, + struct: s.name, + }; + } } } @@ -251,6 +266,26 @@ function buildTypeRef( name: idText(type.messageType), }; } + case "exotic_type": { + if (!types.has(idText(type.struct))) { + throwCompilationError( + `Type ${idTextErr(type.struct)} not found`, + type.loc, + ); + } + if (type.name !== "merkleProof") { + // Should never happen because of grammar + throwCompilationError( + `Unknown exotic type ${type.name}`, + type.loc, + ); + } + return { + kind: "exotic", + name: type.name, + struct: idText(type.struct), + }; + } } } @@ -934,6 +969,9 @@ export function resolveDescriptors(ctx: CompilerContext) { } } break; + case "exotic": + retTupleSize = 1; + break; case "null": case "map": retTupleSize = 1; @@ -2222,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 95190249c..72d585376 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": @@ -393,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, @@ -414,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/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": 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 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" } ] }