From 593f9c34e4559a4ffcdc266bbca5a7e5e9c51f09 Mon Sep 17 00:00:00 2001 From: cryshado Date: Tue, 30 Jul 2024 23:13:27 -0400 Subject: [PATCH] send compile time optimizations --- src/abi/global.ts | 351 ++++++++++++++++++++++++++++++++++++++++++- stdlib/std/base.tact | 37 +++-- stdlib/std/send.tact | 61 -------- 3 files changed, 371 insertions(+), 78 deletions(-) diff --git a/src/abi/global.ts b/src/abi/global.ts index 46a46a587..0efc6c7f8 100644 --- a/src/abi/global.ts +++ b/src/abi/global.ts @@ -5,7 +5,7 @@ import { writeExpression, writeValue, } from "../generator/writers/writeExpression"; -import { throwCompilationError } from "../errors"; +import { throwCompilationError, throwInternalCompilerError } from "../errors"; import { evalConstantExpression } from "../constEval"; import { getErrorId } from "../types/resolveErrors"; import { AbiFunction } from "./AbiFunction"; @@ -13,6 +13,11 @@ import { sha256_sync } from "@ton/crypto"; import path from "path"; import { cwd } from "process"; import { posixNormalize } from "../utils/filePath"; +import { AstExpression } from "../grammar/ast"; +import { Maybe } from "@ton/core/dist/utils/maybe"; +import { WriterContext } from "../generator/Writer"; +import { dummySrcInfo } from "../grammar/grammar"; +import { TypeRef } from "../types/types"; export const GlobalFunctions: Map = new Map([ [ @@ -398,4 +403,348 @@ export const GlobalFunctions: Map = new Map([ }, }, ], + [ + "send", + { + name: "send", + resolve: (ctx, args, ref) => { + if (args.length < 4 || args.length > 7) { + throwCompilationError("send(to: Address, value: Int, bounce: Bool, mode: Int, body: Cell, code: Cell, data: Cell) expects from 4 to 7 arguments", ref); + } + + const arg0 = args[0]!; // to + const arg1 = args[1]!; // value + const arg2 = args[2]!; // bounce + const arg3 = args[3]!; // mode + const arg4 = args[4] as Maybe; // body (may not exist) + const arg5 = args[5] as Maybe; // code (may not exist) + const arg6 = args[6] as Maybe; // data (may not exist) + + // type check of `to` + if (arg0.kind !== 'ref' || arg0.name !== 'Address' || arg0.optional) { + throwCompilationError("send() expects the 1st argument `to` to be `Address`", ref); + } + + // type check of `value` + if (arg1.kind !== 'ref' || arg1.name !== 'Int' || arg1.optional) { + throwCompilationError("send() expects the 2nd argument `value` to be `Int`", ref); + } + + // type check of `bounce` + if (arg2.kind !== 'ref' || arg2.name !== 'Bool' || arg2.optional) { + throwCompilationError("send() expects the 3rd argument `bounce` to be `Bool`", ref); + } + + // type check of `mode` + if (arg3.kind !== 'ref' || arg3.name !== 'Int' || arg3.optional) { + throwCompilationError("send() expects the 4th argument `mode` to be `Int`", ref); + } + + // type check of `body` + if (arg4 && (arg4.kind !== 'ref' || arg4.name !== 'Cell' || arg4.optional)) { + throwCompilationError("send() expects the 5th argument `body` to be `Cell`", ref); + } + + // type check of `code` + if (arg5 && (arg5.kind !== 'ref' || arg5.name !== 'Cell' || arg5.optional)) { + throwCompilationError("send() expects the 6th argument `code` to be `Cell`", ref); + } + + // type check of `data` + if (arg6 && (arg6.kind !== 'ref' || arg6.name !== 'Cell' || arg6.optional)) { + throwCompilationError("send() expects the 7th argument `data` to be `Cell`", ref); + } + + return { kind: "void" }; + }, + generate: (ctx, args, resolved, _ /* ref */) => { + type VComputed = { + kind: 'const'; + value: T + }; + + type VExternal = { + kind: 'ext'; + value: AstExpression + }; + + type V = VComputed | VExternal; + + type StoreUintOperation = { + kind: 'u'; + a0: V; + a1: number; + }; + + type StoreAddressOperation = { + kind: 'a'; + a0: V; + }; + + type StoreCoinsOperation = { + kind: 'c'; + a0: V; + }; + + type StoreRefOperation = { + kind: 'r'; + a0: V; + }; + + type Operation = + | StoreUintOperation + | StoreAddressOperation + | StoreCoinsOperation + | StoreRefOperation; + + ctx.append('{'); + + ctx.inIndent(() => { + ctx.append('builder b = begin_cell();'); + ctx.append(); + + const to = resolved[0]!; // to + const value = resolved[1]!; // value + const bounce = resolved[2]!; // bounce + const mode = resolved[3]!; // mode + const body = resolved[4] as Maybe; // body (may not exist) + const code = resolved[5] as Maybe; // code (may not exist) + const data = resolved[6] as Maybe; // data (may not exist) + + let operations: Operation[] = []; + + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); // int_msg_info$0 tag + operations.push({ kind: 'u', a0: { kind: 'const', value: 1n }, a1: 1 }); // ihr_disabled:Bool + + // bounce:Bool + try { + const bool = evalConstantExpression(bounce, ctx.ctx) as boolean; + const value = (bool ? 1n : 0n) as bigint + operations.push({ kind: 'u', a0: { kind: 'const', value: value }, a1: 1 }); + } catch { + operations.push({ kind: 'u', a0: { kind: 'ext', value: bounce }, a1: 1 }); + } + + + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); // bounced:Bool + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 2 }); // src:MsgAddress + + // dest:MsgAddressInt + try { + const address = evalConstantExpression(to, ctx.ctx) as string; + operations.push({ kind: 'a', a0: { kind: 'const', value: address } }); + } catch { + operations.push({ kind: 'a', a0: { kind: 'ext', value: to } }); + } + + // value:CurrencyCollection -> grams:Grams + try { + const coins = evalConstantExpression(value, ctx.ctx) as bigint; + operations.push({ kind: 'c', a0: { kind: 'const', value: coins } }); + } catch { + operations.push({ kind: 'c', a0: { kind: 'ext', value: value } }); + } + + // value:CurrencyCollection -> other:ExtraCurrencyCollection + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); + + // ihr_fee:Grams fwd_fee:Grams created_lt:uint64 created_at:uint32 + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 4 + 4 + 64 + 32 }); + + if (code !== undefined || data !== undefined) { + // init:(Maybe (Either StateInit ^StateInit)) + operations.push({ kind: 'u', a0: { kind: 'const', value: 1n }, a1: 1 }); + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); + + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); // split_depth:(Maybe (## 5)) + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); // special:(Maybe TickTock) + + // code:(Maybe ^Cell) + if (code) { + operations.push({ kind: 'u', a0: { kind: 'const', value: 1n }, a1: 1 }); + operations.push({ kind: 'r', a0: { kind: 'ext', value: code } }); + } else { + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); + } + + // data:(Maybe ^Cell) + if (data) { + operations.push({ kind: 'u', a0: { kind: 'const', value: 1n }, a1: 1 }); + operations.push({ kind: 'r', a0: { kind: 'ext', value: data } }); + } else { + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); + } + + // library:(Maybe ^Cell), not supported by SendParameters + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); + } else { + // init:(Maybe (Either StateInit ^StateInit)) + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); + } + + if (body) { + // body:(Either X ^X) + operations.push({ kind: 'u', a0: { kind: 'const', value: 1n }, a1: 1 }); + operations.push({ kind: 'r', a0: { kind: 'ext', value: body } }); + } else { + // body:(Either X ^X) + operations.push({ kind: 'u', a0: { kind: 'const', value: 0n }, a1: 1 }); + } + + // move refs to the end 💀 + operations = [ + ...operations.filter(item => item.kind !== 'r'), + ...operations.filter(item => item.kind === 'r') + ]; + + const optimize = (operations: Operation[]): Operation[] => { + let i = 0; + const out: Operation[] = []; + + while (i < operations.length) { + const curr = operations[i]!; + + // u(any), u(any) -> u + if (curr.kind === 'u' && i + 1 < operations.length && operations[i + 1]!.kind === 'u') { + const cop = curr; + const nop = operations[i + 1]! as StoreUintOperation; + + if (cop.a0.kind !== 'const' || nop.a0.kind !== 'const') { + out.push(curr); + i++; + continue; + } + + const shifted = cop.a0.value << BigInt(nop.a1); + const combined = shifted | nop.a0.value; + const newsize = cop.a1 + nop.a1; + + out.push({ kind: 'u', a0: { kind: 'const', value: combined }, a1: newsize }); + + i += 2; + continue; + } + + // c(0), u(any) -> u + if (curr.kind === 'c' && i + 1 < operations.length && operations[i + 1]!.kind === 'u') { + const cop: StoreCoinsOperation = curr; + const nop: StoreUintOperation = operations[i + 1]! as StoreUintOperation; + + if (cop.a0.kind !== 'const' || nop.a0.kind !== 'const') { + out.push(curr); + i++; + continue; + } + + if (cop.a0.value !== 0n) { + out.push(curr); + i++; + continue; + } + + out.push({ kind: 'u', a0: { kind: 'const', value: nop.a0.value }, a1: nop.a1 + 4 }); + + i += 2; + continue; + } + + out.push(curr); + i++; + } + + return out; + }; + + const isseq = (set1: Operation[], set2: Operation[]): boolean => { + if (set1.length !== set2.length) { + return false; + } + + for (let i = 0; i < set1.length; i++) { + const op1 = set1[i]!; + const op2 = set2[i]!; + + if (op1.kind !== op2.kind) { + return false + } + } + + return true; + } + + let prevset: Operation[] = []; + let currset: Operation[] = operations; + + while (!isseq(prevset, currset)) { + prevset = currset; + currset = optimize(currset); + } + + operations = currset; + + const writeU = (ctx: WriterContext, op: StoreUintOperation) => { + if (op.a0.kind === 'const') { + ctx.append(`b = store_uint(b, ${op.a0.value}, ${op.a1});`); + } else { + const exp = writeExpression(op.a0.value, ctx) + ctx.append(`b = store_uint(b, ${exp}, ${op.a1});`); + } + }; + + const writeA = (ctx: WriterContext, op: StoreAddressOperation) => { + ctx.used(`__tact_store_address`); + + if (op.a0.kind === 'const') { + ctx.append(`b = __tact_store_address(b, "${op.a0.value}"a);`); + } else { + const exp = writeExpression(op.a0.value, ctx); + ctx.append(`b = __tact_store_address(b, ${exp});`); + } + }; + + const writeC = (ctx: WriterContext, op: StoreCoinsOperation) => { + if (op.a0.kind === 'const') { + ctx.append(`b = store_coins(b, ${op.a0.value});`); + } else { + const exp = writeExpression(op.a0.value, ctx); + ctx.append(`b = store_coins(b, ${exp});`); + } + }; + + const writeR = (ctx: WriterContext, op: StoreRefOperation) => { + if (op.a0.kind === 'const') { + throwInternalCompilerError('StoreRefOperation is impossible to be const', dummySrcInfo) + } else { + const exp = writeExpression(op.a0.value, ctx); + ctx.append(`b = store_ref(b, ${exp});`); + } + }; + + for (const op of operations) { + switch (op.kind) { + case 'u': + writeU(ctx, op); + break; + case 'a': + writeA(ctx, op); + break; + case 'c': + writeC(ctx, op); + break; + case 'r': + writeR(ctx, op); + break; + } + } + + const modexp = writeExpression(mode, ctx); + + ctx.append(); + ctx.append(`send_raw_message(end_cell(b), ${modexp});`); + }); + + return '}' + } + } + ] ]); diff --git a/stdlib/std/base.tact b/stdlib/std/base.tact index 9e8cdd7dc..81da53f6e 100644 --- a/stdlib/std/base.tact +++ b/stdlib/std/base.tact @@ -1,37 +1,42 @@ trait BaseTrait { virtual const storageReserve: Int = 0; + + fun sendmsg(to: Address, body: Cell, bounce: Bool, mode: Int, stateInit: StateInit?) { + let value: Int = 0; + + if (stateInit != null) { + let sinit: StateInit = stateInit!!; + send(to, value, bounce, mode, body, sinit.code, sinit.data); + } else { + send(to, value, bounce, mode, body); + } + } - virtual inline fun reply(body: Cell?) { + virtual inline fun reply(body: Cell) { self.forward(sender(), body, true, null); } - virtual inline fun notify(body: Cell?) { + virtual inline fun notify(body: Cell) { self.forward(sender(), body, false, null); } - virtual fun forward(to: Address, body: Cell?, bounce: Bool, init: StateInit?) { - - let code: Cell? = null; - let data: Cell? = null; - if (init != null) { - let init2: StateInit = init!!; - code = init2.code; - data = init2.data; - } - - // Lock storage if needed + virtual fun forward(to: Address, body: Cell, bounce: Bool, stateInit: StateInit?) { if (self.storageReserve > 0) { // Optimized in compile-time let ctx: Context = context(); let balance: Int = myBalance(); let balanceBeforeMessage: Int = balance - ctx.value; + if (balanceBeforeMessage < self.storageReserve) { nativeReserve(self.storageReserve, ReserveExact); - send(SendParameters{bounce: bounce, to: to, value: 0, mode: SendRemainingBalance | SendIgnoreErrors, body: body, code: code, data: data }); + + let mode: Int = SendRemainingBalance | SendIgnoreErrors; + self.sendmsg(to, body, bounce, mode, stateInit); + return; } } - // Just send with remaining balance - send(SendParameters{bounce: bounce, to: to, value: 0, mode: SendRemainingValue | SendIgnoreErrors, body: body, code: code, data: data }); + let mode: Int = SendRemainingValue | SendIgnoreErrors; + self.sendmsg(to, body, bounce, mode, stateInit); } } \ No newline at end of file diff --git a/stdlib/std/send.tact b/stdlib/std/send.tact index cf20dbecb..ae3f4a06b 100644 --- a/stdlib/std/send.tact +++ b/stdlib/std/send.tact @@ -8,67 +8,6 @@ const SendPayGasSeparately: Int = 1; const SendDestroyIfZero: Int = 32; const SendBounceIfActionFail: Int = 16; -struct SendParameters { - bounce: Bool = true; - to: Address; - value: Int; - mode: Int = 0; - body: Cell? = null; - code: Cell? = null; - data: Cell? = null; -} - -fun send(params: SendParameters) { - let b: Builder = beginCell(); - b = b.storeInt(1, 2); // internal_message + ihd_disabled - b = b.storeBool(params.bounce); // bounce - b = b.storeInt(0, 3); // bounced + from - b = b.storeAddress(params.to); // To - b = b.storeCoins(params.value); // Value - b = b.storeInt(0, 1 + 4 + 4 + 64 + 32); // currency_collection + IHR fees + Fwd fees + CreatedLT + CreatedAt - - // Stateinit - if (params.code != null || params.data != null) { - b = b.storeBool(true); // State init - - // Assemble state init cell - let bc: Builder = beginCell(); - bc = bc.storeBool(false); // SplitDepth - bc = bc.storeBool(false); // TickTock - if (params.code != null) { - bc = bc.storeBool(true); // Code presence - bc = bc.storeRef(params.code!!); - } else { - bc = bc.storeBool(false); // Code presence - } - if (params.data != null) { - bc = bc.storeBool(true); // Data presence - bc = bc.storeRef(params.data!!); - } else { - bc = bc.storeBool(false); // Data presence - } - bc = bc.storeBool(false); // Library - - b = b.storeBool(true); // Store as ref - b = b.storeRef(bc.endCell()); - } else { - b = b.storeBool(false); // No state init - } - - // Body - let body: Cell? = params.body; - if (body != null) { - b = b.storeBool(true); - b = b.storeRef(body!!); - } else { - b = b.storeBool(false); // No body - } - - // Send - let c: Cell = b.endCell(); - nativeSendMessage(c, params.mode); -} - inline fun emit(body: Cell) { // ext_out_msg_info$11 src:MsgAddressInt dest:MsgAddressExt created_lt:uint64 created_at:uint32 // maybe: stateInit (false) bodyRef: bool (true)