A method of customizing collection attribute tree traversal

Issue #256 resolved
Jesper Öqvist created an issue

JastAdd 2.2.0

JastAdd's current tree traversal algorithm for collection attributes skips NTAs. This avoids traversing infinitely expanding NTA trees, however in some cases it is useful to collect contributions from a bounded NTA subtree.

JastAdd does not currently have direct support for collecting contributions from an NTA subtree. The current options available are:

  • Redesign the collection attribute. This may not always be feasible.
  • Override internal JastAdd-generated collection attribute traversal method(s).

An example of a simple custom traversal method:

  // Custom survey method for coll HashSet<BodyDecl> TypeDecl.accessors() root CompilationUnit;
  protected void ClassDecl.collect_contributors_TypeDecl_accessors(CompilationUnit _root,
      java.util.Map<ASTNode, java.util.Set<ASTNode>> _map) {
    if (hasImplicitConstructor()) {
      getImplicitConstructor().collect_contributors_TypeDecl_accessors(_root, _map);
    }
    super.collect_contributors_TypeDecl_accessors(_root, _map);
  }

The above traversal method traverses the /[ImplicitConstructor]/ child of a ClassDecl during the survey phase of the accessors collection attribute.

The current problems with using custom traversal methods are:

  • The traversal method both checks for contributions, and traverses the AST. If there is already a contribution on a node type then it is not possible to define a custom traversal method on that node type.
  • JastAdd does not guarantee backward compatibility for the internal generated collection attribute code, so updating to a new version may break the custom collection traversal.
  • The code for custom traversal methods is very ugly, and difficult to read.

Proposal for a new custom traversal syntax

To fix the need for controlling the collection attribute tree traversal algorithm, I propose the following new aspect syntax (the syntax sample here mirrors the example above):

// Custom traversal for ClassDecl
// when collecting contributions for TypeDecl.accessors().
ClassDecl contributes {
      if (hasImplicitConstructor()) {
        getImplicitConstructor().collectContributions();
      }
      super.collectContributions();
    }
    to TypeDecl.accessors();

The above declaration would be used during code generation, and inserted into the generated ClassDecl.collect_contributors_TypeDecl_accessors(...) method. All calls to the collectContributions() method are replaced (using regex, as with refine) by collect_contributors_TypeDecl_accessors(_root, _map). The call to super.collectContributions() ensures that contributions from supertypes and all regular AST children are added. Note that omitting super.collectContributions() is a way of excluding a particular subtree from further collection traversal - this could be useful for expert users to optimize their collection attributes.

The proposed syntax above is intentionally free-form. The idea is that this feature will mostly be used by advanced users who don't need or want a simpler but perhaps more limited syntax for controlling the tree traversal.

The advantages I see of using this proposed syntax are:

  • Allows collecting contributions from specific NTA nodes.
  • Allows changing collection attribute traversal even for nodes where a contribution already exists.
  • The syntax would be a supported feature, so there is no risk of a small JastAdd update breaking it.
  • It allows excluding particular subtrees from collection traversal, even if they would normally be included.

The disadvantages of the proposed syntax are:

  • Perhaps confusingly similar to the ordinary contribution syntax.
  • Omitting super.collectContributions() unintentionally is a risk.
  • No way of automatically including all NTAs in a collection attribute.
  • Using regex to replace collectContributions(). Similar problem as with how the refine feature currently works.

