// Copyright (c) 2020-now by the Zeek Project. See LICENSE for details. #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include namespace hilti { class ASTContext; class Context; class Driver; struct Plugin; namespace type { struct Wildcard; } /** * Parses a HILTI source file into an AST. * * @param builder builder to use for constructing the AST * @param in stream to read from * @param filename path associated with the input * * @returns the parsed AST, or a corresponding error if parsing failed. */ Result parseSource(Builder* builder, std::istream& in, const std::string& filename); namespace ast { namespace detail { class DependencyTracker; /** * Helper class to define a strongly-typed index type used with maps inside the * AST context. */ template class ContextIndex { public: /** * Constructor. * * @param index value to store with the index, which should be larger than * zero for valid indices; zero is the default and reserved for `None`. */ explicit ContextIndex(size_t index = 0) : _value(index) {} /** Returns the index' value. */ auto value() const { return _value; } /** * Returns a string representation of the value, including a prefix * indicating the index' type. */ auto str() const { return _value > 0 ? std::string(1, Prefix) + rt::to_string(_value) : std::string("-"); } /** Returns true if the index is not `None` (i.e., zero). */ explicit operator bool() const { return *this != None; } bool operator==(const ContextIndex& other) const { return _value == other._value; } bool operator!=(const ContextIndex& other) const { return _value != other._value; } ContextIndex(const ContextIndex& other) = default; ContextIndex(ContextIndex&& other) noexcept = default; ContextIndex& operator=(const ContextIndex& other) = default; ContextIndex& operator=(ContextIndex&& other) noexcept = default; inline static const ContextIndex None{0}; /**< index with reserved value zero representing an unset index */ private: rt::integer::safe _value = 0; // safe integer to catch any uint32_t overflows }; template inline std::string to_string(const hilti::ast::detail::ContextIndex& index) { return index.str(); } template inline std::ostream& operator<<(std::ostream& out, const hilti::ast::detail::ContextIndex& index) { return out << index.str(); } // Helper to sort declaration pointers by their canonical IDs. struct DeclarationPtrCmp { bool operator()(const Declaration* a, const Declaration* b) const; }; } // namespace detail /** Strongly typed index for declarations. */ using DeclarationIndex = detail::ContextIndex<'D'>; /** Strongly typed index for types. */ using TypeIndex = detail::ContextIndex<'T'>; } // namespace ast /** * Environment for AST-wide state. The context maintains the AST root node and * owns all nodes added to it or, recursively, any of if children. Each node * can be part of just one AST context. Over time, the context also builds up * further state about the AST. */ class ASTContext : public std::enable_shared_from_this { public: // Set of declarations ordered by their canonical IDs. using DeclarationSet = std::set; /** * Constructor. * * @param context compiler context to use for logging and error reporting */ ASTContext(Context* context); /** Destructor. */ ~ASTContext(); /** Returns the current compiler context in use. */ auto* compilerContext() const { return _context; } /** Returns the AST's root node. This always exists. */ auto root() const { return _root.get(); } /** * Parses a source file and adds it to the AST as a new module. If a module * for this file is already part of the AST, returns the existing module * without any further AST changes. # * * * @param builder builder to use for constructing the parsed AST * @param path path to source file to parse * @param process_extension if given, file extension indicating which * plugin to use later for processing the resulting AST for the module; if * not given, the same plugin will be used as for parsing (which is * determined by the path's extension) * @return UID of the parsed module (which is now a part of the AST), or an * error if parsing failed */ Result parseSource(Builder* builder, const hilti::rt::filesystem::path& path, std::optional process_extension = {}); /** * Imports a module from an external source file and adds it to the AST as * a new module. This implements HILTI's `import` statement. If a module * for the requested `import` is already part of the AST, returns the * existing module without any further AST changes. * * @param builder builder to use for constructing the parsed AST * @param id name of the module to import (as in: ``import ``) * @param scope search scope for the import (as in: ``import ... from ``) * @param parse_extension file extension indicating which plugin to use for * parsing the module's source code * @param process_extension if given, file extension indicating which * plugin to use later for processing the resulting AST; if not given, the * same plugin will be used as for parsing * @param search_dirs list of directories to search for the module's source * (in addition to any globally configured search directories) * @return UID of the parsed module (which is now a part of the AST), or an * error if parsing failed */ Result importModule(Builder* builder, const ID& id, const ID& scope, const hilti::rt::filesystem::path& parse_extension, const std::optional& process_extension, std::vector search_dirs); /** Adds a new, empty module to the AST. */ declaration::Module* newModule(Builder* builder, ID id, const hilti::rt::filesystem::path& process_extension); /** * Retrieves a module node from the AST given its UID. Returns null if no * such module exists. * * @param uid UID of module to return */ declaration::Module* module(const declaration::module::UID& uid) const { if ( auto m = _modules_by_uid.find(uid); m != _modules_by_uid.end() ) return m->second; else return nullptr; } /** * Processes the whole AST with all of the compiler's visitor passes. This * is the top-level entry point for all resolving/validating/optimizing. If * successful, the will be fully resolved and validated; and ready for code * generation. * * @param builder current compiler builder, which AST processing may access * @param driver current compiler driver, which AST processing may access */ Result processAST(Builder* builder, Driver* driver); /** * During AST processing, returns the current compiler driver. If called * outside of `processAST() executing, it will return null. */ Driver* driver() const { return _driver; } /** * Returns direct & indirect dependencies that a module imports. This * information will be available only once the AST has been processed * successfully through `processAST()`. * * @param uid UID of module to return dependencies for; the module must be * known, otherwise an internal error is reported * @param recursive if true, return the transitive closure of all * dependent units, vs just direct dependencies of the specified unit * @return set of dependencies */ std::set dependencies(const declaration::module::UID& uid, bool recursive = false) const; /** * Returns direct & indirect, global dependencies of a given global * declaration. A "global declaration" is any declaration declared at * either the root or the module level (i.e., node depth <= 2). The result * will likewise include only such global declarations. * * This information will be available only once the AST has been processed * successfully through `processAST()`. If called before that, the method * will abort with an internal error. * * @param d declaration to return dependencies for, which must be part of * the AST and declared at the root or module level * * @return dependencies of *d*, which will all be root or module-level * nodes as well; will include the declaration *d* itself iff there's a * dependency cycle where any of the children depends in turn on the * declaration itself; will return an empty set if the declaration has no * dependencies, including if *d* is not actually a global declaration */ const DeclarationSet& dependentDeclarations(Declaration* n); /** * Updates an existing UID with new information. * * The given, old UID must correspond to a module parsed or imported into * the context. This method then changes the module associated with that * old UID to be associated with the new UID instead, and updates any * context state accordingly, so that the module can now be found through * the new UID. * * @param old_uid existing UID; it's an internal error if this doesn't exist * @param new_uid new UID to replace `old_uid` */ void updateModuleUID(const declaration::module::UID& old_uid, const declaration::module::UID& new_uid); /** * Registers a declaration with the context, assigning it a unique index * through which it can later be retrieved. That index is automatically * stored with the declaration as its `declarationindex(). * * If the declaration is a type declaration, the method also sets the * declared type's `declarationIndex()` accordingly, linked the type with * its declaration. * * If the same declaration had already been registered earlier, nothing is * changed; the method then simply returns the prior index. * * @param decl declaration to register * @returns the index now associated with the declaration; it's value is * guaranteed to not be `None` (and hence be larger than zero). */ ast::DeclarationIndex register_(Declaration* decl); /** * Returns the declaration associated with an index. * * @param index index to lookup, which must have been registered before to * not trigger an internal error; unless its `None`, in which case it * returns `null`. */ Declaration* lookup(ast::DeclarationIndex index); // must exists, otherwise internal error -> result is not null /** * Replaces a previously registered declaration with a new one. This means * that any lookup for the existing declaration's index will now return the * new declaration instead. The new declaration's `declarationIndex()` will * automatically be set to index; the old declaration's * `declarationIndex()` will not be changed. * * If the new declaration is a type declaration, the method also sets the * declared type's `declarationIndex()` accordingly, linking the type with * its declaration. The old declaraed type is not changed. * * @param old old declaration; if it has not been registered yet at all, * the method returns without doing anything * @param new new declaration to take the place of the old one */ void replace(Declaration* old, Declaration* new_); /** * Registers a type with the context, assigning it a unique index through * which it can later be retrieved. That index is automatically stored with * the type as its `typeIndex(). * * If the same type had already been registered earlier, nothing is * changed; the method then simply returns the prior index. * * @param decl declaration to register * @returns the index now associated with the type; it's value is * guaranteed to not be `None` (and hence be larger than zero). */ ast::TypeIndex register_(UnqualifiedType* type); /** * Returns the type associated with an index. * * @param index index to lookup, which must have been registered before to * not trigger an internal error; unless its `None`, in which case it * returns `null`. */ UnqualifiedType* lookup(ast::TypeIndex index); /** * Replaces a previously registered type with a new one. This means * that any lookup for the existing type's index will now return the * new type instead. The new type's `typeIndex()` will * automatically be set to index; the old type's * `typeIndex()` will not be changed. * * @param old old type; if it has not been registered yet at all, * the method returns without doing any tying * @param new new type to take the place of the old one */ void replace(UnqualifiedType* old, UnqualifiedType* new_); /** * Given an ID that's supposed to become a declaration's canonical ID, * ensure that ID is globally unique within the context, returning an appropriately modified version if necessary. */ ID uniqueCanononicalID(const ID& id) { return _canon_id_uniquer.get(id, false); } /** * Dumps the current total AST of all modules to a debug stream. * * @param stream debug stream to write to * @param prefix prefix line to start output with */ void dump(const hilti::logging::DebugStream& stream, const std::string& prefix); /** * Factory function creating a new node of type T. This allocates the new * through the context-wide memory resource. * * @param args arguments to pass to the constructor of T */ template T* make(Args&&... args) { auto t = new T(std::forward(args)...); _nodes.emplace_back(std::unique_ptr(t)); return t; } /** * Factory function creating a new node of type T. This allocates the new * through the context-wide memory resource. * * We need this variant because `std::initializer_list` cannot be passed * through the generic `std::forward`. * * @param ctx first argument to pass to the constructor of T; must be `this` * @param children second argument to pass to the constructor of T * @param args remaining arguments to pass to the constructor of T */ template T* make(ASTContext* ctx, std::initializer_list children, Args&&... args) { assert(ctx == this); auto t = new T(ctx, children, std::forward(args)...); _nodes.emplace_back(std::unique_ptr(t)); return t; } /** * Factory function creating a new node of type T. This allocates the new * through the context-wide memory resource. * * We need this variant because `std::initializer_list` cannot be passed * through the generic `std::forward`. * * @param ctx first argument to pass to the constructor of T; must be `this` * @param wildcard second argument to pass to the constructor of T; must be `this` * @param children third second argument to pass to the constructor of T * @param args remaining arguments to pass to the constructor of T */ template T* make(ASTContext* ctx, type::Wildcard&& wildcard, std::initializer_list children, Args&&... args) { assert(ctx == this); auto t = new T(ctx, std::forward(wildcard), children, std::forward(args)...); _nodes.emplace_back(std::unique_ptr(t)); return t; } /** Clears up an AST nodes that are not currently retained by anybody. */ void garbageCollect(); /** Release all state. */ void clear(); private: // The following methods implement the corresponding phases of AST processing. Result _parseSource(Builder* builder, const hilti::rt::filesystem::path& path, const ID& scope, std::optional process_extension = {}); Result _init(Builder* builder, const Plugin& plugin); Result _buildScopes(Builder* builder, const Plugin& plugin); Result _clearState(Builder* builder, const Plugin& plugin); Result _resolve(Builder* builder, const Plugin& plugin); Result _resolveUnresolvedNodes(bool* modified, Builder* builder, const Plugin& plugin); Result _resolveRoot(bool* modified, Builder* builder, const Plugin& plugin); Result _validate(Builder* builder, const Plugin& plugin, bool pre_resolver); Result _transform(Builder* builder, const Plugin& plugin); Result _collectErrors(); Result _optimize(Builder* builder); Result _computeDependencies(); // Adds a module to the AST. The module must not be part of any AST yet // (including the current one). declaration::module::UID _addModuleToAST(declaration::Module* module); // Performs internal consistency checks on the AST. Meant to execute only // in debug builds as it may affect performance. void _checkAST(bool finished) const; // Dumps the AST to disk during AST processing, for debugging.. void _saveIterationAST(const Plugin& plugin, const std::string& prefix, int round = 0); // Dumps the AST to disk during AST processing, for debugging.. void _saveIterationAST(const Plugin& plugin, const std::string& prefix, const std::string& tag); // Dumps the AST to a debugging stream. void _dumpAST(const hilti::logging::DebugStream& stream, const Plugin& plugin, const std::string& prefix, int round); // Dumps the AST to a debugging stream. void _dumpAST(std::ostream& stream, const Plugin& plugin, const std::string& prefix, int round); // Dumps the accumulated state tables of the context to a debugging stream. void _dumpState(const logging::DebugStream& stream); // Dump statistics about the AST to a debugging stream. void _dumpStats(const logging::DebugStream& stream, std::string_view tag); // Dumps the accumulated state tables of the context to a debugging stream. void _dumpDeclarations(const logging::DebugStream& stream, const Plugin& plugin); Context* _context = nullptr; // compiler context std::vector> _nodes; // all nodes allocated through the context; used by garbage collection node::RetainedPtr _root = nullptr; // root node of the AST bool _resolved = false; // true if `processAST()` has finished successfully Driver* _driver = nullptr; // pointer to compiler drive during `processAST()`, null outside of that util::Uniquer _canon_id_uniquer; // Produces unique canonified IDs std::unique_ptr _dependency_tracker; // records dependencies between declarations uint64_t _total_rounds = 0; // total number of rounds of AST processing std::unordered_map> _modules_by_uid; // all known modules indexed by UID std::unordered_map _modules_by_path; // all known modules indexed by path (retained by _by_uid) std::map, declaration::Module*> _modules_by_id_and_scope; // all known modules indexed by their ID and search scope (retained by _by_uid) std::vector> _declarations_by_index; // all registered declarations; vector position corresponds to their index std::vector> _types_by_index; // all registered types; vector position corresponds to their index }; /** * Root node for the AST inside an AST context. This will always exist exactly * once. */ class ASTRoot : public Node { public: static auto create(ASTContext* ctx) { return ctx->make(ctx); } protected: ASTRoot(ASTContext* ctx) : Node(ctx, NodeTags, {}, Meta(Location(""))) {} std::string _dump() const final; HILTI_NODE_0(ASTRoot, final); }; } // namespace hilti