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

Add modifier areas #3939

Closed
wants to merge 13 commits into from
Closed

Conversation

randomnetcat
Copy link
Contributor

@randomnetcat randomnetcat commented Apr 18, 2018

Closes #623.

Yes, the code here is pretty sloppy. Feedback (from maintainers and others) is welcome and encouraged!
I'm especially interested in hearing suggestions people have for syntax.

Syntax:

apply ... { ... }

The ellipsis after apply can contain any modifier invocations, a state mutability specifier, and a visibility mutability specifier, or any combination of the above. Inside the area there can be more modifier areas (modifier invocations are cumulative) and function declarations. Nested modifier areas and function declarations cannot have a different visibility or mutability than their modifier areas.

  • Modifier areas
  • Nested modifier areas
  • Visibility areas
  • Mutability areas
  • Some tests
  • Lots of tests

@randomnetcat
Copy link
Contributor Author

Look at that... already found a (significant) bug. (now fixed)

Copy link
Member

@ekpyron ekpyron left a comment

Choose a reason for hiding this comment

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

At least the dependency cycle between modifier areas and function definitions needs to be resolved.

@@ -12,7 +12,7 @@ ContractDefinition = ( 'contract' | 'library' | 'interface' ) Identifier
'{' ContractPart* '}'

ContractPart = StateVariableDeclaration | UsingForDeclaration
| StructDefinition | ModifierDefinition | FunctionDefinition | EventDefinition | EnumDefinition
| StructDefinition | ModifierDefinition | FunctionDefinition | EventDefinition | EnumDefinition | ModifierArea
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a line break may be good.

docs/grammar.txt Outdated
@@ -32,6 +32,9 @@ EventDefinition = 'event' Identifier EventParameterList 'anonymous'? ';'
EnumValue = Identifier
EnumDefinition = 'enum' Identifier '{' EnumValue? (',' EnumValue)* '}'

ModifierArea = 'using modifier' ModifierInvocation (',' ModifierInvocation)*
'{' (ModifierArea | FunctionDefinition) '}'
Copy link
Member

Choose a reason for hiding this comment

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

Is this correct? Can't there be multiple function definitions and modifier areas in a modifier area? Actually there should also be test cases for that (at the moment I only see test cases with a single function definition in a modifier area).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I think I just forgot the asterisk.


Multiple modifier invocations can be used in a single modifier area:
`using modifier A, B { /* ... */ }` declares a modifier area
where both modifiers are used on every function.a
Copy link
Member

Choose a reason for hiding this comment

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

I guess the a in the end is a typo?

std::vector<FunctionDefinition const*> totalFunctions = filteredNodes<FunctionDefinition>(m_subNodes);
std::vector<ModifierArea const*> baseModifierAreas = modifierAreas();
std::vector<ModifierArea const*> nextGroups;
while (!baseModifierAreas.empty())
Copy link
Member

Choose a reason for hiding this comment

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

I think this may be easier to read, if you used a queue of ModifierArea const*. Then you could extract the front element into a local variable, pop it from the queue, process it and while doing so just add the subareas back to the queue (and continue until the queue is empty).

This may only be my personal taste, though, and it should also be fine as it is now.

if (m_modifierArea)
{
auto const areaModifiers = _modifierArea->totalModifiers();
realModifiers.insert(realModifiers.begin(), areaModifiers.begin(), areaModifiers.end());
Copy link
Member

Choose a reason for hiding this comment

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

There are overloads for + and += for vectors in libdevcore (I think) - so you should be able to just use
realModifiers = areaModifiers + realModifiers;.

if (m_parent)
{
std::vector<ASTPointer<ModifierInvocation>> parentModifiers = m_parent->totalModifiers();
completeVector.insert(completeVector.begin(), parentModifiers.begin(), parentModifiers.end());
Copy link
Member

Choose a reason for hiding this comment

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

Same as above. Also: why do you need the local variable completeVector? You could just start by setting m_totalModifiers = m_declaredModifiers; and then continue with m_totalModifiers = parentModifiers + m_totalModifiers;. Even better: do it the other way round: if there is a parent first set m_totalModifiers = parentModifers; and then use m_totalModifiers += m_declaredModifiers; after the ìf.

if (_visitor.visit(*this))
{
listAccept(m_declaredModifiers, _visitor);
listAccept(*m_functions, _visitor);
Copy link
Member

Choose a reason for hiding this comment

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

Somewhere you should probably assert that these members (m_functions, m_subAreas) aren't null pointers. Either here or in the constructor of ModifierArea or even at both places (or otherwise this should be an if (m_functions) listAccept(...) if the pointer may actually be null, although it doesn't look like they can... as stated above it may make sense for them not to be pointer at all).

RecursionGuard recursionGuard(*this);

if (m_scanner->peekNextToken() == Token::Identifier)
{
Copy link
Member

Choose a reason for hiding this comment

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

We usually don't use brackets for single line if, resp. else.

private:
std::vector<ASTPointer<ModifierInvocation>> m_totalModifiers;
std::vector<ASTPointer<ModifierInvocation>> m_declaredModifiers;
ASTPointer<std::vector<ASTPointer<FunctionDefinition>> const> m_functions;
Copy link
Member

Choose a reason for hiding this comment

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

Actually: it doesn't seem like those can ever be null or that they are passed around, resp. shared - so why use ASTPointers and not just the vectors themselves as members?

ASTPointer<std::vector<ASTPointer<FunctionDefinition>>> functions = std::make_shared<std::vector<ASTPointer<FunctionDefinition>>>();
ASTPointer<std::vector<ASTPointer<ModifierArea>>> subAreas = std::make_shared<std::vector<ASTPointer<ModifierArea>>>();

ASTPointer<ModifierArea> modifierArea = nodeFactory.createNode<ModifierArea>(modifiers, functions, subAreas, _parent);
Copy link
Member

Choose a reason for hiding this comment

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

OK - this is probably why you use ASTPointer's for the functions and subareas in ModifierArea - so that you can create the modifier area here and still fill in the functions and subareas below. However: are you sure you don't introduce cyclic dependencies of shared pointers here? The modifier areas contains a list of shared pointers to function definitions and the function definitions contain a shared pointer back to the modifier area? You probably at least need to change the parent pointers in the function definitions to a plain pointer instead of a shared pointer, although it would even be better if you could avoid such cyclic constructions altogether, if at all possible.

@randomnetcat
Copy link
Contributor Author

@ekpyron , thanks for all of the feedback. I can't actually do anything for 5 days for personal reasons, but I will definitely look at it then :).

@ekpyron
Copy link
Member

ekpyron commented Jun 12, 2018

@meowingtwurtle No worries :-)!

@federicobond
Copy link
Contributor

federicobond commented Jun 12, 2018

What would be the syntax if we add visibility eventually? Something like this?

using visibility public {
    using modifier [mod], [mod], ... {
        <functions, sub-modifier areas>
    }
}

Or is it going to be possible to put visibility besides other modifiers? In that case, are we ok with prefixing the whole list with the keyword modifier?

Edit: perhaps I should have brought this up in #623 or #2608 (comment)

@chriseth
Copy link
Contributor

I would actually prefer to have visibility (and payability) specifiers and modifiers treated in the same way, although I would not use a comma as separator (we also do not have a comma in the function syntax).

@randomnetcat
Copy link
Contributor Author

Okay, I see two ways to do this, and I would love ideas:

  1. Have no ModifierArea ASTNode, just add modifiers/visibility in parser. I'm not sure how I feel about this, really, since it kind of hides the fact that they exist at all.
  2. Have a ModifierArea ASTNode. However, this introduces the problems already seen in my code (cyclic dependencies). One way to fix this would just to not give FunctionDefinitions a shared_ptr to their ModifierArea, but I don't really like this idea, since it means that FunctionDefinitions would no longer have all of their context necessary for generation.

@ekpyron
Copy link
Member

ekpyron commented Jun 16, 2018

@meowingtwurtle You should definitely have AST nodes for the modifier areas.

The main problem with the current code is that it leaks memory. Since the modifier area AST node contains shared pointers to the function definitions and the function definitions contain shared pointers to the modifier area, they can never be freed, since the reference counters of the shared pointers will never fall to zero. Do you see the problem?

The easiest solution for that would be to use a plain, non-reference-counted pointer to the modifier area in the function definitions. That way you still have access to the modifier area from the function definitions, but you still break the reference cycle. You can compare with e.g. Scopable in the AST. It contains a plain pointer to the scope instead of an ASTPointer (i.e. shared_ptr) for the very same reason. In general you want to use shared pointers from parent to child, but never from child to parent in the AST. That way the life-time of objects is guaranteed, but you still don't introduce cycles (i.e. reference cycles of shared pointers).

An alternative would be to not have a pointer to the modifier area in the function definitions themselves, but instead to add a (plain) pointer to FunctionDefinitionAnnotation and fill them in while traversing the AST later on. The solution above is probably simpler and perfectly fine (probably even better) in this case, though.

@randomnetcat
Copy link
Contributor Author

Okay, thank you!

@randomnetcat
Copy link
Contributor Author

randomnetcat commented Jun 16, 2018

Okay, taking @ekpyron 's advice about pointers and @chriseth 's comment about visibility, mutability, and comma separation. (haven't finished yet)

Right now, the only way to differentiate between modifier area and using directive for parsing is whether or not the token after using is an identifier. This means that we have to have modifier or some other keyword/token after using. Or, we could use a different reserved keyword, like apply, here to signal it.

@randomnetcat randomnetcat force-pushed the modifierAreas branch 5 times, most recently from cec8e15 to dccdbce Compare June 18, 2018 17:10
@randomnetcat
Copy link
Contributor Author

apply has now been selected to replace using modifier

@@ -182,6 +182,7 @@ namespace solidity
K(Var, "var", 0) \
K(View, "view", 0) \
K(While, "while", 0) \
K(Apply, "apply", 0) \
Copy link
Member

Choose a reason for hiding this comment

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

This section is alphabetically ordered.

else if (_modifierArea && _modifierArea->visibility() != Declaration::Visibility::Default)
{
parserError("Cannot override modifier area's visibility.");
m_scanner->next();
Copy link
Member

Choose a reason for hiding this comment

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

Can these checks be moved to SyntaxtChecker?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes

@@ -723,6 +721,42 @@ class VariableDeclaration: public Declaration
Location m_location; ///< Location of the variable if it is of reference type.
};

class ModifierArea: public ASTNode
{
public:
Copy link
Member

Choose a reason for hiding this comment

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

I think indentation here is off.

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 just copied it from surrounding classes

@@ -646,6 +643,7 @@ class FunctionDefinition: public CallableDeclaration, public Documented, public
bool m_isConstructor;
std::vector<ASTPointer<ModifierInvocation>> m_functionModifiers;
ASTPointer<Block> m_body;
ModifierArea* m_modifierArea;
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps use ASTPointer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was something @ekpyron mentioned. We can''t use a smart pointer here because then there is a dependency loop and neither ModifierArea nor FunctionDefinition would be destructed.

@@ -620,6 +614,9 @@ class FunctionDefinition: public CallableDeclaration, public Documented, public
std::vector<ASTPointer<ModifierInvocation>> const& modifiers() const { return m_functionModifiers; }
std::vector<ASTPointer<VariableDeclaration>> const& returnParameters() const { return m_returnParameters->parameters(); }
Block const& body() const { solAssert(m_body, ""); return *m_body; }
ModifierArea* modifierArea() { return m_modifierArea; }

std::string fullyQualifiedName() const;
Copy link
Member

Choose a reason for hiding this comment

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

I think this isn't needed anymore?

Copy link
Contributor Author

@randomnetcat randomnetcat Aug 8, 2018

Choose a reason for hiding this comment

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

Done

Remove contract name parameters
Fix function visibility/state mutability
@codecov
Copy link

codecov bot commented Aug 8, 2018

Codecov Report

Merging #3939 into develop will decrease coverage by 0.02%.
The diff coverage is 92%.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #3939      +/-   ##
===========================================
- Coverage    87.56%   87.54%   -0.03%     
===========================================
  Files          313      313              
  Lines        30813    30954     +141     
  Branches      3662     3677      +15     
===========================================
+ Hits         26981    27098     +117     
- Misses        2579     2589      +10     
- Partials      1253     1267      +14
Flag Coverage Δ
#all 87.54% <92%> (-0.03%) ⬇️
#syntax 28.45% <68.66%> (+0.14%) ⬆️

@ekpyron ekpyron dismissed their stale review August 10, 2018 12:00

Dismissing outdated review.

@chriseth
Copy link
Contributor

chriseth commented Nov 4, 2019

Closing since this is unfortunately too much outdate for now :(

@chriseth chriseth closed this Nov 4, 2019
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.

Modifier Areas
5 participants