tact-lang / tact

Tact compiler main repository
https://tact-lang.org
MIT License
360 stars 100 forks source link

Consider adding more higher-level approaches to build and parse Cells, which would be compile-time friendly #344

Closed novusnota closed 3 months ago

novusnota commented 4 months ago

Motivation

Currently, one has to use the Builder pattern to construct Cells starting with beginCell() and then storeSmth() up as needed. Then, for parsing some obtained Cell one has to convert it to Slice and use loadSmth() functions until it's fully read. It is alright for FunC, but we can definitely do better.

Existing stuff

As the simple construction of messages goes, we already have SendParameters Struct alongside with the send() function, which are quite easy to reason about — it should be possible to come up with something like that, but now for parsing messages. And for parsing raw field of the Context Struct too!

Gas optimization

As a bonus, this may lead to new compile-time optimizations (read: off-chain computation regarding Cells), which are a massive win in terms of gas usage — beginCell() on its own costs around 500 gas to begin with, despite not doing much. If we could pre-compute as much of the Cell construction as possible, we may achieve massive gas benefits.

Potentially, with new ways to parse common TL-B layouts of Cells (or even introducing neat mappings to generalize work with any TL-B layouts?), Cell parsing can also get optimized away too. But that's more of a suggestion, the real thing to focus in message parsing/composing.

Gusarich commented 4 months ago

Can you provide more context please? What are the cases where is it useful to parse/compose cells other than messages?

novusnota commented 4 months ago

That was just a suggestion to think about automated or semi-automated TL-B ←→ Tact mappings, but the focus is obviously on composing and parsing messages, until we have more use-cases for other stuff :)

Gusarich commented 3 months ago

Not sure if you meant this in the issue, but we need a way to convert Cell → Struct for sure. Example: you want to parse forward_payload from jetton transfer. Currently you have to use .loadXXX functions and manually work with cells, but we don't want Tact devs to do that.

Something like myStruct.fromCell(myCell) or myStruct.fromCell(myCell.beginParse()) should do the job.

anton-trunov commented 3 months ago

It needs to be a function of type Cell -> Struct? because deserialization (cell parsing) can fail to produce a valid struct.

myStruct.fromCell(myCell)

This is not going to work, because you don't have that struct at hand, this is something you aim to produce. If our structs had associated namespaces, then Tact could generate cell parsers and put them under the struct's namespace, e.g. for struct Foo { ... } it would look like Foo::fromCell(c: Cell): Foo?. Another option would be to generate methods like .toFoo(self: Cell): Foo? for each Foo struct.

Gusarich commented 3 months ago

It needs to be a function of type Cell -> Struct? because deserialization (cell parsing) can fail to produce a valid struct.

I believe that it makes more sense to throw an error in this case instead of returning null

anton-trunov commented 3 months ago

I believe that it makes more sense to throw an error in this case instead of returning null

Is it because it will be more gas efficient? Or some other reason?

I usually like to use type systems for these kinds of things, because it's a good documentation, always up-to-date. Also, what would be the default mode for calling those cell parsers?

try {
  MyStruct.fromCell(c)
} catch (e) {
  if (e == exit_code_for_struct_parsing_error) {
    /// do some damage control
    /// like send funds somewhere, etc.
  }
}

Let's come up with a realistic use case and illustrate how this should work before finalizing the design decision.

Gusarich commented 3 months ago

Is it because it will be more gas efficient? Or some other reason?

First of all, in cases of error throwing, the developer has the ability to check the error code at runtime.

Another thing is that, in order to implement what you suggested, we'll have to either use try-catch under the hood or add if-else statements for each field during parsing. The first option makes no sense because it just takes away some available data (the error code) from the developer, and the second option is very gas-consuming.

Gusarich commented 3 months ago

Let's come up with a realistic use case and illustrate how this should work before finalizing the design decision.

As I've already said above: one of the common use cases for struct parsing is working with various payloads in messages (such as forward payloads in token transfers). Currently, in order to handle it the dev does something like this:

let s = msg.forwardPayload;
// suppose that we expect forward payload here to has some exact structure
let someField1 = s.loadSomething();
let someField2 = s.loadSomething();
let someField3 = s.loadSomething();

// work with values

And with this addition, it'll look like this:

let payload = MyStruct.fromSlice(msg.forwardPayload);
// work with values

Or in case when the dev wants to perform some action if incorrect payload is provided in message:

let payload: MyStruct;
try {
    payload = MyStruct.fromSlice(msg.forwardPayload);
} catch {
    // return tokens back to the sender
}

However, there still won't be a way to handle multiple kinds of payloads without some raw work with cells, so in such cases dev will have to do this:

let op = msg.forwardPayload.loadUint(32);
if (op == SOME_OP) {
    let payload = MyStruct.fromSlice(msg.forwardPayload);
    // do something
} else if (op == SOME_OTHER_OP) {
    let payload = AnotherStruct.fromSlice(msg.forwardPayload);
    // do something else
}