Skip to content

feat: add support for codeOf T feature #1948

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

Merged
merged 22 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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 dev-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `--output` CLI flag for specifying custom output directory in single-contract compilation: PR [#1793](https://github.com/tact-lang/tact/pull/1793)
- New functions `Slice.asAddressUnsafe` and `contractHash` in stdlib: PR [#1766](https://github.com/tact-lang/tact/pull/1766)
- New function `Slice.asAddress` in stdlib: PR [#1766](https://github.com/tact-lang/tact/pull/1766)
- New `codeOf T` expression to get code of the contract: PR [#1948](https://github.com/tact-lang/tact/pull/1948)

### Changed

Expand Down
3 changes: 3 additions & 0 deletions src/ast/ast-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ export function eqExpressions(
eqNames(ast1.contract, (ast2 as A.AstInitOf).contract) &&
eqArrays(ast1.args, (ast2 as A.AstInitOf).args, eqExpressions)
);
case "code_of":
return eqNames(ast1.contract, (ast2 as A.AstCodeOf).contract);
case "op_unary":
return (
ast1.op === (ast2 as A.AstOpUnary).op &&
Expand Down Expand Up @@ -389,6 +391,7 @@ function checkLiteral<T>(
case "id":
case "method_call":
case "init_of":
case "code_of":
case "op_unary":
case "op_binary":
case "conditional":
Expand Down
5 changes: 5 additions & 0 deletions src/ast/ast-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ export const ppAstStructValue = ({ type, args }: A.AstStructValue) =>
export const ppAstInitOf = ({ contract, args }: A.AstInitOf) =>
`initOf ${ppAstId(contract)}(${ppExprArgs(args)})`;

export const ppAstCodeOf = ({ contract }: A.AstCodeOf) =>
`codeOf ${ppAstId(contract)}`;

export const ppAstNumber = astNumToString;
export const ppAstBoolean = ({ value }: A.AstBoolean) => value.toString();
export const ppAstId = ({ text }: A.AstId) => text;
Expand Down Expand Up @@ -260,6 +263,7 @@ export const ppAstExpressionNested = makeVisitor<A.AstExpression>()({
id: ppLeaf(ppAstId),
null: ppLeaf(ppAstNull),
init_of: ppLeaf(ppAstInitOf),
code_of: ppLeaf(ppAstCodeOf),
string: ppLeaf(ppAstString),
static_call: ppLeaf(ppAstStaticCall),
simplified_string: ppLeaf(ppAstSimplifiedString),
Expand Down Expand Up @@ -880,6 +884,7 @@ export const ppAstNode: Printer<A.AstNode> = makeVisitor<A.AstNode>()({
struct_instance: exprNode(ppAstExpression),
struct_value: exprNode(ppAstStructValue),
init_of: exprNode(ppAstExpression),
code_of: exprNode(ppAstExpression),
conditional: exprNode(ppAstExpression),
number: exprNode(ppAstExpression),
id: exprNode(ppAstExpression),
Expand Down
8 changes: 8 additions & 0 deletions src/ast/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ export type AstExpression =
| AstStructInstance
| AstId
| AstInitOf
| AstCodeOf
| AstString
| AstLiteral;

Expand Down Expand Up @@ -495,6 +496,13 @@ export type AstInitOf = {
readonly loc: SrcInfo;
};

export type AstCodeOf = {
readonly kind: "code_of";
readonly contract: AstId;
readonly id: number;
readonly loc: SrcInfo;
};

export type AstConditional = {
readonly kind: "conditional";
readonly condition: AstExpression;
Expand Down
4 changes: 4 additions & 0 deletions src/ast/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ export function cloneNode<T extends AstNode>(
...src,
args: src.args.map(recurse),
});
} else if (src.kind === "code_of") {
return cloneNode({
...src,
});
} else if (src.kind === "constant_def") {
return cloneNode({
...src,
Expand Down
6 changes: 6 additions & 0 deletions src/ast/getAstSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,12 @@ export const getAstSchema = (
args,
loc: toSrcInfo(loc),
}),
CodeOf: (contract: A.AstId, loc: Loc): A.AstCodeOf =>
createNode<A.AstCodeOf>({
kind: "code_of",
contract,
loc: toSrcInfo(loc),
}),
Conditional: (
condition: A.AstExpression,
thenBranch: A.AstExpression,
Expand Down
3 changes: 3 additions & 0 deletions src/ast/iterators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,9 @@ export function traverseAndCheck(
traverseAndCheck(e, callback);
});
break;
case "code_of":
traverseAndCheck(node.contract, callback);
break;
case "conditional":
traverseAndCheck(node.condition, callback);
traverseAndCheck(node.thenBranch, callback);
Expand Down
10 changes: 10 additions & 0 deletions src/ast/random.infra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ function randomAstInitOf(
);
}

function randomAstCodeOf(): fc.Arbitrary<A.AstCodeOf> {
return dummyAstNode(
fc.record({
kind: fc.constant("code_of"),
contract: randomAstId(),
}),
);
}

function randomAstStaticCall(
expression: fc.Arbitrary<A.AstExpression>,
): fc.Arbitrary<A.AstStaticCall> {
Expand Down Expand Up @@ -313,6 +322,7 @@ export function randomAstExpression(
randomAstStructFieldInitializer(subExpr()),
),
randomAstInitOf(subExpr()),
randomAstCodeOf(),
randomAstString(),
randomAstOpUnary(subExpr()),
randomAstOpBinary(subExpr(), subExpr()),
Expand Down
2 changes: 2 additions & 0 deletions src/generator/writers/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export const ops = {
used(`$${type}$_contract_init`, ctx),
contractInitChild: (type: string, ctx: WriterContext) =>
used(`$${type}$_init_child`, ctx),
contractCodeChild: (type: string, ctx: WriterContext) =>
used(`$${type}$_code_child`, ctx),
contractLoad: (type: string, ctx: WriterContext) =>
used(`$${type}$_contract_load`, ctx),
contractStore: (type: string, ctx: WriterContext) =>
Expand Down
15 changes: 15 additions & 0 deletions src/generator/writers/writeContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,21 @@ export function writeInit(
ctx.append(`return (init_code, b.end_cell());`);
});
});

ctx.fun(ops.contractCodeChild(t.name, ctx), () => {
const sig = `cell ${ops.contractCodeChild(t.name, ctx)}()`;
ctx.signature(sig);
ctx.flag("inline");
ctx.context("type:" + t.name + "$init");
ctx.body(() => {
ctx.write(`
slice sc' = __tact_child_contract_codes.begin_parse();
cell source = sc'~load_dict();
;; Contract Code: ${t.name}
return ${ctx.used("__tact_dict_get_code")}(source, ${t.uid});
`);
});
});
}

export function writeMainContract(
Expand Down
13 changes: 13 additions & 0 deletions src/generator/writers/writeExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,19 @@ export function writeExpression(
return `${ops.contractInitChild(idText(f.contract), wCtx)}(${initArgs.join(", ")})`;
}

//
// Code of
//

if (f.kind === "code_of") {
// In case of using `codeOf T` in contract `T`, we simply use MYCODE.
if (wCtx.name === f.contract.text) {
return `my_code()`;
}

return `${ops.contractCodeChild(idText(f.contract), wCtx)}()`;
}

//
// Ternary operator
//
Expand Down
130 changes: 127 additions & 3 deletions src/grammar/next/__snapshots__/grammar.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ exports[`grammar should fail contract-empty-traits-list-with-keyword 1`] = `
`;

exports[`grammar should fail contract-getter-parens-no-method-id 1`] = `
"<unknown>:2:9: Expected "!", "(", "+", "-", "0", "\\"", "false", "initOf", "null", "true", "~", capitalized identifier, digit, or identifier
"<unknown>:2:9: Expected "!", "(", "+", "-", "0", "\\"", "codeOf", "false", "initOf", "null", "true", "~", capitalized identifier, digit, or identifier
1 | contract Test {
> 2 | get() fun test(): Int {
^
Expand Down Expand Up @@ -179,7 +179,7 @@ exports[`grammar should fail destructuring-duplicate-source-id 1`] = `
`;

exports[`grammar should fail expr-fun-call-trailing-comma-no-args 1`] = `
"<unknown>:6:14: Expected "!", "(", ")", "+", "-", "0", "\\"", "false", "initOf", "null", "true", "~", capitalized identifier, digit, or identifier
"<unknown>:6:14: Expected "!", "(", ")", "+", "-", "0", "\\"", "codeOf", "false", "initOf", "null", "true", "~", capitalized identifier, digit, or identifier
5 | fun b(): Int {
> 6 | return a(,);
^
Expand All @@ -188,7 +188,7 @@ exports[`grammar should fail expr-fun-call-trailing-comma-no-args 1`] = `
`;

exports[`grammar should fail expr-method-call-trailing-comma-no-args 1`] = `
"<unknown>:2:24: Expected "!", "(", ")", "+", "-", "0", "\\"", "false", "initOf", "null", "true", "~", capitalized identifier, digit, or identifier
"<unknown>:2:24: Expected "!", "(", ")", "+", "-", "0", "\\"", "codeOf", "false", "initOf", "null", "true", "~", capitalized identifier, digit, or identifier
1 | fun another() {
> 2 | return 42.toString(,);
^
Expand Down Expand Up @@ -1054,6 +1054,130 @@ exports[`grammar should parse case-35 1`] = `
}
`;

exports[`grammar should parse codeOf 1`] = `
{
"id": 8,
"imports": [],
"items": [
{
"attributes": [],
"declarations": [
{
"id": 6,
"kind": "contract_init",
"loc": init() {
let code = codeOf Foo;
},
"params": [],
"statements": [
{
"expression": {
"contract": {
"id": 3,
"kind": "id",
"loc": Foo,
"text": "Foo",
},
"id": 4,
"kind": "code_of",
"loc": codeOf Foo,
},
"id": 5,
"kind": "statement_let",
"loc": let code = codeOf Foo;,
"name": {
"id": 2,
"kind": "id",
"loc": code,
"text": "code",
},
"type": null,
},
],
},
],
"id": 7,
"kind": "contract",
"loc": contract Foo {
init() {
let code = codeOf Foo;
}
},
"name": {
"id": 1,
"kind": "id",
"loc": Foo,
"text": "Foo",
},
"traits": [],
},
],
"kind": "module",
}
`;

exports[`grammar should parse codeOf 2`] = `
{
"id": 8,
"imports": [],
"items": [
{
"attributes": [],
"declarations": [
{
"id": 6,
"kind": "contract_init",
"loc": init() {
let code = codeOf Foo;
},
"params": [],
"statements": [
{
"expression": {
"contract": {
"id": 3,
"kind": "id",
"loc": Foo,
"text": "Foo",
},
"id": 4,
"kind": "code_of",
"loc": codeOf Foo,
},
"id": 5,
"kind": "statement_let",
"loc": let code = codeOf Foo;,
"name": {
"id": 2,
"kind": "id",
"loc": code,
"text": "code",
},
"type": null,
},
],
},
],
"id": 7,
"kind": "contract",
"loc": contract Foo {
init() {
let code = codeOf Foo;
}
},
"name": {
"id": 1,
"kind": "id",
"loc": Foo,
"text": "Foo",
},
"traits": [],
},
],
"kind": "module",
}
`;

exports[`grammar should parse contract-getter-with-method-id 1`] = `
{
"id": 17,
Expand Down
6 changes: 4 additions & 2 deletions src/grammar/next/grammar.gg
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ primary
/ IntegerLiteral
/ BoolLiteral
/ InitOf
/ CodeOf
/ Null
/ StringLiteral
/ Id;
Expand All @@ -270,6 +271,7 @@ Parens = child:parens;

StructInstance = type:TypeId "{" fields:commaList<StructFieldInitializer>? "}";
InitOf = keyword<"initOf"> name:Id params:parameterList<expression>;
CodeOf = keyword<"codeOf"> name:Id;

StructFieldInitializer = name:Id init:(":" @expression)?;

Expand Down Expand Up @@ -321,7 +323,7 @@ reservedWord "reserved word" = keyword<(
"fun" / "let" / "return" / "receive" / "native" / "primitive" / "null" /
"if" / "else" / "while" / "repeat" / "do" / "until" / "try" / "catch" /
"foreach" / "as" / "map" / "mutates" / "extends" / "external" / "import"
/ "with" / "trait" / "initOf" / "override" / "abstract" / "virtual" /
/ "with" / "trait" / "initOf" / "codeOf" / "override" / "abstract" / "virtual" /
"inline" / "const"
)>;

Expand All @@ -334,4 +336,4 @@ singleLineComment = "//" @$[^\r\n]*;
// it is useful for imports resolution
JustImports = imports:Import* .*;

inter<A, B> = head:A tail:(op:B right:A)*;
inter<A, B> = head:A tail:(op:B right:A)*;
Loading
Loading