vendredi 10 mars 2017

Referencing AST nodes after construction with Visitor pattern

I have a memory management question. For one of my projects I am building an interpreter for a small programming language. One of the first steps is to model and build an Abstract Syntax Tree.

As of now, I'm using smart pointers to manage the lifetime of nodes, and I figured that every parent node is the owner of it's children, but it also must be shared with the environment (for example, to know which part of the tree a method's body belongs to) and with the garbage collector, which must keep a list of all references to enact the naïve mark and sweep algorithm. Therefore, I am using std::shared_ptr to keep track of the references. For instance, here's an example of a Block node, which basically represents a lambda expression:

#ifndef NAYLANG_BLOCK_H
#define NAYLANG_BLOCK_H

#include <model/ast/expressions/Expression.h>
#include <model/ast/declarations/Declaration.h>
#include <memory>
#include <vector>

namespace naylang {

#define BlockPtr std::shared_ptr<Block>

class Block : public Expression {

    std::vector<std::shared_ptr<Statement>> _body;
    std::vector<std::shared_ptr<Declaration>> _params;

public:

    Block() = default;

    void accept(Evaluator &evaluator) override;

    const std::vector<std::shared_ptr<Statement>> &body() const;
    const std::vector<std::shared_ptr<Declaration>> &params() const;

    void addStatement(std::shared_ptr<Statement> statement);
    void addParameter(std::shared_ptr<Declaration> param);
};

} // end namespace naylang

#endif //NAYLANG_BLOCK_H

As you can see, this node is the owner of all it's parameters and body expressions, and has accessors so that the Evaluator can traverse the tree.

Now, the problem comes when trying to have nodes that are bound at evaluation time to other nodes, for example:

#ifndef NAYLANG_REQUEST_H
#define NAYLANG_REQUEST_H

#include <model/ast/expressions/Expression.h>
#include <string>
#include <memory>
#include <vector>
#include <model/ast/declarations/MethodDeclaration.h>

namespace naylang {

class Request : public Expression {

    std::string _name;
    std::vector<ExpressionPtr> _params;

    // We use naked pointers because we don't want to worry
    // about memory management, and there is no ownership
    // with the declaration.
    const MethodDeclaration *_binding;

public:

    Request(const std::string &methodName);
    Request(const std::string &methodName, const std::vector<ExpressionPtr> params);

    void accept(Evaluator &evaluator) override;

    void bindTo(const MethodDeclaration *_binding);

    const std::string &method() const;
    const std::vector<ExpressionPtr> &params() const;
    const MethodDeclaration &declaration() const;
};

} // end namespace naylang

#endif //NAYLANG_REQUEST_H

As you can see, bindTo() is called when a BindingEvaluator (subclass of Evaluator) evaluates a Request object, long after it's constructed. However, I am really not sure about what the _binding parameter should look like. Here's a part of the Evaluator interface:

#ifndef NAYLANG_EVALUATOR_H
#define NAYLANG_EVALUATOR_H

#include <model/ast/Statement.h>

namespace naylang {

class Request;
class Block;

class Evaluator {

public:

    Evaluator() = default;
    virtual ~Evaluator() = default;

    // Methods left blank to be overridden by the subclasses.
    // For example, a Binding Evaluator might be only interested in
    // evaluating VariableReference and Request Statements

    virtual void evaluate(Request &expression) {}
    virtual void evaluate(Block &expression) {}
};    
}
#endif //NAYLANG_EVALUATOR_H

Here's my rationale:

  • The reference should be polymorphic, and therefore should be some kind of pointer.
  • The reference does not denote ownership, and therefore it should not be a std::shared_ptr.
  • In addition, we need the Visitor pattern, so every node has the function void accept(Evaluator &evaluator);. As a node cannot return a shared_ptrof itself, we cannot change the interface to something like virtual void evaluate(std::shared_ptr<Request> &expression) {}.

Thus, naked pointers. I really want to get this right before moving on, because it's a ton of code to change every time I rethink it (ASTs are verbose...)

Thank you in advance.

Aucun commentaire:

Enregistrer un commentaire