Skip to content

Conversation

@rodiazet
Copy link
Contributor

@rodiazet rodiazet commented Dec 23, 2025

In this PR:

  • TypeChecker rework for MemberAccess implementation
    • Move isPure setting for VariableDeclaration of constant variable to the context of accessing it via TypeType, not via this.
    • Rework TypeType case for ContractType for to properly handle events, errors, structs and enums when accessing via contract name.
    • The same as above but in context of module.
    • Enable removed previously sanity check which did not work
  • Rework nativeMembers implementation and typeViaContractName to return function type with call kind properly set for any context.
  • Add and update tests for member access.

@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 7, 2026
@github-actions github-actions bot removed the stale The issue/PR was marked as stale because it has been open for too long. label Jan 8, 2026
@rodiazet rodiazet force-pushed the fix-sanity-check branch 8 times, most recently from 5a4a8e4 to 154b9bc Compare January 14, 2026 15:56
@argotorg argotorg deleted a comment from github-actions bot Jan 14, 2026
@rodiazet rodiazet marked this pull request as ready for review January 15, 2026 14:55
@rodiazet rodiazet requested a review from cameel January 15, 2026 15:24
@cameel cameel added this to the 0.8.34 milestone Jan 16, 2026
else if (exprType->category() == Type::Category::Module)
{
annotation.isPure = *_memberAccess.expression().annotation().isPure;
// Very similar as for `ContractType`, but additionally we have to handle `Mod.C` case, where `C` in contract
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// Very similar as for `ContractType`, but additionally we have to handle `Mod.C` case, where `C` in contract
// Very similar as for `ContractType`, but additionally we have to handle `Mod.C` case, where `C` is contract

Comment on lines 3303 to 3309
auto const* typeTypeMember = dynamic_cast<TypeType const*>(annotation.type);
typeTypeMember &&
(
typeTypeMember->actualType()->category() == Type::Category::Struct ||
typeTypeMember->actualType()->category() == Type::Category::Enum ||
typeTypeMember->actualType()->category() == Type::Category::Contract
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would add UserDefinedValueType as well.

And if you add that I think this covers pretty much every category that is possible inside a module via language syntax. So I'd simply make it always true for TypeType and just keep the condition as an assert to make sure we're correct about this assumption.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Same thing for the contract case. UserDefinedValueType and definitions are allowed inside contracts. Contract is not, but if it was, I should be marked pure as well, so I'd include it.

This makes the code the same as for modules. And this sounds about right - I don't think this logic is in any way specific to modules. TypeType should generally always be constant. This is what we already do when type-checking Identifier:

else if (dynamic_cast<TypeType const*>(annotation.type))
annotation.isPure = true;

Comment on lines 3293 to 3299
if (auto const* functionTypeMember = dynamic_cast<FunctionType const*>(annotation.type);
functionTypeMember &&
(
functionTypeMember->isPure() ||
functionTypeMember->kind() == FunctionType::Kind::Internal ||
functionTypeMember->kind() == FunctionType::Kind::Event
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this is missing Error. Same for the contract case.

And, again, with that this covers all possible function types you can have as members on a module, so you can just assert and reduce the condition to if (functionTypeMember).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well, technically you could try to tack an External function onto a module this way:

import "m.sol" as M;

library L {
    function f(uint x) external {}
}

contract C {
    using L for *;

    function test() public {
        M.f; // Error: Member "f" not found or not visible after argument-dependent lookup in module "m.sol".
    }
}

using for attaches the function to all types, even those unnameable, so you can use this to attach functions even to things which cannot otherwise be the type of x, e.g. to literals. And we cannot filter out non-matching types simply by checking if the type is the same, because we have to take into account implicit conversions so there is a chance that you could attach your function to weird things this way, even if you won't be able to call it.

Not sure if we're doing more complex filtering here or if it's just hardcoded to only accept some specific types, but either way this fortunately does not compile, so we do not have to worry about external functions after all.

Copy link
Contributor Author

@rodiazet rodiazet Jan 19, 2026

Choose a reason for hiding this comment

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

Error and Declaration are cover by isPure.

// See `ContractType::nativeMembers` for details.
solAssert(annotation.referencedDeclaration);
annotation.isLValue = annotation.referencedDeclaration->isLValue();
// Expressions like `C.foo`, `C.Ev` are pure and they must generate `Statement has no effect.` warning.
Copy link
Collaborator

Choose a reason for hiding this comment

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

What about function pointers?

contract C {
    function() internal returns (uint) fi;
    function() internal returns (uint) gi;
    function() external returns (uint) fe;
    function() external returns (uint) ge;

    function test() public {
        C.fi = C.gi;
        C.fe = C.ge;
    }
}

And generally it looks like you can refer to member variables via contract name, which means that you can have non-pure things as contract type members:

contract C {
    uint x;
    uint y;

    function test() public {
        C.x = C.y;
    }
}

Copy link
Contributor Author

@rodiazet rodiazet Jan 19, 2026

Choose a reason for hiding this comment

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

The comment was a little unclear. I was thinking of C.foo;, C.Ev; statements. The same should apply to C.fi;, C.gi; ,C.fe;, C.ge;, C.x; and C.y;, the warning should be generated. Problem is that it's generated based on isPure flag, but this flag is also used to mark that an expression can be used to initialize a constant (compile-time) variable.
If we set that all the expression have isPure == true, it makes

contract C {
    uint y;
    uint constant x = C.y;
}

compiles. Which is obviously wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have decided to leave internal call kind function as not pure. So C.foo is never pure. In the #8263 the pure flag was set to true for external call kind functions accessed via contract name. Should we revert this or it's ok? @cameel
For the function pointers we mark them pure only if the pointer variable is constant. (Similar to other variables.) For now it never happens because there is no way to initialize constant function pointer (#16339).

Comment on lines 3278 to 3284
// In case `Base.value` or `Lib.value` and when `value` is constant, the expression is pure.
else if (
auto const* varDeclMember = dynamic_cast<VariableDeclaration const*>(annotation.referencedDeclaration);
varDeclMember &&
varDeclMember->isConstant()
)
annotation.isPure = *_memberAccess.expression().annotation().isPure;
annotation.isPure = true;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Currently you're checking isConstant() only on contract types and modules, assuming that the only other possible case is access via a contract variable (which means it must be a getter function). This is probably true, but we should guard it with an assert which will fail if we missed any other case.

Or it might be simpler to just keep the check as it was and only specifically exclude the contract variable case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer to add separated asset.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

BTW this can be also a variable declaration in a struct, accessed via a member access.

Comment on lines 300 to 302
/// @returns the type for members of the containing contract type that refer to this declaration.
/// This can only be called once types of variable declarations have already been resolved.
virtual Type const* typeViaContractName() const { return type(); }
virtual Type const* typeViaContractName(bool) const { return type(); }
Copy link
Collaborator

Choose a reason for hiding this comment

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

The new argument should be documented.

Also, what exactly does it mean? inDerivingScope sounds a little ambiguous, maybe the name should be adjusted? Is it saying whether we're still within the scope that contains the definition or something? EDIT: After reading further I see that's a preexisting name and literally refers to the derived contract. How about calling the parameter inherited?

I would also consider making it an enum. These boolean args we already have are not very readable at the point of call and I usually suggest adding a comment that mentions the parameter name when calling the function anyway. An enum would make that unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reworked. I separated additional function typeViaForeignContractName and removed the flag.

Comment on lines 494 to 499
Type const* FunctionDefinition::typeViaContractName(bool inDerivingScope) const
{
solAssert(
libraryFunction() || visibility() != Visibility::Private,
"Private function members are not available via contract type name."
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe also assert that if inDerivedScope, the function cannot be a library function? That combination would not make sense.

And isVisibleInDerivedContracts() must also be true for that function. On the other hand when not in derived scope, isVisibleInContract() should still be true.

Copy link
Collaborator

@cameel cameel Jan 16, 2026

Choose a reason for hiding this comment

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

Also, when libraryFunction(), assert that isVisibleAsLibraryMember() is true. This will not let you call this with private library functions, but those are not callable as Lib.foo(), only foo() anyway.

And that isImplemented() is true for library functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are cases when accessing private library members is allowed. I.e. in using statement.

Copy link
Contributor Author

@rodiazet rodiazet Jan 21, 2026

Choose a reason for hiding this comment

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

We also cannot isImplemented at this stage as it's checked later in TypeChecker::visit(FunctionDefinition const& _function). Test triggering this error syntaxTests/nameAndTypeResolution/229_call_to_library_function

{
// In case of regular contract being accessed from by external contract (not in deriving scope),
// add only externally visible members.
if (declaration->isVisibleViaContractTypeAccess())
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should rename this function to something like isVisibleViaContractInstance(). I thought it was referring to access like C.foo() until I looked at the definition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

// In case of regular contract being accessed from by external contract (not in deriving scope),
// add only externally visible members.
if (declaration->isVisibleViaContractTypeAccess())
members.emplace_back(declaration, declaration->typeViaContractName(inDerivingScope));
Copy link
Collaborator

Choose a reason for hiding this comment

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

All three branches here now call typeViaContractName(), so you could simplify it all into a single condition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not any more. :)

// In case of regular contract (not library) and member is in the same deriving scope, add all
// members which are not private. Private members cannot be accessed via contract type name
// i.e C.fooPrivate.
if (declaration->visibility() > Visibility::Private)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this will add external functions while previously isVisibleInDerivedContracts() would filter them out.

Copy link
Contributor Author

@rodiazet rodiazet Jan 21, 2026

Choose a reason for hiding this comment

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

Yes. And the Declaration kind was set below. Now it's handled by the typeViaContractName(ContractNameAccessKind _accessKind)

// TODO: because of different kind. Left-hand side of the variable declaration never has `Declaration` kind.
if (auto const* functionTypeMember = dynamic_cast<FunctionType const*>(annotation.type))
{
if(

Choose a reason for hiding this comment

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

Coding style error

annotation.isPure = *_memberAccess.expression().annotation().isPure;
if (auto const* functionTypeMember = dynamic_cast<FunctionType const*>(annotation.type))
{
if(

Choose a reason for hiding this comment

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

Coding style error

@rodiazet rodiazet requested a review from cameel January 21, 2026 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants