Skip to content

Constant evaluator support for member constants #16056

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

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from

Conversation

matheusaaguiar
Copy link
Collaborator

Fix #16055.

@matheusaaguiar matheusaaguiar self-assigned this May 18, 2025
@matheusaaguiar matheusaaguiar force-pushed the constantEvaluatorSupportForMemberAccess branch 2 times, most recently from 93d7f5a to 78e1d3e Compare May 19, 2025 04:41
@matheusaaguiar matheusaaguiar marked this pull request as ready for review May 19, 2025 04:41
@matheusaaguiar matheusaaguiar force-pushed the constantEvaluatorSupportForMemberAccess branch from 1e7955b to 985b03f Compare May 20, 2025 15:13
Copy link
Member

@clonker clonker left a comment

Choose a reason for hiding this comment

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

I am not sure if there aren't any unwanted side-effects hiding in this. Looks good to me generally, though!

@matheusaaguiar matheusaaguiar force-pushed the constantEvaluatorSupportForMemberAccess branch from ad3f23a to 7fa4363 Compare May 22, 2025 22:26
clonker
clonker previously approved these changes May 23, 2025
Copy link
Member

@clonker clonker left a comment

Choose a reason for hiding this comment

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

LGTM aside from the typo

@matheusaaguiar matheusaaguiar force-pushed the constantEvaluatorSupportForMemberAccess branch 2 times, most recently from e596763 to d5beb9e Compare May 23, 2025 14:35
@matheusaaguiar matheusaaguiar force-pushed the constantEvaluatorSupportForMemberAccess branch from d5beb9e to 9d5f93f Compare June 4, 2025 13:57
@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Jun 19, 2025
@matheusaaguiar matheusaaguiar removed the stale The issue/PR was marked as stale because it has been open for too long. label Jun 24, 2025
@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Jul 9, 2025
@matheusaaguiar matheusaaguiar removed the stale The issue/PR was marked as stale because it has been open for too long. label Jul 9, 2025
@matheusaaguiar matheusaaguiar force-pushed the constantEvaluatorSupportForMemberAccess branch 3 times, most recently from 66a8367 to a1c8007 Compare July 10, 2025 15:16
clonker
clonker previously approved these changes Jul 10, 2025
@matheusaaguiar matheusaaguiar force-pushed the constantEvaluatorSupportForMemberAccess branch from a1c8007 to 1145d25 Compare July 14, 2025 12:33
Copy link
Member

@cameel cameel left a comment

Choose a reason for hiding this comment

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

Actually, there are some problems here.