Comments (12)

  1. Niklas Fors

    I think it would be nice to skip the call to the method defined in super. Instead this call would be implicit.

    Also, maybe it would be a good idea to add the keyword nta to make the syntax more different from the ordinary contribution syntax?

    Today I write something like this (which is too much code):

    protected void DiagramType.collect_contributors_Component_pred(DiagramType _root, 
            java.util.Map<ASTNode, java.util.Set<ASTNode>> _map) {
        super.collect_contributors_Component_pred(_root, _map);
        getConnections().collect_contributors_Component_pred(_root, _map);
    }
    

    This could be rewritten to using the new feature (omitting the super call and adding the keyword nta):

    DiagramType contributes nta {
        getConnections().collectContributions();
    } to Component.pred();
    

    It would be nice to write the above code in a simpler way, for example:

    DiagramType contributes nta getConnections() to Component.pred();
    
  2. Jesper Öqvist reporter

    It would be nice to write the above code in a simpler way, for example:

    DiagramType contributes nta getConnections() to Component.pred();
    

    It was something like this I had in mind when thinking about alternatives, but this form also loses some flexibility so I prefer the statement block style. The call to super.collectContributors() would be useful in ExtendJ for excluding normal tree traversal in a few nodes.

    For example, in ExtendJ there is a form of the LambdaExpr node that can have a regular Block as the lambda body. The type LambdaExpr also has an NTA toClass() which evaluates to an anonymous class with a copy of the block. During code generation the collection attribute nestedTypes() is evaluated to find nested types. The nestedTypes() attribute should find the nested types of the toClass() NTA, but not those nested inside the regular block:

    LambdaExpr contributes {
          toClass().collectContributors(); // Skips regular children, only traverses toClass().
        }
        to TypeDecl.nestedTypes();
    
  3. Jesper Mattsson

    Wouldn't it be possible to allow both? It seems to me that you could add a limited but simpler form that is essentially syntactic sugar for a special case of the more flexible but longer form. That would allow using the shorter syntax for most cases.

  4. Jesper Öqvist reporter

    Wouldn't it be possible to allow both? It seems to me that you could add a limited but simpler form that is essentially syntactic sugar for a special case of the more flexible but longer form. That would allow using the shorter syntax for most cases.

    I like this idea, it would just take a bit more work to implement and document the two syntaxes.

  5. Niklas Fors

    Supporting both the simpler form and the complex form would be a nice solution. Then the simpler form would implicitly call the collector method in the super type. For example, the following code

    T1 contributes nta getChild() to T2.attr();
    

    would then be equivalent to

    T1 contributes nta {
        getChild().collectContributors();
        super.collectContributions();
    } to T2.attr();
    

    Also, using the simple form, it would be possible to have several nta contributions defined modularly which are combined. Thus,

    aspect A { 
        T1 contributes nta getChild1() to T2.attr(); 
    }
    aspect B { 
        T1 contributes nta getChild2() to T2.attr(); 
    }
    

    =>

    T1 contributes nta {
        getChild1().collectContributors();
        getChild2().collectContributors();
        super.collectContributions();
    } to T2.attr();
    
  6. Jesper Öqvist reporter

    I have implemented the original suggestion. I don't plan on implementing the simpler form, and I will close this issue soon. I suggest that the simpler form should be a separate ticket on the issue tracker.

    Also, using the simple form, it would be possible to have several nta contributions defined modularly which are combined.

    My current implementation does allow modular additions of extra custom survey code:

    aspect A { 
      T1 contributes {
        getChild1().collectContributions();
        super.collectContributions();
      } to T2.attr();
    }
    aspect B { 
      T1 contributes {
        getChild2().collectContributions();
      } to T2.attr();
    }
    

    Only one of the above custom survey blocks above needs to use super.collectContributions(). The code of each block is appended to the tree traversing survey method for attribute T2.attr() in node type T1.

  7. Jesper Öqvist reporter

    Add custom collection tree traversal feature

    Added a new collection contribution construct that can be used to control the collection survey phase. The reference manual has been updated to document this new feature.

    fixes #256 (bitbucket)

    → <<cset 63de7884badc>>

  8. Jesper Öqvist reporter

    Will this be released soon?

    I'd like to make a JastAdd release soon because I want to use this in ExtendJ, and a few other fixes in JastAdd are blocking fixes for issues in ExtendJ.

  9. Jesper Öqvist reporter

    I implemented the short form of this feature also, so it is now possible to write:

    aspect Test {
      coll LinkedList<B> R.bList();
    
      B contributes this to R.bList();
    }
    
    aspect B1 {
      // A single NTA child can be searched for contributions:
      A contributes nta implicitB1() to R.bList();
    
      syn nta B A.implicitB1() = new B();
    }
    
    aspect B2 {
      A contributes nta implicitB2() to R.bList();
    
      syn nta B A.implicitB2() = new B();
    }
    
  10. Jesper Öqvist reporter

    I have done the release process! @fors when I tried to push the release commit I noticed that you had fixed the same line in the documentation which I also fixed in the release commit, so I did a force-push to avoid a merge and redo the release process!

  11. Log in to comment