Skip to content
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

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

Closed
novusnota opened this issue May 17, 2024 · 8 comments · Fixed by #418
Assignees
Labels
gas Gas consumption and fee-related things stdlib The Tact standard library ux
Milestone

Comments

@novusnota
Copy link
Member

novusnota commented May 17, 2024

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.

@novusnota novusnota changed the title Consider adding more higher-level approaches to build and parse Cells Consider adding more higher-level approaches to build and parse Cells, which would be compile-time friendly May 17, 2024
@Gusarich
Copy link
Member

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

@novusnota
Copy link
Member Author

novusnota commented May 17, 2024

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 :)

@novusnota novusnota added ux stdlib The Tact standard library gas Gas consumption and fee-related things labels May 21, 2024
@Gusarich
Copy link
Member

Gusarich commented Jun 11, 2024

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
Copy link
Member

anton-trunov commented Jun 11, 2024

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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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
Copy link
Member

Gusarich commented Jun 14, 2024

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
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
gas Gas consumption and fee-related things stdlib The Tact standard library ux
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants