1245 lines
44 KiB
C++
1245 lines
44 KiB
C++
// Copyright (c) 2020-now by the Zeek Project. See LICENSE for details.
|
||
|
||
#pragma once
|
||
|
||
#include <algorithm>
|
||
#include <cinttypes>
|
||
#include <iterator>
|
||
#include <map>
|
||
#include <memory>
|
||
#include <optional>
|
||
#include <ostream>
|
||
#include <string>
|
||
#include <unordered_set>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include <hilti/ast/doc-string.h>
|
||
#include <hilti/ast/forward.h>
|
||
#include <hilti/ast/id.h>
|
||
#include <hilti/ast/meta.h>
|
||
#include <hilti/ast/node-range.h>
|
||
#include <hilti/ast/node-tag.h>
|
||
#include <hilti/ast/scope.h>
|
||
#include <hilti/ast/visitor-dispatcher.h>
|
||
|
||
#define __HILTI_NODE_COMMON_final(NS, CLASS) \
|
||
::hilti::Node* _clone(::hilti::ASTContext* ctx) const final { return ctx->make<CLASS>(*this); }
|
||
|
||
#define __HILTI_NODE_COMMON_override(NS, CLASS)
|
||
|
||
#define __HILTI_NODE_COMMON(NS, CLASS, override_) \
|
||
friend class ::NS::builder::NodeBuilder; \
|
||
friend class hilti::ASTContext; \
|
||
friend class hilti::Node; \
|
||
std::string _typename() const override { return hilti::util::typename_(*this); } \
|
||
__HILTI_NODE_COMMON_##override_(NS, CLASS)
|
||
|
||
#define __HILTI_NODE_0(NS, CLASS, override_) \
|
||
__HILTI_NODE_COMMON(NS, CLASS, override_) \
|
||
\
|
||
static constexpr uint16_t NodeLevel = 1; \
|
||
static constexpr ::hilti::node::Tag NodeTag = ::hilti::node::tag::CLASS; \
|
||
static constexpr ::hilti::node::Tags NodeTags = {::hilti::node::tag::Node, ::hilti::node::tag::CLASS};
|
||
|
||
#define HILTI_NODE_0(CLASS, override_) \
|
||
__HILTI_NODE_0(hilti, CLASS, override_) \
|
||
\
|
||
void dispatch(::hilti::visitor::Dispatcher& v) override_ { \
|
||
v(static_cast<::hilti::Node*>(this)); \
|
||
v(this); \
|
||
}
|
||
|
||
#define __HILTI_NODE_1(NS, CLASS, BASE, override_) \
|
||
__HILTI_NODE_COMMON(NS, CLASS, override_) \
|
||
\
|
||
static constexpr uint16_t NodeLevel = 2; \
|
||
static constexpr ::hilti::node::Tag NodeTag = ::hilti::node::tag::CLASS; \
|
||
static constexpr ::hilti::node::Tags NodeTags = {::hilti::node::tag::Node, ::hilti::node::tag::BASE, \
|
||
::hilti::node::tag::CLASS};
|
||
|
||
#define HILTI_NODE_1(CLASS, BASE, override_) \
|
||
__HILTI_NODE_1(hilti, CLASS, BASE, override_) \
|
||
\
|
||
void dispatch(::hilti::visitor::Dispatcher& v) override_ { \
|
||
v(static_cast<::hilti::Node*>(this)); \
|
||
v(static_cast<BASE*>(this)); \
|
||
v(this); \
|
||
}
|
||
|
||
#define __HILTI_NODE_2(NS, CLASS, BASE1, BASE2, override_) \
|
||
__HILTI_NODE_COMMON(NS, CLASS, override_) \
|
||
\
|
||
static constexpr uint16_t NodeLevel = 3; \
|
||
static constexpr ::hilti::node::Tag NodeTag = ::hilti::node::tag::CLASS; \
|
||
static constexpr ::hilti::node::Tags NodeTags = {::hilti::node::tag::Node, ::hilti::node::tag::BASE2, \
|
||
::hilti::node::tag::BASE1, ::hilti::node::tag::CLASS};
|
||
|
||
#define HILTI_NODE_2(CLASS, BASE1, BASE2, override_) \
|
||
__HILTI_NODE_2(hilti, CLASS, BASE1, BASE2, override_) \
|
||
\
|
||
void dispatch(::hilti::visitor::Dispatcher& v) override_ { \
|
||
v(static_cast<::hilti::Node*>(this)); \
|
||
v(static_cast<BASE1*>(this)); \
|
||
v(static_cast<BASE2*>(this)); \
|
||
v(this); \
|
||
}
|
||
|
||
namespace hilti {
|
||
namespace builder {
|
||
class NodeBuilder;
|
||
}
|
||
|
||
namespace node {
|
||
|
||
namespace detail {
|
||
/** Backend for `node::deepcopy()`, see there. */
|
||
Node* deepcopy(ASTContext* ctx, Node* n, bool force);
|
||
} // namespace detail
|
||
|
||
/**
|
||
* Deep-copies a node by creating new instances of a node itself and,
|
||
* recursively, any of its children.
|
||
*
|
||
* If `force` is true, this always happens. If `force` it false, the copy takes
|
||
* place only if the node does not currently have a parent node, meaning it's
|
||
* not part of an AST. The latter behavior is usually what one wants because
|
||
* it performs the copy only if then adding the node to an AST.
|
||
*/
|
||
template<typename T>
|
||
T* deepcopy(ASTContext* ctx, T* n, bool force = false) {
|
||
if ( ! n )
|
||
return nullptr;
|
||
|
||
return detail::deepcopy(ctx, n, force)->template as<T>();
|
||
}
|
||
|
||
/** Value of a node property, stored as part of `Properties`. */
|
||
using PropertyValue = std::variant<bool, const char*, double, int, int64_t, unsigned int, uint64_t, std::string, ID,
|
||
std::optional<uint64_t>>;
|
||
|
||
/** Renders a property value into a string for display. */
|
||
inline std::string to_string(const PropertyValue& v) {
|
||
struct Visitor {
|
||
auto operator()(bool s) { return std::string(s ? "true" : "false"); }
|
||
auto operator()(const char* s) { return util::escapeUTF8(s); }
|
||
auto operator()(double d) { return util::fmt("%.6f", d); }
|
||
auto operator()(int i) { return util::fmt("%d", i); }
|
||
auto operator()(int64_t i) { return util::fmt("%" PRId64, i); }
|
||
auto operator()(const std::string& s) { return util::escapeUTF8(s); }
|
||
auto operator()(const ID& id) { return id.str(); }
|
||
auto operator()(const std::optional<uint64_t>& u) { return u ? util::fmt("%" PRIu64, *u) : "<not set>"; }
|
||
auto operator()(unsigned int u) { return util::fmt("%u", u); }
|
||
auto operator()(uint64_t u) { return util::fmt("%" PRIu64, u); }
|
||
};
|
||
|
||
return std::visit(Visitor(), v);
|
||
};
|
||
|
||
/** Importance of reporting an error, relative to others. */
|
||
enum class ErrorPriority {
|
||
High = 3, /**< high priority error that will always be reported */
|
||
Normal = 2, /**< normal priority error that will be reported if there are no higher priority ones */
|
||
Low = 1, /**< low priority error that will be reported if there are no higher priority ones */
|
||
NoError = 0 /**< place-holder for comparison if no error was encountered */
|
||
};
|
||
|
||
inline bool operator<(ErrorPriority x, ErrorPriority y) {
|
||
return static_cast<std::underlying_type_t<ErrorPriority>>(x) <
|
||
static_cast<std::underlying_type_t<ErrorPriority>>(y);
|
||
}
|
||
|
||
/** Error information associated with nodes. */
|
||
struct Error {
|
||
std::string message; /**< main error message to report */
|
||
Location location; /**< location associated with the error */
|
||
std::vector<std::string> context; /**< additional lines to print along with error as context */
|
||
ErrorPriority priority = ErrorPriority::Normal; /**< priority of error */
|
||
|
||
// Comparison considers message & location, so that we can unique based
|
||
// on those two.
|
||
bool operator<(const Error& other) const {
|
||
return std::tie(message, location) < std::tie(other.message, other.location);
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Properties associated with an AST node. A property is a key/value pair
|
||
* recording node-specific, atomic information that's not represented by
|
||
* further child nodes.
|
||
*/
|
||
using Properties = std::map<std::string, node::PropertyValue>;
|
||
|
||
/** Smart pointer wrapping a node, automatically pinning and unpinning it. */
|
||
template<typename T>
|
||
class RetainedPtr {
|
||
public:
|
||
RetainedPtr() = default;
|
||
RetainedPtr(T* n) : _node(n) { retain(); }
|
||
RetainedPtr(const RetainedPtr& other) : _node(other._node) { retain(); }
|
||
RetainedPtr(RetainedPtr&& other) noexcept : _node(other._node) {
|
||
other._node = nullptr;
|
||
// reuse the other's retain
|
||
}
|
||
|
||
~RetainedPtr() { release(); }
|
||
|
||
RetainedPtr& operator=(const RetainedPtr& other) {
|
||
if ( this == &other )
|
||
return *this;
|
||
|
||
release();
|
||
_node = other._node;
|
||
retain();
|
||
return *this;
|
||
}
|
||
|
||
RetainedPtr& operator=(RetainedPtr&& other) noexcept {
|
||
if ( this == &other )
|
||
return *this;
|
||
|
||
release();
|
||
_node = other._node;
|
||
other._node = nullptr;
|
||
// reuse the other's retain
|
||
return *this;
|
||
}
|
||
|
||
void reset() {
|
||
release();
|
||
_node = nullptr;
|
||
}
|
||
|
||
T* operator->() const { return _node; }
|
||
T& operator*() const { return *_node; }
|
||
explicit operator bool() const { return _node != nullptr; }
|
||
operator T*() const { return _node; }
|
||
|
||
T* get() const { return _node; }
|
||
|
||
private:
|
||
void retain() {
|
||
if ( _node )
|
||
_node->retain();
|
||
}
|
||
|
||
void release() {
|
||
if ( _node ) {
|
||
_node->release();
|
||
_node = nullptr;
|
||
}
|
||
}
|
||
|
||
T* _node = nullptr;
|
||
};
|
||
|
||
} // namespace node
|
||
|
||
/** Base class for all AST nodes. */
|
||
class Node {
|
||
public:
|
||
virtual ~Node();
|
||
|
||
/** Returns the node tag associated with the instance's class. */
|
||
node::Tag nodeTag() const {
|
||
// Get the last non-zero tag. The last element(s) may be unset
|
||
for ( auto it = _node_tags.rbegin(); it != _node_tags.rend(); it++ ) {
|
||
if ( *it != 0 )
|
||
return *it;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/** Returns true if the node has a parent (i.e., it's part of an AST). */
|
||
bool hasParent() const { return _parent; }
|
||
|
||
/**
|
||
* Returns a parent node, assuming the node is part of an AST.
|
||
*
|
||
* @param i level of the parent to return, counting from 1 for the immediate parent
|
||
* @return parent node, or null if the requested parent does not exist
|
||
*/
|
||
Node* parent(int i = 1) const {
|
||
if ( i == 0 )
|
||
return nullptr;
|
||
|
||
Node* p = _parent;
|
||
for ( ; p && i > 1; i-- )
|
||
p = p->_parent;
|
||
|
||
return p;
|
||
}
|
||
|
||
/**
|
||
* Returns the first parent node of a give type.
|
||
*
|
||
* @tparam T type of parent to search
|
||
* @return parent node, or null if the requested parent does not exist
|
||
*/
|
||
template<typename T>
|
||
T* parent() const {
|
||
if ( ! _parent )
|
||
return nullptr;
|
||
else if ( _parent->isA_<T>() )
|
||
return static_cast<T*>(_parent);
|
||
else
|
||
return _parent->parent<T>();
|
||
}
|
||
|
||
/**
|
||
* Returns the length of the AST path to the current node from the AST's
|
||
* root. If the node is part of an AST, returns a number >= 1. If it is not
|
||
* (i.e., there's no parent node), returns 0.
|
||
*/
|
||
auto pathLength() const {
|
||
size_t i = 0;
|
||
for ( auto n = parent(); n; i++, n = n->parent() )
|
||
;
|
||
|
||
return i;
|
||
}
|
||
|
||
|
||
/** Returns the meta data associated with the node. */
|
||
const auto& meta() const { return *_meta; }
|
||
|
||
/** Short-cut to return the location from the node's meta information. */
|
||
const auto& location() const { return _meta->location(); }
|
||
|
||
/** Sets the meta data associated with the node. */
|
||
void setMeta(Meta m) { _meta = Meta::get(std::move(m)); }
|
||
|
||
/**
|
||
* Returns the scope associated with the node, if any. Returns null if no
|
||
* scope has been created for the node yet.
|
||
*/
|
||
auto scope() const { return _scope.get(); }
|
||
|
||
/**
|
||
* Returns the node's direct scope if already created, or creates one if it
|
||
* hasn't yet. In the latter case, the new scope is permanently associated
|
||
* with the node before being returned.
|
||
*/
|
||
auto getOrCreateScope() {
|
||
if ( ! _scope )
|
||
_scope = std::make_unique<Scope>();
|
||
|
||
return _scope.get();
|
||
}
|
||
|
||
/**
|
||
* Removes any associated scope from the node. Afterwards, `scope()` will
|
||
* return null again.
|
||
*/
|
||
void clearScope() { _scope.reset(); }
|
||
|
||
/**
|
||
* Looks up an ID in the node's chain of scope, following HILTI's scoping and visibility rules.
|
||
*
|
||
* @param id id to look up
|
||
* @param what description of what we're looking for, for error reporting
|
||
* @return if found, a pair of the declaration the ID refers to plus the declarations canonical ID; if not found an
|
||
* error message appropriate for reporting to the user
|
||
*/
|
||
Result<std::pair<Declaration*, ID>> lookupID(const ID& id, const std::string_view& what) const;
|
||
|
||
/**
|
||
* Returns a flag indicating whether a scope lookup passing this node
|
||
* shall find IDs in parent nodes as well. This returns true by default.
|
||
*/
|
||
virtual bool inheritScope() const { return true; }
|
||
|
||
/**
|
||
* Returns the C++-level type for the nodes' class. This should be only
|
||
* used for for debugging purposes.
|
||
**/
|
||
std::string typename_() const { return _typename(); }
|
||
|
||
/** Returns a globally unique numeric identifier for the node. */
|
||
uintptr_t identity() const { return reinterpret_cast<uintptr_t>(this); }
|
||
|
||
/** Returns the set of all children. */
|
||
const auto& children() const { return _children; }
|
||
|
||
/**
|
||
* Returns a child.
|
||
*
|
||
* @tparam T type that the child nodes are assumed to (and must) have
|
||
* @param i zero-based index of the child, in the order they were passed into the constructor and/or added
|
||
* @return child casted to type `T`, or null if there's no child node at that index
|
||
*/
|
||
template<typename T>
|
||
T* child(unsigned int i) const {
|
||
if ( i >= _children.size() )
|
||
return nullptr;
|
||
|
||
return _children[i] ? _children[i]->as<T>() : nullptr;
|
||
}
|
||
|
||
/**
|
||
* Returns a child.
|
||
*
|
||
* @tparam T type that the child nodes are assumed to (and must) have
|
||
* @param i zero-based index of the child, in the order they were passed into the constructor and/or added
|
||
* @return child casted to type `T`, or null if there's no child node at that index
|
||
*/
|
||
template<typename T>
|
||
T* childTryAs(unsigned int i) const {
|
||
if ( i >= _children.size() )
|
||
return nullptr;
|
||
|
||
return _children[i] ? _children[i]->tryAs<T>() : nullptr;
|
||
}
|
||
|
||
/**
|
||
* Returns a child at given index inside the vector of all children. The order in that vector is determined by
|
||
* the order in which the children were passed into the constructor and/or added.
|
||
*
|
||
* @param i index of the child, with zero being the first
|
||
* @return child at given index, or null if there's no child at that index
|
||
**/
|
||
Node* child(unsigned int i) const {
|
||
if ( i >= _children.size() )
|
||
return nullptr;
|
||
|
||
return _children[i];
|
||
}
|
||
|
||
/**
|
||
* Returns a subrange of children. The indices correspond to the order
|
||
* children were passed into the constructor and/or added.
|
||
*
|
||
* @tparam T type that the child nodes are assumed to (and must) have
|
||
* @param begin index of first child to include
|
||
* @param end index of one beyond last child to include; a negative index for *end* counts Python-style from end of
|
||
* list; if not given, all children from `start` to the end of the list are returned
|
||
* @return range containing children from `start` to `end`
|
||
*/
|
||
template<typename T>
|
||
auto children(int begin, std::optional<int> end) const {
|
||
end = _normalizeEndIndex(begin, end);
|
||
if ( end )
|
||
return hilti::node::Range<T>(_children.begin() + begin, _children.begin() + *end);
|
||
else
|
||
return hilti::node::Range<T>();
|
||
}
|
||
|
||
/**
|
||
* Returns a subrange of children. The indices correspond to the order
|
||
* children were passed into the constructor and/or added.
|
||
*
|
||
* @tparam T type that the child nodes are assumed to (and must) have
|
||
* @param begin index of first child to include
|
||
* @param end index of one beyond last child to include; a negative index for *end* counts Python-style from end of
|
||
* list; if not given, all children from `start` to the end of the list are returned
|
||
* @return range containing children from `start` to `end`
|
||
*/
|
||
template<typename T>
|
||
auto children(int begin, std::optional<int> end) {
|
||
end = _normalizeEndIndex(begin, end);
|
||
if ( end )
|
||
return hilti::node::Range<T>(_children.begin() + begin, _children.begin() + *end);
|
||
else
|
||
return hilti::node::Range<T>();
|
||
}
|
||
|
||
/**
|
||
* Returns a subset of children selected by their type.
|
||
*
|
||
* @tparam T type of children to return
|
||
* @return set of all children that have type `T`
|
||
*/
|
||
template<typename T>
|
||
node::Set<T> childrenOfType() const {
|
||
typename hilti::node::Set<T> n;
|
||
for ( auto c = _children.begin(); c != _children.end(); c = std::next(c) ) {
|
||
if ( ! *c )
|
||
continue;
|
||
|
||
if ( auto t = (*c)->tryAs<T>() )
|
||
n.push_back(t);
|
||
}
|
||
|
||
return n;
|
||
}
|
||
|
||
/**
|
||
* Returns the subsequent sibling of given child node. This skips over null
|
||
* children.
|
||
*
|
||
* @param n child whose sibling to return
|
||
* @return sibling of *n*, or null if *n* is the last child or not child at all
|
||
**/
|
||
Node* sibling(Node* n) const {
|
||
auto i = std::find(_children.begin(), _children.end(), n);
|
||
if ( i == _children.end() )
|
||
return nullptr;
|
||
|
||
while ( true ) {
|
||
if ( ++i == _children.end() )
|
||
return nullptr;
|
||
|
||
if ( *i )
|
||
return *i;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Adds a child node. The node will be appended to the end of the current
|
||
* list of children, and its parent will be set to the current node. If the
|
||
* node already has a parent, it will be deep-copied first, and the new
|
||
* instance will be added instead of the once passed in. ×
|
||
*
|
||
* @param ctx current context in use
|
||
* @param n child node to add; it's ok for this to be null to leave a child slot unset
|
||
*/
|
||
void addChild(ASTContext* ctx, Node* n) {
|
||
if ( ! n ) {
|
||
_children.emplace_back(nullptr);
|
||
return;
|
||
}
|
||
|
||
n = _newChild(ctx, n);
|
||
|
||
if ( ! n->location() && _meta->location() )
|
||
n->_meta = _meta;
|
||
|
||
_children.emplace_back(n);
|
||
n->_parent = this;
|
||
n->retain();
|
||
}
|
||
|
||
/**
|
||
* Adds a series of child nodes. This operates like calling `addChild()` on
|
||
* each of them individually.
|
||
*
|
||
* @param ctx current context in use
|
||
* @param children nodes to add
|
||
*/
|
||
void addChildren(ASTContext* ctx, const Nodes& children) {
|
||
for ( auto&& n : children )
|
||
addChild(ctx, n);
|
||
}
|
||
|
||
/**
|
||
* Removes a child from the node. It's parent will be set back to null.
|
||
* Does nothing if the child isn't found.
|
||
*
|
||
* @param n child node to remove
|
||
*/
|
||
void removeChild(Node* n) {
|
||
if ( ! n )
|
||
return;
|
||
|
||
if ( auto i = std::find(_children.begin(), _children.end(), n); i != _children.end() ) {
|
||
(*i)->_parent = nullptr;
|
||
(*i)->release();
|
||
_children.erase(i);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Removes a range of children from the node. They nodes won't be destroyed
|
||
* but their parents will be reset back to null.
|
||
*
|
||
* @param begin index of first child to remove
|
||
* @param end index of one beyond last child to include; a negative index for *end* counts Python-style from end of
|
||
* list; if not given, all children from `start` to the end of the list are returned
|
||
*/
|
||
void removeChildren(int begin, std::optional<int> end) {
|
||
end = _normalizeEndIndex(begin, end);
|
||
if ( ! end )
|
||
return;
|
||
|
||
auto end_ = _children.begin() + *end;
|
||
for ( auto i = _children.begin() + begin; i < end_; i++ ) {
|
||
if ( *i ) {
|
||
(*i)->_parent = nullptr;
|
||
(*i)->release();
|
||
}
|
||
}
|
||
|
||
_children.erase(_children.begin() + begin, end_);
|
||
}
|
||
|
||
/**
|
||
* Sets the child at a particular index. Its parent will be set to the
|
||
* current node. If the node already has a parent, it will be deep-copied
|
||
* first, and the new instance will be added instead of the once passed in.
|
||
* If there's an existing child at that index, it'll be removed first and
|
||
* its parent cleared.
|
||
*
|
||
* @param ctx current context in use
|
||
* @param idx index of child to set
|
||
* @param n child node to set; this may be null to unset the particular index
|
||
*/
|
||
void setChild(ASTContext* ctx, size_t idx, Node* n) {
|
||
if ( auto old = _children[idx] ) {
|
||
old->_parent = nullptr;
|
||
old->release();
|
||
}
|
||
|
||
if ( ! n ) {
|
||
_children[idx] = nullptr;
|
||
return;
|
||
}
|
||
|
||
n = _newChild(ctx, n);
|
||
n->_parent = this;
|
||
n->retain();
|
||
|
||
if ( ! n->location() && _meta->location() )
|
||
n->_meta = _meta;
|
||
|
||
_children[idx] = n;
|
||
}
|
||
|
||
/**
|
||
* Replaces *all* children with a new set children. The function operates
|
||
* like first removing all children, and then adding all the new ones in
|
||
* the same order, with the same semantics for parent pointering and
|
||
* deep-copying as adding/removing individual children exhibits.
|
||
*
|
||
* @param ctx current context in use
|
||
* @param children new children to set
|
||
*/
|
||
void replaceChildren(ASTContext* ctx, const Nodes& children);
|
||
|
||
/**
|
||
* Replaces a single child with a new one. The old one is removed, and the
|
||
* new one is then stored at the old one's index. Semantics for parent
|
||
* pointering and deep-copying are the same as removing/adding individual
|
||
* children.
|
||
*
|
||
* @param ctx current context in use
|
||
* @param old child to replace, which must exist (otherwise the method will abort with an internal error)
|
||
* @param new_ new child to replace *old* with
|
||
*/
|
||
void replaceChild(ASTContext* ctx, Node* old, Node* new_);
|
||
|
||
/** Returns true if a node is of a particular type (class). */
|
||
template<typename T>
|
||
bool isA() const {
|
||
#ifndef NDEBUG
|
||
_checkCast<T>(false);
|
||
#endif
|
||
return (T::NodeLevel < _node_tags.size() && T::NodeTag == _node_tags[T::NodeLevel]);
|
||
}
|
||
|
||
/**
|
||
* Alternate version to check if a node is of a particular type (class).
|
||
* This version skips any potential internal consistency checks, which can
|
||
* be helpful in case of false positives. You should normally avoid using
|
||
* this unless absolutely necessary.
|
||
*/
|
||
template<typename T>
|
||
bool isA_() const {
|
||
return (T::NodeLevel < _node_tags.size() && T::NodeTag == _node_tags[T::NodeLevel]);
|
||
}
|
||
|
||
/**
|
||
* Casts a node into a particular class. The cast must be a valid C++
|
||
* dynamic pointer cast, otherwise execution will abort with an internal error.
|
||
*/
|
||
template<typename T>
|
||
T* as() const {
|
||
#ifndef NDEBUG
|
||
_checkCast<T>(true);
|
||
#endif
|
||
return static_cast<const T*>(this);
|
||
}
|
||
|
||
/**
|
||
* Casts a node into a particular class. The cast from the node to the
|
||
* target type must be valid. If it isn't, in release builds, the call will
|
||
* result in undefined behavior (and probably crash). In debug builds,
|
||
* we'll catch invalid cases and abort with an internal error.
|
||
*/
|
||
template<typename T>
|
||
T* as() {
|
||
#ifndef NDEBUG
|
||
_checkCast<T>(true);
|
||
#endif
|
||
return static_cast<T*>(this);
|
||
}
|
||
|
||
/**
|
||
* Attempts to casts a node into a particular class. Returns a nullptr if
|
||
* the cast failed.
|
||
*/
|
||
template<typename T>
|
||
const T* tryAs() const {
|
||
#ifndef NDEBUG
|
||
_checkCast<T>(false);
|
||
#endif
|
||
|
||
if ( isA<T>() )
|
||
return static_cast<const T*>(this);
|
||
else
|
||
return nullptr;
|
||
}
|
||
|
||
/**
|
||
* Attempts to casts a node into a particular class. Returns a nullptr if
|
||
* the cast failed.
|
||
*/
|
||
template<typename T>
|
||
T* tryAs() {
|
||
#ifndef NDEBUG
|
||
_checkCast<T>(false);
|
||
#endif
|
||
if ( isA<T>() )
|
||
return static_cast<T*>(this);
|
||
else
|
||
return nullptr;
|
||
}
|
||
|
||
/**
|
||
* Alternate version to attempt casting a node into a particular class.
|
||
* Returns a nullptr if the cast failed. This version skips any potential
|
||
* internal consistency checks, which can be helpful in case of false
|
||
* positives. You should normally avoid using this unless absolutely
|
||
* necessary.
|
||
*/
|
||
template<typename T>
|
||
T* tryAs_() {
|
||
if ( isA_<T>() )
|
||
return static_cast<T*>(this);
|
||
else
|
||
return nullptr;
|
||
}
|
||
|
||
/**
|
||
* Print out a HILTI source code representation of the node and all its
|
||
* children. If the node is not the root of an AST, it's not guaranteed
|
||
* that the result will form valid HILTI source code (but it can still be
|
||
* used, e.g., in error messages).
|
||
*
|
||
* @param out output stream
|
||
* @param compact create a one-line representation
|
||
* @param user_visible if true, signal to the printer that the output is
|
||
* intended for user consumption, permitting it to do some visual polishing
|
||
*
|
||
*/
|
||
void print(std::ostream& out, bool compact, bool user_visible) const;
|
||
|
||
/**
|
||
* Returns a HILTI source code representation of the node and all its
|
||
* children. This always renders the code as "user-visible", per the flag
|
||
* in the extended version of `print()`.
|
||
*
|
||
* Note that this can be called from inside a debugger.
|
||
*/
|
||
std::string print() const;
|
||
|
||
/**
|
||
* Returns a HILTI source code representation of the node and all
|
||
* its children. This always renders the code as *not*
|
||
* "user-visible", per the flag in the extended version of Zeek.
|
||
*
|
||
* Note that this can be called from inside a debugger.
|
||
*/
|
||
std::string printRaw() const;
|
||
|
||
/**
|
||
* Renders the node as HILTI source code, using the same semantics
|
||
* as `print()`.
|
||
*/
|
||
operator std::string() const { return print(); }
|
||
|
||
/**
|
||
* Returns an internal string representation of the node and all its
|
||
* children. Note that this can be called from inside a debugger.
|
||
*/
|
||
std::string dump() const;
|
||
|
||
/**
|
||
* Returns an internal string representation of the node itself, excluding
|
||
* its children.
|
||
*
|
||
* @param include_location if true, include source code locations into
|
||
* the output
|
||
*/
|
||
std::string renderSelf(bool include_location = true) const;
|
||
|
||
/**
|
||
* Associates an error message with the node. The error's location will be
|
||
* that of the current node, and it will have normal priority.
|
||
*
|
||
* @param msg error message to report
|
||
* @param context further lines of context to show along with error
|
||
*
|
||
*/
|
||
void addError(std::string msg, std::vector<std::string> context = {}) {
|
||
addError(std::move(msg), location(), std::move(context));
|
||
}
|
||
|
||
/**
|
||
* Associates an error message with the node. The error's location will be
|
||
* that of the current node.
|
||
*
|
||
* @param msg error message to report
|
||
* @param priority importance of showing the error
|
||
* @param context further lines of context to show along with error
|
||
*
|
||
*/
|
||
void addError(std::string msg, node::ErrorPriority priority, std::vector<std::string> context = {}) {
|
||
addError(std::move(msg), location(), priority, std::move(context));
|
||
}
|
||
|
||
/**
|
||
* Associates an error message with the node. The error will have normal
|
||
* priority.
|
||
*
|
||
* @param msg error message to report
|
||
* @param l custom location to associate with the error
|
||
* @param context further lines of context to show along with error
|
||
*/
|
||
void addError(std::string msg, const Location& l, std::vector<std::string> context = {}) {
|
||
addError(std::move(msg), location(), node::ErrorPriority::Normal, std::move(context));
|
||
}
|
||
|
||
/**
|
||
* Associates an error message with the node.
|
||
*
|
||
* @param msg error message to report
|
||
* @param l custom location to associate with the error
|
||
* @param priority importance of showing the error
|
||
* @param context further lines of context to show along with error
|
||
*/
|
||
void addError(std::string msg, Location l, node::ErrorPriority priority, std::vector<std::string> context = {}) {
|
||
node::Error error;
|
||
error.message = std::move(msg);
|
||
error.location = std::move(l);
|
||
error.context = std::move(context);
|
||
error.priority = priority;
|
||
|
||
if ( ! _errors )
|
||
_errors = std::make_unique<std::vector<node::Error>>();
|
||
|
||
_errors->emplace_back(std::move(error));
|
||
}
|
||
|
||
/** Returns true if there are any errors associated with the node. */
|
||
bool hasErrors() const { return _errors && ! _errors->empty(); }
|
||
|
||
/** Returns any error messages associated with the node. */
|
||
const auto& errors() const {
|
||
static std::vector<node::Error> no_errors;
|
||
return _errors ? *_errors : no_errors;
|
||
}
|
||
|
||
/** Clears any error message associated with the node. */
|
||
void clearErrors() { _errors.reset(); }
|
||
|
||
/**
|
||
* Removes all children from the node. It doesn't destroy the children,
|
||
* pointers remain valid, it just unlinks them from their current parent.
|
||
*/
|
||
void clearChildren();
|
||
|
||
/** Pins the node in memory, ensuring garbage collection won't delete it. */
|
||
void retain() {
|
||
assert(_ref_count != -1); // dtor sets ref count to -1
|
||
++_ref_count;
|
||
}
|
||
|
||
/**
|
||
* Unpins the node, allowing garbage collection to delete it (assuming no
|
||
* other pins).
|
||
*/
|
||
void release() {
|
||
assert(_ref_count != -1); // dtor sets ref count to -1
|
||
assert(_ref_count > 0);
|
||
--_ref_count;
|
||
}
|
||
|
||
/** Returns true if at least one party has currently retained the node. */
|
||
bool isRetained() const { return _ref_count > 0; }
|
||
|
||
/**
|
||
* Returns any instance properties associated with the node. These are used
|
||
* (only) for debug logging. Derived classes should override this to add
|
||
* any properties they retain inside internal node member variables.
|
||
*/
|
||
virtual node::Properties properties() const { return {}; }
|
||
|
||
/** Dispatch for the visitor API. */
|
||
virtual void dispatch(visitor::Dispatcher& v) = 0;
|
||
|
||
/**
|
||
* Optional tag associated with the AST subbranch that this node is the top
|
||
* of. If a tag is returned, this may be used to by visitors to skip the
|
||
* subbranch entirely based on that tag.
|
||
*
|
||
* @return tag associated with the subbranch, or an empty string if none
|
||
*/
|
||
virtual std::string_view branchTag() const { return ""; }
|
||
|
||
Node& operator=(const Node& other) = delete;
|
||
Node& operator=(Node&& other) noexcept = delete;
|
||
|
||
static constexpr uint16_t NodeLevel = 0; // distance from `Node` in the inheritance hierarchy
|
||
static constexpr ::hilti::node::Tag NodeTag = ::hilti::node::tag::Node; // this class' node tag
|
||
static constexpr ::hilti::node::Tags NodeTags = {::hilti::node::tag::Node}; // this class' inheritance path
|
||
|
||
protected:
|
||
/**
|
||
* Constructor initializing the node with children and meta data. The
|
||
* semantics for the children's parent pointering and potential
|
||
* deep-copying are the same as if they were added individually through
|
||
* `addChild()`.
|
||
*
|
||
* @param ctx current context in use
|
||
* @param children child nodes to add initially
|
||
*/
|
||
Node(ASTContext* ctx, node::Tags node_tags, Nodes children, Meta meta)
|
||
: _node_tags(node_tags), _meta(Meta::get(std::move(meta))) {
|
||
assert(! _node_tags.empty());
|
||
_children.reserve(children.size());
|
||
for ( auto&& c : children ) {
|
||
if ( c ) {
|
||
c = _newChild(ctx, c);
|
||
assert(! c->_parent);
|
||
c->_parent = this;
|
||
c->retain();
|
||
}
|
||
|
||
_children.push_back(c);
|
||
}
|
||
}
|
||
|
||
/** Constructor initializing the node with meta data but no children. */
|
||
Node(ASTContext* ctx, node::Tags node_tags, Meta meta) : _node_tags(node_tags), _meta(Meta::get(std::move(meta))) {
|
||
assert(! _node_tags.empty());
|
||
}
|
||
|
||
Node(Node&& other) = default;
|
||
|
||
/**
|
||
* Copy constructor. This copies only meta data and internal flags, but not
|
||
* any children. The parent of the copied node will be unset.
|
||
*
|
||
* Use `node::deepcopy()` to fully copy a node.
|
||
*
|
||
*/
|
||
Node(const Node& other) : _node_tags(other._node_tags) {
|
||
_meta = other._meta;
|
||
_parent = nullptr;
|
||
|
||
// Don't copy children. We can't copy the pointers because that would
|
||
// produce messed up parent pointers. And we can't deep copy here
|
||
// because we don't have the context. That's all ok because this isn't
|
||
// public anyways; to run this one needs to go through Node's cloning
|
||
// functions.
|
||
}
|
||
|
||
/**
|
||
* Returns the C++-level class name for the node's type. This is for
|
||
* internal use only. It's the virtual backend to `typename_()`, which is
|
||
* the one to call instead of this method.
|
||
*/
|
||
virtual std::string _typename() const { return util::typename_(*this); }
|
||
|
||
/**
|
||
* Performs a shallow copy. A new instance of the current node class is
|
||
* created and initialized with copies of all attributes and children with
|
||
* latter being copied just by reference. This is for internal use only,
|
||
* use node::deepcopy() to fully clone nodes.
|
||
*/
|
||
virtual Node* _clone(ASTContext* ctx) const = 0; // shallow copy
|
||
|
||
/**
|
||
* Returns additional information to include into the node's `dump()`
|
||
* output, as provided by derived classes.
|
||
*/
|
||
virtual std::string _dump() const { return ""; }
|
||
|
||
private:
|
||
friend Node* node::detail::deepcopy(ASTContext* ctx, Node* n, bool force);
|
||
|
||
// Prepares a node for being added as a child, deep-copying it if it
|
||
// already has a parent.
|
||
static Node* _newChild(ASTContext* ctx, Node* child);
|
||
|
||
// Do Python-style array indexing with negative indices.
|
||
std::optional<int> _normalizeEndIndex(int begin, std::optional<int> end) const {
|
||
if ( end && *end < 0 )
|
||
end = static_cast<int>(_children.size()) + *end;
|
||
|
||
if ( ! end )
|
||
end = _children.size();
|
||
|
||
if ( end > begin )
|
||
return end;
|
||
else {
|
||
// Some versions of GCC diagnose a maybe uninitialized variable
|
||
// here. Since we just return an unset optional, this should not be
|
||
// possible. (pragmas copied from intrusive-ptr.h, where there's a
|
||
// similar issue.)
|
||
#pragma GCC diagnostic push
|
||
#pragma GCC diagnostic ignored "-Wpragmas"
|
||
#pragma GCC diagnostic ignored "-Wunknown-warning-option"
|
||
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
|
||
return {};
|
||
#pragma GCC diagnostic pop
|
||
}
|
||
}
|
||
|
||
#ifndef NDEBUG
|
||
// Checks casts for common mistakes.
|
||
//
|
||
// @param enforce_success if true, we'll abort if the cast fails due to a type mismatch
|
||
template<typename T>
|
||
void _checkCast(bool enforce_success) const {
|
||
// Ensure that our RTTI information yields the same `isA<>` result as
|
||
// the C++ type system.
|
||
auto ours = (T::NodeLevel < _node_tags.size() && T::NodeTag == _node_tags[T::NodeLevel]);
|
||
auto theirs = (dynamic_cast<const T*>(this) != nullptr);
|
||
|
||
if ( ours != theirs ) {
|
||
std::cerr << util::fmt("internal error: Node::_checkCast() RTTI mismatch\n")
|
||
<< util::fmt("isA<T=%s>(%s) -> %s but dynamic_cast() says %s\n", typeid(T).name(), _typename(),
|
||
ours ? "true" : "false", theirs ? "true" : "false")
|
||
<< util::fmt("T::type_level=%" PRIu16 " T::node_tags={%s} this->types={%s}\n", T::NodeLevel,
|
||
node::to_string(T::NodeTags), node::to_string(_node_tags));
|
||
abort();
|
||
}
|
||
|
||
if ( enforce_success && ! ours ) {
|
||
std::cerr << util::fmt("internal error: unexpected type, want %s but have %s\n", util::typename_<T>(),
|
||
_typename());
|
||
abort();
|
||
}
|
||
|
||
// Debugging helper: If `this` is a `QualifiedType`, it's virtually
|
||
// always wrong to try casting it into a class derived from
|
||
// `UnqualifiedType`. Most likely that's meant to instead cast
|
||
// `this->type()`. We abort here to make debugging such problems
|
||
// easier.
|
||
if ( std::is_base_of_v<UnqualifiedType, T> && ! std::is_same_v<UnqualifiedType, T> )
|
||
_checkCastBackend();
|
||
}
|
||
#endif
|
||
|
||
// Helper for _checkCast() to spot unexpected casts from `QualifiedType`.
|
||
void _checkCastBackend() const;
|
||
|
||
const node::Tags _node_tags; // inheritance path for the node
|
||
int64_t _ref_count = 0; // number of pins currently held on the node; -1 is a special value set by the dtor to mark
|
||
// an already destroyed node (for debugging)
|
||
Node* _parent = nullptr; // parent node inside the AST, or null if not yet added to an AST
|
||
Nodes _children; // set of child nodes
|
||
const Meta* _meta; // meta information associated with the node; returned and managed by Meta::get()
|
||
|
||
std::unique_ptr<Scope> _scope = nullptr; // scope associated with the node, or null if non (i.e., scope is empty)
|
||
std::unique_ptr<std::vector<node::Error>> _errors; // errors associated with the node, or null if none
|
||
};
|
||
|
||
namespace node {
|
||
|
||
/**
|
||
* Mix-in class for nodes that need a globally unique ID identifying it. The ID
|
||
* is retained across copies of the node.
|
||
*/
|
||
class WithUniqueID {
|
||
public:
|
||
/**
|
||
* Returns an ID that's unique for this node. The ID is
|
||
* retained across copies of the node.
|
||
*/
|
||
ID uniqueID() const { return _id; }
|
||
|
||
/** Helper to call from the main node's properties() method. */
|
||
node::Properties properties() const { return {{"unique_id", _id}}; }
|
||
|
||
WithUniqueID() = delete;
|
||
WithUniqueID(const WithUniqueID& other) = default;
|
||
WithUniqueID(WithUniqueID&& other) noexcept = default;
|
||
~WithUniqueID() = default;
|
||
|
||
WithUniqueID& operator=(const WithUniqueID& other) = default;
|
||
WithUniqueID& operator=(WithUniqueID&& other) noexcept = default;
|
||
|
||
protected:
|
||
/**
|
||
*
|
||
* Constructor for derived classes.
|
||
*
|
||
* @param prefix prefix to use for the ID, used just for readability of the
|
||
* generated IDs
|
||
*/
|
||
WithUniqueID(const char* prefix) : _id(util::fmt("%s_%" PRIu64, prefix, _id_counter++)) {}
|
||
|
||
private:
|
||
ID _id;
|
||
|
||
inline static uint64_t _id_counter = 0;
|
||
};
|
||
|
||
/** Mix-in class for nodes storing doc strings. */
|
||
class WithDocString {
|
||
public:
|
||
/** Returns the documentation associated with the declaration, if any. */
|
||
const std::optional<DocString>& documentation() const { return _doc; }
|
||
|
||
/** Clears out any documentation associated with the declaration. */
|
||
void clearDocumentation() { _doc.reset(); }
|
||
|
||
/** Sets the documentation associated with the declaration. */
|
||
void setDocumentation(DocString doc) {
|
||
if ( doc )
|
||
_doc = std::move(doc);
|
||
else
|
||
_doc.reset();
|
||
}
|
||
|
||
private:
|
||
std::optional<DocString> _doc;
|
||
};
|
||
|
||
/** Helper to handle visitor cycles loops. */
|
||
class CycleDetector {
|
||
public:
|
||
void recordSeen(const Node* n) { _seen.insert(n); }
|
||
bool haveSeen(const Node* n) const { return _seen.find(n) != _seen.end(); }
|
||
void clear() { _seen.clear(); }
|
||
|
||
private:
|
||
std::unordered_set<const Node*> _seen;
|
||
};
|
||
|
||
/**
|
||
* Creates `Node` instances for a vector of objects all implementing the
|
||
* `Node` interface.
|
||
*/
|
||
template<typename T>
|
||
Nodes flatten(std::vector<T> t) {
|
||
Nodes v;
|
||
v.reserve(t.size());
|
||
for ( auto it = std::make_move_iterator(t.begin()); it != std::make_move_iterator(t.end()); ++it )
|
||
v.emplace_back(*it);
|
||
|
||
return v;
|
||
}
|
||
|
||
/**
|
||
* Copies a ramge of nodes over into a vector. Note that as with all copies of
|
||
* node, this performs shallow copying.
|
||
*/
|
||
template<typename T>
|
||
Nodes flatten(hilti::node::Range<T> t) {
|
||
Nodes v;
|
||
v.reserve(t.size());
|
||
for ( const auto& i : t )
|
||
v.emplace_back(std::move(i));
|
||
|
||
return v;
|
||
}
|
||
|
||
/** Create a 1-element vector of nodes for an object implementing the `Node` API. */
|
||
template<typename T = Node*>
|
||
inline Nodes flatten(Node* n) {
|
||
return {n};
|
||
}
|
||
|
||
/** Create a 1-element vector of nodes for an object implementing the `Node` API. */
|
||
template<typename T>
|
||
inline Nodes flatten(T* n) {
|
||
return {std::move(n)};
|
||
}
|
||
|
||
/** Create a 1-element vector of nodes for a nullptr. */
|
||
template<typename T = std::nullptr_t>
|
||
inline Nodes flatten(std::nullptr_t) {
|
||
return {nullptr};
|
||
}
|
||
|
||
/** Create an empty nodes list. */
|
||
inline Nodes flatten() { return Nodes(); }
|
||
|
||
/**
|
||
* Creates `Node` instances for objects all implementing the `Node`
|
||
* interface.
|
||
*/
|
||
template<typename T, typename... Ts, std::enable_if_t<(0 != sizeof...(Ts))>* = nullptr>
|
||
Nodes flatten(T t, Ts... ts) {
|
||
return util::concat(std::move(flatten(std::move(t))), flatten(std::move(ts)...));
|
||
}
|
||
|
||
/**
|
||
* Filters a node vector through a boolean predicate, returning a set
|
||
* containing the matching ones.
|
||
*/
|
||
template<typename X, typename F>
|
||
auto filter(const hilti::node::Range<X>& x, F f) {
|
||
hilti::node::Set<X> y;
|
||
for ( const auto& i : x ) {
|
||
if ( f(i) )
|
||
y.push_back(i);
|
||
}
|
||
|
||
return y;
|
||
}
|
||
|
||
/**
|
||
* Filters a node set through a boolean predicate, returning a new set
|
||
* containing the matching ones.
|
||
*/
|
||
template<typename X, typename F>
|
||
auto filter(const hilti::node::Set<X>& x, F f) {
|
||
hilti::node::Set<X> y;
|
||
for ( const auto& i : x ) {
|
||
if ( f(i) )
|
||
y.push_back(i);
|
||
}
|
||
|
||
return y;
|
||
}
|
||
|
||
/**
|
||
* Applies a function to each element of a node range, returning a new vector
|
||
* with the results.
|
||
*/
|
||
template<typename X, typename F>
|
||
auto transform(const hilti::node::Range<X>& x, F f) {
|
||
using Y = std::invoke_result_t<F, X*>;
|
||
std::vector<Y> y;
|
||
y.reserve(x.size());
|
||
for ( const auto& i : x )
|
||
y.push_back(f(i));
|
||
|
||
return y;
|
||
}
|
||
|
||
/**
|
||
* Applies a function to each element of a node set, returning a new vector of
|
||
* with the results.
|
||
*/
|
||
template<typename X, typename F>
|
||
auto transform(const hilti::node::Set<X>& x, F f) {
|
||
using Y = std::invoke_result_t<F, X*>;
|
||
std::vector<Y> y;
|
||
y.reserve(x.size());
|
||
for ( const auto& i : x )
|
||
y.push_back(f(i));
|
||
|
||
return y;
|
||
}
|
||
|
||
} // namespace node
|
||
|
||
/** Renders a node as HILTI source code. */
|
||
inline std::ostream& operator<<(std::ostream& out, const Node& n) {
|
||
n.print(out, true, true);
|
||
return out;
|
||
}
|
||
|
||
} // namespace hilti
|
||
|
||
inline hilti::node::Properties operator+(hilti::node::Properties p1, hilti::node::Properties p2) {
|
||
p1.merge(std::move(p2));
|
||
return p1;
|
||
}
|