tact-lang / tact

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

`self` in traits and contracts #657

Open anton-trunov opened 1 month ago

anton-trunov commented 1 month ago

Problem statement

Take a look at the following snippet.

 import "@stdlib/deploy";

trait Trait {
    id: Int;

    get fun contractData(): Trait {     // typechecker thinks it's ok here
        return self;
    }
}

contract Contract with Deployable, Trait {
    id: Int as uint32 = 0;
}

Its compilation fail with the following error message:

💼 Compiling project test-trait ...
Tact compilation failed
Error: test-trait.tact:5:9: Type mismatch: "Contract" is not assignable to "Trait"
Line 5, col 9:
  4 |     get fun contractData(): Trait {
> 5 |         return self;
              ^~~~~~~~~~~~
  6 |     }

Inside the Trait trait the self variable has type Trait, but then contractData gets re-typechecked as a contract getter inside the Contract contract where self has type Contract.

Possible solutions

First of all, let me note that disallowing re-typechecking is not a solution, since during code generation we need the full type information.

Forbid using self as a standalone variable

In other words, passing self around, returning it from functions, including getters (see also a related issue: #579) would be prohibited. Only expressions of the form self.Var would be allowed in this case.

This restriction's scope could be limited to traits only, so a whole-contract state getter featured in #579 is still possible.

Introduce Self type variable

So self: Self in any context (including mutating methods). This would allow to generalize the type of the problematic getter as follows:

trait Trait {
    id: Int;

    get fun contractData(): Self {  // `Self` means `Trait` here 
        return self;
    }
}

But when the getter gets inserted in the contract scope, the contractData's result type will be Contract.

This solution would support to implement a trait that allows easier debugging by providing the whole contract state:

trait ContractStateGetter {
    get fun contractState(): Self { return self }
}

which could be included automatically when the debug config option is set to true.

novusnota commented 1 month ago

For consistency reasons, the first solution (with prohibiting self in traits) seems the way to go.

Also, what are the fields and methods for the second use case — all of them? What happens if we pass such contract self to some function that handles it, what it would be able to do?

anton-trunov commented 1 month ago

Also, what are the fields and methods for the second use case — all of them?

In the getter example we just treat a contract as basically a struct

What happens if we pass such contract self to some function that handles it, what it would be able to do?

Anything possible for a struct with extension functions

novusnota commented 1 month ago

In the getter example we just treat a contract as basically a struct

Hmm, then the second is nicer in terms of UX. Would that make contracts into Struct-like types, such that the following could be defined?

contract Fizz {
    m: map<Int, Int>;
}

struct Buzz {
    field: Fizz; // field of type Fizz, which is the contract above, but treated as a Struct
}
anton-trunov commented 1 month ago

Would that make contracts into Struct-like types, such that the following could be defined?

I think yes. It does not seem to be insanely useful but I believe this should be acceptable code

novusnota commented 1 month ago

I think yes. It does not seem to be insanely useful but I believe this should be acceptable code

There's an interesting limitation to that — contract's name must be capitalized to use in type annotations, as they only allow identifiers starting with a capital letter. That is, not all contracts qualify. But that's not a big deal, I guess