diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md index cee37573dc..90cc3aeefd 100644 --- a/dev-docs/CHANGELOG.md +++ b/dev-docs/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Code generation + +- Optimized context field access in internal receivers: PR [#3329](https://github.com/tact-lang/tact/pull/3329) + ### Docs - Described off-chain calls and mention exit code 11 for getters: PR [#3314](https://github.com/tact-lang/tact/pull/3314) diff --git a/src/benchmarks/notcoin/gas.json b/src/benchmarks/notcoin/gas.json index 1a0bea62bb..ede94c4361 100644 --- a/src/benchmarks/notcoin/gas.json +++ b/src/benchmarks/notcoin/gas.json @@ -98,6 +98,15 @@ "burn": "11739", "discovery": "5818" } + }, + { + "label": "1.6.13 with optimized context access", + "pr": "https://github.com/tact-lang/tact/pull/3329", + "gas": { + "transfer": "15139", + "burn": "11413", + "discovery": "5818" + } } ] } diff --git a/src/benchmarks/notcoin/size.json b/src/benchmarks/notcoin/size.json index 61c2c1f6d0..0dc2253b0b 100644 --- a/src/benchmarks/notcoin/size.json +++ b/src/benchmarks/notcoin/size.json @@ -119,6 +119,16 @@ "wallet cells": "13", "wallet bits": "7708" } + }, + { + "label": "1.6.13 with optimized context access", + "pr": "https://github.com/tact-lang/tact/pull/3329", + "size": { + "minter cells": "29", + "minter bits": "15423", + "wallet cells": "13", + "wallet bits": "7628" + } } ] } diff --git a/src/benchmarks/wallet-v4/gas.json b/src/benchmarks/wallet-v4/gas.json index 2c99495571..00aa6fea27 100644 --- a/src/benchmarks/wallet-v4/gas.json +++ b/src/benchmarks/wallet-v4/gas.json @@ -125,6 +125,15 @@ "addPlugin": "7399", "pluginTransfer": "3709" } + }, + { + "label": "1.6.13 with optimized context access", + "pr": "https://github.com/tact-lang/tact/pull/3329", + "gas": { + "externalTransfer": "3625", + "addPlugin": "7399", + "pluginTransfer": "3619" + } } ] } diff --git a/src/generator/Writer.ts b/src/generator/Writer.ts index ba98aabe9f..42459467c8 100644 --- a/src/generator/Writer.ts +++ b/src/generator/Writer.ts @@ -47,6 +47,11 @@ export class WriterContext { #nextId = 0; // #headers: string[] = []; #rendered: Set = new Set(); + private _currentReceiverType: + | "internal" + | "external" + | "bounced" + | undefined = undefined; constructor(ctx: CompilerContext, name: string) { this.ctx = ctx; @@ -287,8 +292,18 @@ export class WriterContext { } } - currentContext() { - return this.#pendingName; + currentContext(): string { + return this.#pendingName ?? ""; + } + + currentReceiverType(): "internal" | "external" | "bounced" | undefined { + return this._currentReceiverType; + } + + setReceiverType( + receiverType: "internal" | "external" | "bounced" | undefined, + ) { + this._currentReceiverType = receiverType; } // diff --git a/src/generator/writers/writeContract.ts b/src/generator/writers/writeContract.ts index 9a3e55bf9a..41c5d14cb3 100644 --- a/src/generator/writers/writeContract.ts +++ b/src/generator/writers/writeContract.ts @@ -405,6 +405,8 @@ export function writeMainContract( wCtx.inBlock( "() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure", () => { + wCtx.setReceiverType("internal"); + wCtx.context("internal-receiver"); wCtx.append(); wCtx.append(`;; Context`); wCtx.append(`var cs = in_msg_cell.begin_parse();`); @@ -435,6 +437,7 @@ export function writeMainContract( contractReceivers.internal, contract, wCtx, + "internal", ); }, ); @@ -453,6 +456,8 @@ export function writeMainContract( wCtx.append(); wCtx.inBlock("() recv_external(slice in_msg) impure", () => { + wCtx.setReceiverType("external"); + wCtx.context("external-receiver"); if (contract.globalVariables.has("inMsg")) { wCtx.append(`__tact_in_msg = in_msg;`); } @@ -463,6 +468,7 @@ export function writeMainContract( contractReceivers.external, contract, wCtx, + "external", ); }); } diff --git a/src/generator/writers/writeExpression.ts b/src/generator/writers/writeExpression.ts index b3d033362e..a5ffa2969b 100644 --- a/src/generator/writers/writeExpression.ts +++ b/src/generator/writers/writeExpression.ts @@ -470,19 +470,44 @@ const writeUnaryExpr = * NOTE: this branch resolves "a.b", where "a" is an expression and "b" is a field name */ const writeFieldExpr = - (f: Ast.FieldAccess) => + (f: Ast.FieldAccess, receiverType?: "internal" | "external" | "bounced") => (wCtx: WriterContext): string => { - // Optimize Context().sender to sender() - // This is a special case to improve gas efficiency + // Optimize context() field accesses inside receivers + // Use local variables where possible for better efficiency if ( f.aggregate.kind === "static_call" && f.aggregate.function.text === "context" && - f.aggregate.args.length === 0 && - f.field.text === "sender" + f.aggregate.args.length === 0 ) { - // Use sender() directly instead of context().sender - wCtx.used("__tact_context_get_sender"); - return `__tact_context_get_sender()`; + const field = f.field.text; + + // Check if we're in an internal receiver context + const currentContext = wCtx.currentContext(); + const writerReceiverType = wCtx.currentReceiverType(); + const isInternalReceiver = + currentContext === "internal-receiver" || + receiverType === "internal" || + writerReceiverType === "internal"; + + // Only use direct variable access inside internal receivers where variables exist + if (isInternalReceiver) { + if (field === "sender") { + return "msg_sender_addr"; + } else if (field === "value") { + return "msg_value"; + } else if (field === "bounceable") { + return "msg_bounceable"; + } else if (field === "raw") { + return "cs"; + } + } else { + // For external receivers, getters, and other contexts, use function calls + if (field === "sender") { + wCtx.used("__tact_context_get_sender"); + return `__tact_context_get_sender()`; + } + // Other fields (value, bounceable, raw) fall through to default field access logic + } } // Resolve the type of the expression @@ -931,6 +956,7 @@ const writeExpressionAux: ( export function writeExpression( f: Ast.Expression, wCtx: WriterContext, + receiverType?: "internal" | "external" | "bounced", ): string { // literals and constant expressions are covered here @@ -953,6 +979,10 @@ export function writeExpression( if (!(error instanceof TactConstEvalError) || error.fatal) throw error; } + if (f.kind === "field_access") { + return writeFieldExpr(f, receiverType)(wCtx); + } + return writeExpressionAux(f)(wCtx); } diff --git a/src/generator/writers/writeFunction.ts b/src/generator/writers/writeFunction.ts index dd65ecfc92..4cb60b4a80 100644 --- a/src/generator/writers/writeFunction.ts +++ b/src/generator/writers/writeFunction.ts @@ -29,9 +29,10 @@ export function writeCastedExpression( expression: Ast.Expression, to: TypeRef, ctx: WriterContext, + receiverType?: "internal" | "external" | "bounced", ) { const expr = getExpType(ctx.ctx, expression); - return cast(expr, to, writeExpression(expression, ctx), ctx); // Cast for nullable + return cast(expr, to, writeExpression(expression, ctx, receiverType), ctx); // Cast for nullable } function unwrapExternal( @@ -68,6 +69,7 @@ export function writeStatement( self: string | null, returns: TypeRef | null | string, ctx: WriterContext, + receiverType?: "internal" | "external" | "bounced", ) { switch (f.kind) { case "statement_return": { @@ -83,6 +85,7 @@ export function writeStatement( f.expression, returns, ctx, + receiverType, ); // Return @@ -114,7 +117,9 @@ export function writeStatement( case "statement_let": { // Underscore name case if (f.name.kind === "wildcard") { - ctx.append(`${writeExpression(f.expression, ctx)};`); + ctx.append( + `${writeExpression(f.expression, ctx, receiverType)};`, + ); return; } @@ -129,11 +134,11 @@ export function writeStatement( if (tt.kind === "contract" || tt.kind === "struct") { if (t.optional) { ctx.append( - `tuple ${funcIdOf(f.name)} = ${writeCastedExpression(f.expression, t, ctx)};`, + `tuple ${funcIdOf(f.name)} = ${writeCastedExpression(f.expression, t, ctx, receiverType)};`, ); } else { ctx.append( - `var ${resolveFuncTypeUnpack(t, funcIdOf(f.name), ctx)} = ${writeCastedExpression(f.expression, t, ctx)};`, + `var ${resolveFuncTypeUnpack(t, funcIdOf(f.name), ctx)} = ${writeCastedExpression(f.expression, t, ctx, receiverType)};`, ); } return; @@ -141,7 +146,7 @@ export function writeStatement( } ctx.append( - `${resolveFuncType(t, ctx)} ${funcIdOf(f.name)} = ${writeCastedExpression(f.expression, t, ctx)};`, + `${resolveFuncType(t, ctx)} ${funcIdOf(f.name)} = ${writeCastedExpression(f.expression, t, ctx, receiverType)};`, ); return; } @@ -163,14 +168,14 @@ export function writeStatement( const tt = getType(ctx.ctx, t.name); if (tt.kind === "contract" || tt.kind === "struct") { ctx.append( - `${resolveFuncTypeUnpack(t, path, ctx)} = ${writeCastedExpression(f.expression, t, ctx)};`, + `${resolveFuncTypeUnpack(t, path, ctx)} = ${writeCastedExpression(f.expression, t, ctx, receiverType)};`, ); return; } } ctx.append( - `${path} = ${writeCastedExpression(f.expression, t, ctx)};`, + `${path} = ${writeCastedExpression(f.expression, t, ctx, receiverType)};`, ); return; } @@ -189,8 +194,8 @@ export function writeStatement( if (f.op === "&&=" || f.op === "||=") { const rendered = f.op === "&&=" - ? `(${path} ? ${writeExpression(f.expression, ctx)} : (false))` - : `(${path} ? (true) : ${writeExpression(f.expression, ctx)})`; + ? `(${path} ? ${writeExpression(f.expression, ctx, receiverType)} : (false))` + : `(${path} ? (true) : ${writeExpression(f.expression, ctx, receiverType)})`; ctx.append(`${path} = ${cast(t, t, rendered, ctx)};`); return; @@ -198,16 +203,16 @@ export function writeStatement( const op = binaryOperationFromAugmentedAssignOperation(f.op); ctx.append( - `${path} = ${cast(t, t, `${path} ${op} ${writeExpression(f.expression, ctx)}`, ctx)};`, + `${path} = ${cast(t, t, `${path} ${op} ${writeExpression(f.expression, ctx, receiverType)}`, ctx)};`, ); return; } case "statement_condition": { - writeCondition(f, self, false, returns, ctx); + writeCondition(f, self, false, returns, ctx, receiverType); return; } case "statement_expression": { - const exp = writeExpression(f.expression, ctx); + const exp = writeExpression(f.expression, ctx, receiverType); if (exp === "") { return; } @@ -220,7 +225,7 @@ export function writeStatement( ); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } }); ctx.append(`}`); @@ -230,7 +235,7 @@ export function writeStatement( ctx.append(`do {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } }); ctx.append( @@ -239,10 +244,12 @@ export function writeStatement( return; } case "statement_repeat": { - ctx.append(`repeat (${writeExpression(f.iterations, ctx)}) {`); + ctx.append( + `repeat (${writeExpression(f.iterations, ctx, receiverType)}) {`, + ); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } }); ctx.append(`}`); @@ -252,7 +259,7 @@ export function writeStatement( ctx.append(`try {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } }); @@ -267,7 +274,7 @@ export function writeStatement( } ctx.inIndent(() => { for (const s of catchBlock.catchStatements!) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } }); } else { @@ -335,7 +342,7 @@ export function writeStatement( ctx.append(`while (${flag}) {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_${vKind}`)}(${path}, ${bits}, ${key}${vBits});`, @@ -349,7 +356,7 @@ export function writeStatement( ctx.append(`while (${flag}) {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_int`)}(${path}, ${bits}, ${key}, 1);`, @@ -363,7 +370,7 @@ export function writeStatement( ctx.append(`while (${flag}) {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_cell`)}(${path}, ${bits}, ${key});`, @@ -377,7 +384,7 @@ export function writeStatement( ctx.append(`while (${flag}) {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_slice`)}(${path}, ${bits}, ${key});`, @@ -395,7 +402,7 @@ export function writeStatement( `var ${resolveFuncTypeUnpack(t.value, funcIdOf(f.valueName), ctx)} = ${ops.typeNotNull(t.value, ctx)}(${ops.readerOpt(t.value, ctx)}(${value}));`, ); for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_${kind}_cell`)}(${path}, ${bits}, ${key});`, @@ -428,7 +435,7 @@ export function writeStatement( ctx.append(`while (${flag}) {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_${vKind}`)}(${path}, 267, ${key}${vBits});`, @@ -442,7 +449,7 @@ export function writeStatement( ctx.append(`while (${flag}) {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_int`)}(${path}, 267, ${key}, 1);`, @@ -456,7 +463,7 @@ export function writeStatement( ctx.append(`while (${flag}) {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_cell`)}(${path}, 267, ${key});`, @@ -470,7 +477,7 @@ export function writeStatement( ctx.append(`while (${flag}) {`); ctx.inIndent(() => { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_slice`)}(${path}, 267, ${key});`, @@ -488,7 +495,7 @@ export function writeStatement( `var ${resolveFuncTypeUnpack(t.value, funcIdOf(f.valueName), ctx)} = ${ops.typeNotNull(t.value, ctx)}(${ops.readerOpt(t.value, ctx)}(${value}));`, ); for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } ctx.append( `(${key}, ${value}, ${flag}) = ${ctx.used(`__tact_dict_next_slice_cell`)}(${path}, 267, ${key});`, @@ -547,13 +554,13 @@ export function writeStatement( }); ctx.append( - `var (${leftHands.join(", ")}) = ${writeCastedExpression(f.expression, t, ctx)};`, + `var (${leftHands.join(", ")}) = ${writeCastedExpression(f.expression, t, ctx, receiverType)};`, ); return; } case "statement_block": { for (const s of f.statements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } return; } @@ -664,6 +671,7 @@ function writeCondition( elseif: boolean, returns: TypeRef | null | string, ctx: WriterContext, + receiverType?: "internal" | "external" | "bounced", ) { const throwCode = extractThrowErrorCode(f.trueStatements, ctx.ctx); const isAloneIf = @@ -674,7 +682,7 @@ function writeCondition( // if (!cond) { throw(X) } => throw_unless(X, cond) const { kind, condition } = rewriteWithConditionalThrow(f); ctx.append( - `${kind}(${writeExpression(throwCode, ctx)}, ${writeExpression(condition, ctx)});`, + `${kind}(${writeExpression(throwCode, ctx, receiverType)}, ${writeExpression(condition, ctx, receiverType)});`, ); return; } @@ -686,19 +694,19 @@ function writeCondition( ); ctx.inIndent(() => { for (const s of f.trueStatements) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } }); if (f.falseStatements && f.falseStatements.length > 0) { const [head, ...tail] = f.falseStatements; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- eslint bug if (head && tail.length === 0 && head.kind === "statement_condition") { - writeCondition(head, self, true, returns, ctx); + writeCondition(head, self, true, returns, ctx, receiverType); } else { ctx.append(`} else {`); ctx.inIndent(() => { for (const s of f.falseStatements!) { - writeStatement(s, self, returns, ctx); + writeStatement(s, self, returns, ctx, receiverType); } }); ctx.append(`}`); @@ -837,7 +845,13 @@ export function writeFunction(f: FunctionDescription, ctx: WriterContext) { // Process statements for (const s of fAst.statements) { - writeStatement(s, returnsStr, f.returns, ctx); + writeStatement( + s, + returnsStr, + f.returns, + ctx, + undefined, + ); } // Auto append return diff --git a/src/generator/writers/writeRouter.ts b/src/generator/writers/writeRouter.ts index 3474b7860d..be7e4760af 100644 --- a/src/generator/writers/writeRouter.ts +++ b/src/generator/writers/writeRouter.ts @@ -62,6 +62,7 @@ export function writeNonBouncedRouter( receivers: Receivers, contract: TypeDescription, wCtx: WriterContext, + receiverType: "internal" | "external" = "internal", ): void { // - Special case: there are no receivers at all if ( @@ -89,7 +90,13 @@ export function writeNonBouncedRouter( const writeBinaryReceivers = (msgOpcodeRemoved: boolean) => { receivers.binary.forEach((binRcv) => { - writeBinaryReceiver(binRcv, msgOpcodeRemoved, contract, wCtx); + writeBinaryReceiver( + binRcv, + msgOpcodeRemoved, + contract, + wCtx, + receiverType, + ); wCtx.append(); }); }; @@ -151,7 +158,7 @@ export function writeNonBouncedRouter( const emptyRcv = receivers.empty; wCtx.append(";; Receive empty message"); wCtx.inBlock("if ((op == 0) & (in_msg_length <= 32))", () => { - writeReceiverBody(emptyRcv, contract, wCtx); + writeReceiverBody(emptyRcv, contract, wCtx, receiverType); }); } @@ -163,6 +170,7 @@ export function writeNonBouncedRouter( typeof receivers.fallback !== "undefined", contract, wCtx, + receiverType, ); if (typeof receivers.fallback !== "undefined") { @@ -179,6 +187,7 @@ function writeBinaryReceiver( msgOpcodeRemoved: boolean, contract: TypeDescription, wCtx: WriterContext, + receiverType: "internal" | "external" | "bounced" = "internal", ): void { const selector = binaryReceiver.selector; if ( @@ -214,7 +223,7 @@ function writeBinaryReceiver( writeCellParser(allocation.root, type, 0, wCtx, name, "in_msg"); } - writeReceiverBody(binaryReceiver, contract, wCtx); + writeReceiverBody(binaryReceiver, contract, wCtx, receiverType); }); } @@ -226,6 +235,7 @@ function writeCommentReceivers( fallbackReceiverExists: boolean, contract: TypeDescription, wCtx: WriterContext, + receiverType: "internal" | "external" = "internal", ): void { // - Special case: no text receivers at all if ( @@ -292,7 +302,7 @@ function writeCommentReceivers( wCtx.append(`;; Receive "${comment}" message`); wCtx.inBlock(`if (text_op == 0x${hash})`, () => { - writeReceiverBody(commentRcv, contract, wCtx); + writeReceiverBody(commentRcv, contract, wCtx, receiverType); }); }); @@ -684,9 +694,10 @@ function writeReceiverBody( rcv: ReceiverDescription, contract: TypeDescription, wCtx: WriterContext, + receiverType: "internal" | "external" | "bounced" = "internal", ): void { for (const stmt of rcv.ast.statements) { - writeRcvStatement(stmt, rcv.effects, contract, wCtx); + writeRcvStatement(stmt, rcv.effects, contract, wCtx, receiverType); } wCtx.append( storeContractVariablesConditionally(rcv.effects, contract, wCtx), @@ -715,6 +726,7 @@ function writeRcvStatement( rcvEffects: ReadonlySet, contract: TypeDescription, wCtx: WriterContext, + receiverType: "internal" | "external" | "bounced" = "internal", ): void { // XXX: if this is the last return statement in the receiver, the user will get contract storage updated twice, // wasting gas, but this is a rare case and we don't want to complicate the code for this, @@ -724,7 +736,7 @@ function writeRcvStatement( contract, wCtx, ); - writeStatement(stmt, null, returns, wCtx); + writeStatement(stmt, null, returns, wCtx, receiverType); } export function messageOpcode(n: Ast.Number): string {