Comment on lines 415 to 417
if (auto const* parentIdentifier = dynamic_cast<Identifier const*>(&_memberAccess.expression()))
{
if (auto const* contract = dynamic_cast<ContractDefinition const*>(parentIdentifier->annotation().referencedDeclaration))
Copy link
Member

Choose a reason for hiding this comment

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

This does not cover all relevant cases. I think it will break at least on these:

  • m.CONST (expression is a module)
  • (C).CONST (expression is not an identifier)
  • m.C.CONST (member access on a member access)

Especially the first and the third one are not far fetched at all.

These are probably not all cases either. I can also think about a few more that are now disallowed due to our previous changes in implicit conversions:

  • [C][0].CONST
  • (true ? C : C).CONST

Copy link
Member

Choose a reason for hiding this comment

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

This should really be possible to implement in a more general way, without handling all those cases one by one. Have you tried getting the constant definition via _memberAccess.annotation().referencedDeclaration?

Copy link
Collaborator Author

@matheusaaguiar matheusaaguiar Jul 15, 2025

Choose a reason for hiding this comment

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

The problem is that constant evaluation is done "on demand" whenever it is needed, so in some (most?) cases information like referencedDeclaration may not be present and will only be calculated later.
For example, in the case of array sizes, the expression will be evaluated during the DeclarationTypeChecker step and referencedDeclaration is resolved later when TypeChecker step runs.
On the other hand, in the case of the custom storage layout of the contract, the evaluation would occur during the same step and information like referencedDeclaration would be available.
I ended up focusing only in the simplest use of contract constants, and then later was considering handling modules in a different PR to not hold this one back.
I am not sure about expressions like (true ? C : C).CONST, I think maybe it kinda falls out of scope, because currently the constant evaluator can't handle them, so it would not be able to yield C as the "result" there...

Copy link
Member

Choose a reason for hiding this comment

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

in some (most?) cases information like referencedDeclaration may not be present and will only be calculated later.

I see. I looked at this and I think we can change this though. In cases that we are interested in, we could actually calculate referencedDeclaration earlier.

In most cases referencedDeclaration is set by ReferencesResolver, which runs even before DeclarationTypeChecker. It just defers resolution of some Identifiers and all of MemberAccess to TypeChecker, because resolving them often required knowledge about the type. This happens for two reasons:

  1. The meaning of the member may change depending on the type of the identifier. E.g. depending on the type of x, x.transfer() can mean:
    • An external call on a contract instance.
    • An internal call on a contract type.
    • A call of the transfer() builtin on address payable.
    • A call of a function pointer stored in a struct.
  2. using for can affect overload resolution by adding new candidates to a scope.

However, not all cases are this complicated. Some could be resolved earlier. Specifically member access on an identifier that refers to a known scope (so members are already known) and where the member being accessed is not a function (so overloading is impossible). Our case falls into this, because the name of a contract/library/interface type or of a module represents a scope (that cannot be extended with using for) and constants cannot be overloaded.

So the solution I'd propose here is to add MemberAccess handler for such constants to ReferencesResolver. Then you'll be able to safely access referencedDeclaration in ConstantEvaluator even at this early stage. It not being set will simply mean that it's not a constant.

Copy link
Member

Choose a reason for hiding this comment

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

I am not sure about expressions like (true ? C : C).CONST, I think maybe it kinda falls out of scope, because currently the constant evaluator can't handle them, so it would not be able to yield C as the "result" there...

Yes, we do not have to make this case constant evaluated - it does not work for numbers either. My point was that there may be more such expressions and with your approach we'd have to find and special-case them all. It's not really about whether these particular cases are useful, but about the language being consistent. It should work based on generic rules rather than require us to enumerate cases in which things work. The rule is just that constants can be member-accessed and this rule should compose nicely with the rules we already have on what things can contain constants and rules for how expressions work. We should not have to analyze the expression again here.

I'm more concerned about the (C).CONST case. It's also an expression and this one should work. And that will actually be a problem. My ReferencesResolver suggestion above can cover things like m.C.CONST in a generic way, but anything where the expression is more complex would require special handling for such expressions in ReferencesResolver.

One way around it might be to create something similar to ConstantEvaluator but for evaluating types. So that it could tell us that (C) is really just C. It could give up on anything that involves variables. ReferencesResolver could run that on the base expression of MemberAccess and resolve it. Still, I don't think it's worth the effort for now. We can deal with it later, when we expand the constant evaluation.

So for now, in this PR, it is fine for ConstantEvaluator to just give up on (C).CONST due to referencedDeclaration not being set.

Copy link
Member

@cameel cameel Jul 14, 2025

Choose a reason for hiding this comment

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

We need more tests. At least:

  • Access to a non-constant member should produce an appropriate error.
  • Member access that is not a top-level expression in array size, e.g. a[1 + C.CONST + 2].
  • Nesting: member access to a constant whose value contains member access as well.
  • Cycles: Two member access constants using each other as values (should be an error).

Especially the last one looks worrying. I just found #12928 (comment), which may be the reason we had this limitation to begin with. If that's the case, then properly solving this may require deeper changes to analysis phases.

Copy link
Member

Choose a reason for hiding this comment

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

If that's the case, then properly solving this may require deeper changes to analysis phases.

It should be fine actually. The comment mentions that constant cycles are checked only in PostTypeChecker, but we also have a recursion depth check in ConstantEvaluator::evaluate(), which will catch the cycle if a cyclic constant is used as an array length. This already works for constant Identifiers and should work for MemberAccess as well.

It's not great that it's not PostTypeChecker that catches this, but we can live for now and we can try to fix it when we get to implementing compile-time constant evaluation.

@matheusaaguiar matheusaaguiar force-pushed the constantEvaluatorSupportForMemberAccess branch from 08f11e6 to 4496590 Compare July 21, 2025 13:32
@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Aug 6, 2025
@cameel cameel removed the stale The issue/PR was marked as stale because it has been open for too long. label Aug 7, 2025
@ethereum ethereum deleted a comment from github-actions bot Aug 10, 2025
@ethereum ethereum deleted a comment from github-actions bot Aug 10, 2025
@ethereum ethereum deleted a comment from github-actions bot Aug 10, 2025
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.

Compile-time evaluation of member constants
3 participants