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

Visiting type class bodies in experimental analysis #14578

Closed
wants to merge 22 commits into from

Conversation

cameel
Copy link
Member

@cameel cameel commented Sep 28, 2023

Depends on #14566.

This adds "pass 3" as described in Experimental Type System Notes, which is responsible for collecting types of type class member functions.

I also moved builtin class registration to TypeClassRegistration, though builtin classes are going to be removed soon (we'll instead have specific class members marked as builtin).

@@ -216,8 +216,7 @@ bool TypeInference::visit(TypeClassDefinition const& _typeClassDefinition)
subNode->accept(*this);
auto const* functionDefinition = dynamic_cast<FunctionDefinition const*>(subNode.get());
solAssert(functionDefinition);
// TODO: need polymorphicInstance?
auto functionType = polymorphicInstance(typeAnnotation(*functionDefinition));
auto functionType = typeAnnotation(*functionDefinition);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still need to confirm that removing this does not break things and makes sense.

@@ -628,8 +557,6 @@ bool TypeInference::visit(TypeClassInstantiation const& _typeClassInstantiation)
[&](ASTPointer<IdentifierPath> _typeClassName) -> std::optional<TypeClass> {
if (auto const* typeClassDefinition = dynamic_cast<TypeClassDefinition const*>(_typeClassName->annotation().referencedDeclaration))
{
// visiting the type class will re-visit this instantiation
typeClassDefinition->accept(*this);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% I can really remove this.

Initially I assumed that we're doing the separate pass partially to get rid of the necessity to do such a jump. But I see that visit(TypeClassDefinition) then recursively goes back to here and I wonder if this has other consequences in the type system. We do not have any tests that would reveal potential problems though so it's hard to tell.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's still needed. Otherwise type inference breaks if the instantiation is visited before the class:

pragma experimental solidity;

type word = __builtin("word");

type T = word;

instantiation T: D {
    function f(self: T) -> word {}
}

class Self: D {
    function f(self: Self) -> word;
}
TypeError 7428: Type mismatch for function f T -> word != 'x:type -> 'w:type

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need it, if we already construct the full type of the class functions in the previous pass - but yeah, that depends on already constructing types from type expressions, which we still need to disentangle from inference (see also https://github.com/ethereum/solidity/pull/14578/files#r1343054472)

Comment on lines +145 to +149
// For type class members the type is assigned by TypeClassMemberRegistration.
// Fill in the `type` annotation to avoid `visit(FunctionDefinition)` giving it a fresh one.
Type functionType = functionTypes.at(functionDefinition->name());
solAssert(!annotation(*functionDefinition).type.has_value());
annotation(*functionDefinition).type = functionType;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit awkward and I'm starting to doubt if this is really what I was supposed to do. What I did here is to create the function type for every type class member in TypeClassMemberRegistration and then copy it into this annotation here during TypeInference.

I wonder if it would make more sense to have a separate pass that initializes type annotations with fresh variables instead so that other passes before type inference can refer to them.

I also expected to be able to move more of the stuff from visit(TypeClassDefinition) but it seems to be mostly tied to unification so most of it had to stay and I still have iterate over FunctionDefinitions.

The notes say this:

Pass 3: visit type class bodies, collecting function signatures (can involve defined types, so depends on 2!)

The thing is, we're not really collecting signatures - we don't know these until after inference - before it we only have two fresh variables, one for the whole tuple of arguments and one for a tuple of returns. And doing this does not require knowing anything about defined types since those are just two opaque type variables before inference. So I could just as well do this already in TypeClassRegistration.

I must be missing something because I don't really see why exactly this needs to be a separate pass.

Copy link
Member

@ekpyron ekpyron Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do the signatures of type class functions depend on type inference? (They don't...) But yeah, I'll need to look at the rest of the comments and the PR to see how to clarify things :-).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I see the problem now. The signatures of type class functions don't depend on type inference, really - but what type inference also does is to construct types from expressions in type context. That we may need to move to a separate pass actually, since that's what you need to get function signatures before inference.

Copy link
Member Author

@cameel cameel Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify what exactly you understand as "function signatures"? Technically they don't depend on inference but before inference all you know is that it's some under-specified function type - its inputs and outputs are just fresh variables. I was assuming that signature is the fully inferred type.

Actually, initially I assumed that it's what stated in the declaration in the source file (however incomplete it may be), but this does not seem like collecting that would be of any use.

Comment on lines +78 to +87
defineConversion(BuiltinClass::Integer, PrimitiveType::Integer, "fromInteger");

defineBinaryMonoidalOperator(BuiltinClass::Mul, Token::Mul, "mul");
defineBinaryMonoidalOperator(BuiltinClass::Add, Token::Add, "add");

defineBinaryCompareOperator(BuiltinClass::Equal, Token::Equal, "eq");
defineBinaryCompareOperator(BuiltinClass::Less, Token::LessThan, "lt");
defineBinaryCompareOperator(BuiltinClass::LessOrEqual, Token::LessThanOrEqual, "leq");
defineBinaryCompareOperator(BuiltinClass::Greater, Token::GreaterThan, "gt");
defineBinaryCompareOperator(BuiltinClass::GreaterOrEqual, Token::GreaterThanOrEqual, "geq");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matheusaaguiar The task for you now, on top of this PR, would be to remove the whole concept of BuiltinClass and instead make it possible to register these members using __builtin(). This is the remaining part of pass 3 from Daniel's note.

To do this, you'd remove this hard-coded initialization and only fill in the typeClassFunctions/operators annotations when visiting Builtin AST node. Of course only if the usage is correct - only one definition, only those predefined operators/conversions, etc. After this change we'll also no longer need the ability to use a token as type class name, so I'd remove that too (otherwise it will just be dead code).

You'll also need a small parser/AST modification to allow more than one argument in __builtin() (in #14566 I made it expect exactly one).

When you do this, the code that currently looks like this:

pragma experimental solidity;

type word = __builtin("word");

type uint256 = word;

instantiation uint256: + {
    function add(x: uint256, y: uint256) -> uint256 {}
}

function f(x: uint256, y: uint256) -> uint256 {
    return x + y;
}

should look like this:

pragma experimental solidity;

type word = __builtin("word");

type uint256 = word;

__builtin("+", Add.add);

class uint256: Add {
    function add(x: uint256, y: uint256) -> uint256;
}

instantiation uint256: Add {
    function add(x: uint256, y: uint256) -> uint256 {}
}

function f(x: uint256, y: uint256) -> uint256 {
    return x + y;
}

Without __builtin() most of this should still work, but the x + y bit should report an error due to the operator + not being defined.

@cameel cameel force-pushed the new-analysis-defining-builtins branch from 7111950 to 0819e31 Compare September 28, 2023 15:45
@cameel cameel force-pushed the new-analysis-class-body-visitation-pass branch 2 times, most recently from 3a4f754 to bab6e3d Compare September 28, 2023 19:07
Base automatically changed from new-analysis-defining-builtins to newAnalysis September 28, 2023 19:10
@cameel cameel force-pushed the new-analysis-class-body-visitation-pass branch from bab6e3d to a47aa09 Compare September 28, 2023 19:12
Comment on lines +15 to +21
instantiation T: D {
function f(self: T) -> word {}
}

class Self: D {
function f(self: Self) -> word;
}
Copy link
Member Author

@cameel cameel Sep 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is broken after my changes:

/solidity/test/soltest.cpp(120): error: in "syntaxTests/experimental/inference/infinite_recursion_two_functions": Exception during extracted test: /solidity/libsolidity/experimental/analysis/TypeInference.cpp(319): Throw in function virtual bool solidity::frontend::experimental::TypeInference::visit(const solidity::frontend::VariableDeclaration &)
Dynamic exception type: boost::wrapexcept<solidity::langutil::InternalCompilerError>
std::exception::what: Solidity assertion failed
[solidity::util::tag_comment*] = Solidity assertion failed

Looks like I need a check against multiple visitation in TypeInference::visit(FunctionDefinition) after all. The problem is that I can no longer rely on the type annotation for that...

Copy link
Member

@ekpyron ekpyron Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be an issue... this pass shouldn't look at instantiations at all, but only at the class definitions - but at all of those - so the state after this pass should be that all function definitions in class definitions have types from this pass, but none of the ones within intantiations (for types of those we need to rely on inference first) But yeah, not yet sure I understand the problem correctly :-).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The broken bit is in the type inference pass. I can solve it by adding some extra flag, but I don't like that too much and looks like we'll have more things moved to the extra pass so maybe this problem will just solve itself.


m_currentFunctionType = functionType;
functionAnnotation.type = functionType;
// For type class members the type annotation is filled by visit(TypeClassDefinition)
Copy link
Member

@ekpyron ekpyron Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just don't visit the bodies of type classes again here. (EDIT: that also only works if we actually fully determine the function types earlier)

@cameel
Copy link
Member Author

cameel commented Oct 4, 2023

Based on our discussions on the call and on the chat today, here's how I understand what needs to be done in this PR next:

  • Keep it as a separate pass.

  • Create a helper for a specific use case pattern of unification for typing function applications

    Unification is pretty much the most basic operation on types there is - so we should have more helpers for specific patterns involving unification, in particular for the "function application" pattern...

    • E.g. a unification helper that gives you the return type of a function given its generic type and concrete argument types. Currently that usually done ad-hoc inline.
    • If there are any other uses for types, they could also be separate helpers needed only in the new pass.
    • The if (!m_activeInstantiations.empty()) block in TypeInference::unify() should become unnecessary after the PR and can be removed.
  • Visit function definitions, including the whole parameter list and return type.

    • The new pass should have a copy of the type annotation (maybe use a more specific name to keep things clear).
    • Anything needed to fill the type annotation of a function definition inside a type class should be moved to the new pass. For unify() use the new helpers.
    • Basically everything from TypeInference::visit(TypeClassDefinition) should be moved, including the unification.
    • The unification should give us a complete type, with no type variables other than the class variable (validate that).
    • The back and forth visitation between type classes and the instantiation is no longer necessary.
    • Also keep the current logic of visiting non-class functions in type inference.
  • Move all the visitation in type and sort context to the new pass

    • We don't really need the distinction between type and sort context. Do it inline and remove the context mechanism.

@cameel cameel force-pushed the newAnalysis branch 2 times, most recently from f2dbadb to ddbcf57 Compare December 13, 2023 17:32
@r0qs r0qs force-pushed the newAnalysis branch 2 times, most recently from 8d311b9 to 194b114 Compare December 18, 2023 15:55
Base automatically changed from newAnalysis to develop December 18, 2023 20:15
Copy link

github-actions bot commented Jan 2, 2024

This pull request is stale because it has been open for 14 days with no activity.
It will be closed in 7 days unless the stale label is removed.

@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Jan 2, 2024
@cameel cameel removed the stale The issue/PR was marked as stale because it has been open for too long. label Jan 2, 2024
Copy link

This pull request is stale because it has been open for 14 days with no activity.
It will be closed in 7 days unless the stale label is removed.

@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Jan 17, 2024
@ekpyron ekpyron removed the stale The issue/PR was marked as stale because it has been open for too long. label Jan 17, 2024
Copy link

github-actions bot commented Feb 1, 2024

This pull request is stale because it has been open for 14 days with no activity.
It will be closed in 7 days unless the stale label is removed.

@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Feb 1, 2024
@cameel cameel removed the stale The issue/PR was marked as stale because it has been open for too long. label Feb 1, 2024
Copy link

This pull request is stale because it has been open for 14 days with no activity.
It will be closed in 7 days unless the stale label is removed.

@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Feb 16, 2024
@cameel cameel removed the stale The issue/PR was marked as stale because it has been open for too long. label Feb 17, 2024
Copy link

github-actions bot commented Mar 3, 2024

This pull request is stale because it has been open for 14 days with no activity.
It will be closed in 7 days unless the stale label is removed.

@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Mar 3, 2024
@matheusaaguiar matheusaaguiar removed the stale The issue/PR was marked as stale because it has been open for too long. label Mar 4, 2024
Copy link

This pull request is stale because it has been open for 14 days with no activity.
It will be closed in 7 days unless the stale label is removed.

@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Mar 19, 2024
@cameel cameel removed the stale The issue/PR was marked as stale because it has been open for too long. label Mar 20, 2024
Copy link

github-actions bot commented Apr 4, 2024

This pull request is stale because it has been open for 14 days with no activity.
It will be closed in 7 days unless the stale label is removed.

@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Apr 4, 2024
@cameel cameel removed the stale The issue/PR was marked as stale because it has been open for too long. label Apr 4, 2024
@ekpyron ekpyron closed this Apr 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants