/** * @file node.hpp * * Checked casts between node structs, iteration over lists of nodes, and * walking the tree from C++. */ #ifndef PRISM_NODE_HPP #define PRISM_NODE_HPP #include "prism/compiler/cplusplus.hpp" #include "prism/compiler/exceptions.hpp" #include "prism/compiler/lifetimebound.hpp" #include "prism/ast.hpp" #include "prism/node.h" #include "prism/node_type.hpp" #include #include #include #include #include #include namespace prism { /** * Whether the given node, which must not be null, is of the given struct * * @tparam T The node struct type to check against. * @param node The node to check. * @return True if the node is of the given struct, false otherwise. */ template [[nodiscard]] bool isa(const pm_node_t *node) noexcept { static_assert(is_node_v, "isa requires a node struct declared in ast.h"); assert(node != nullptr); return node->type == node_type_v; } /** * Cast the given node, which must not be null and must be of the given struct, * to that struct. The type is checked in debug builds. * * @tparam T The node struct type to cast to. * @param node The node to cast. * @return The node cast to the given struct. */ template [[nodiscard]] T * cast(pm_node_t *node) noexcept { assert(isa(node)); return reinterpret_cast(node); } /** * Cast the given node, which must not be null and must be of the given struct, * to that struct. The type is checked in debug builds. * * @tparam T The node struct type to cast to. * @param node The node to cast. * @return The node cast to the given struct. */ template [[nodiscard]] const T * cast(const pm_node_t *node) noexcept { assert(isa(node)); return reinterpret_cast(node); } /** * Cast the given node to the given struct if it is of that struct, and return * null otherwise. The node may be null, in which case this returns null. * * @tparam T The node struct type to cast to. * @param node The node to cast. * @return The node cast to the given struct if it is of that struct, or null * otherwise. */ template [[nodiscard]] T * dyn_cast(pm_node_t *node) noexcept { return (node != nullptr && isa(node)) ? reinterpret_cast(node) : nullptr; } /** * Cast the given node to the given struct if it is of that struct, and return * null otherwise. The node may be null, in which case this returns null. * * @tparam T The node struct type to cast to. * @param node The node to cast. * @return The node cast to the given struct if it is of that struct, or null * otherwise. */ template [[nodiscard]] const T * dyn_cast(const pm_node_t *node) noexcept { return (node != nullptr && isa(node)) ? reinterpret_cast(node) : nullptr; } /** * Return the pm_node_t embedded at the start of the given node struct. * * @tparam T The node struct type. * @param node The node struct to upcast. * @return The pm_node_t embedded at the start of the node struct. */ template [[nodiscard]] pm_node_t * upcast(T *node) noexcept { static_assert(is_node_v, "upcast requires a node struct declared in ast.h"); return &node->base; } /** * Return the pm_node_t embedded at the start of the given node struct. * * @tparam T The node struct type. * @param node The node struct to upcast. * @return The pm_node_t embedded at the start of the node struct. */ template [[nodiscard]] const pm_node_t * upcast(const T *node) noexcept { static_assert(is_node_v, "upcast requires a node struct declared in ast.h"); return &node->base; } /** * The nodes in a list, usable in a range-based for loop. It refers to the list * rather than copying it, so the list must outlive it. */ class node_range { public: /** * Create a range over the nodes in the given list. * * @param list The list of nodes to create a range for. */ explicit node_range(const pm_node_list_t &list PRISM_LIFETIMEBOUND) noexcept : list_(&list) {} /** * A pointer to the first node. * * @return A pointer to the first node, or null if the list is empty. */ pm_node_t *const *begin() const noexcept { return list_->nodes; } /** * A pointer one past the last node. * * @return A pointer one past the last node, or null if the list is empty. */ pm_node_t *const *end() const noexcept { return list_->nodes + list_->size; } /** * The number of nodes. * * @return The number of nodes. */ std::size_t size() const noexcept { return list_->size; } /** * Whether there are no nodes. * * @return True if there are no nodes, false otherwise. */ [[nodiscard]] bool empty() const noexcept { return list_->size == 0; } private: const pm_node_list_t *list_; }; namespace detail { /** * The state shared between a walk and its C callback, passed through the C * function's void * data parameter. */ template struct visit_context { /** * The context for a visit, holding the visitor and any exception thrown. * * @param visitor The visitor callable to be used for each node. */ explicit visit_context(F *visitor) noexcept : callable(visitor) {} /** The visitor that the walk calls for each node. */ F *callable; #ifdef PRISM_HAS_EXCEPTIONS /** The exception thrown by the visitor, if any. */ std::exception_ptr exception; #endif }; /** * Forwards the C visitor callback to a C++ callable. An exception cannot unwind * through the C frames that call this function, so one thrown by the callable * is caught here and the walk is stopped. The function that started the walk * rethrows it once the C walk has returned. Without exceptions, the callable * cannot throw, so it is called directly. * * @tparam F The type of the visitor callable. * @param node The current node being visited. * @param data The visit context containing the visitor and exception state. * @return The result of the visit, indicating whether to descend, skip, or stop. */ template pm_visit_result_t visit_callback(const pm_node_t *node, void *data) noexcept { visit_context *context = static_cast *>(data); #ifdef PRISM_HAS_EXCEPTIONS try { return std::invoke(*context->callable, node); } catch (...) { context->exception = std::current_exception(); return PM_VISIT_STOP; } #else return std::invoke(*context->callable, node); #endif } } /** * Walk the tree rooted at the given node in pre-order, calling the visitor with * each node. The visitor returns PM_VISIT_DESCEND to continue into that node's * children, PM_VISIT_SKIP to continue past them, or PM_VISIT_STOP to end the * walk. Returns false if the walk was stopped. If the visitor throws, the walk * stops and the exception propagates from this function. * * @tparam F The type of the visitor callable. * @param node The root node to start the walk from. * @param visitor The visitor callable to invoke for each node. * @return True if the walk completed, false if it was stopped. */ template bool visit_node(const pm_node_t *node, F &&visitor) { static_assert(std::is_invocable_r_v, "visit_node requires a callable taking const pm_node_t * and returning pm_visit_result_t"); detail::visit_context> context(std::addressof(visitor)); bool completed = pm_visit_node(node, detail::visit_callback>, &context); #ifdef PRISM_HAS_EXCEPTIONS if (context.exception) std::rethrow_exception(context.exception); #endif return completed; } /** * Walk the children of the given node, as visit_node does for every node whose * visitor returns PM_VISIT_DESCEND. Returns false if the walk was stopped. If * the visitor throws, the walk stops and the exception propagates from this * function. * * @tparam F The type of the visitor callable. * @param node The node whose children to walk. * @param visitor The visitor callable to invoke for each child node. * @return True if the walk completed, false if it was stopped. */ template bool visit_child_nodes(const pm_node_t *node, F &&visitor) { static_assert(std::is_invocable_r_v, "visit_child_nodes requires a callable taking const pm_node_t * and returning pm_visit_result_t"); detail::visit_context> context(std::addressof(visitor)); bool completed = pm_visit_child_nodes(node, detail::visit_callback>, &context); #ifdef PRISM_HAS_EXCEPTIONS if (context.exception) std::rethrow_exception(context.exception); #endif return completed; } } #endif