// Copyright (c) 2020-now by the Zeek Project. See LICENSE for details. // p #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace hilti { class JIT; struct Options; namespace driver { /** Enum to specify type of dependencies to output. */ enum class Dependencies { None, /**< No output of dependencies. */ All, /**< Output all other modules being depended on. */ Code /**< Output other modules being depended if they require separate compilation of their own to produce code. */ }; /** * Options for the compiler driver * * @note Only one of the `output_*` can be used at any time. */ struct Options { bool include_linker = false; /**< if true, perform custom HILTI linker phase */ bool output_hilti = false; /**< render HILTI inputs back into HILTI source code */ bool output_prototypes = false; /**< output C++ prototypes for generated code */ bool output_cxx = false; /**< output generated C++ code */ std::string output_cxx_prefix; /**< when outputting generated C++ code, prefix each module name with this string */ bool output_linker = false; /**< output generated HILTI linker C++ code */ Dependencies output_dependencies = Dependencies::None; /**< output dependencies for compiled modules */ bool execute_code = false; /**< compile code, and execute unless output_path is set */ bool show_backtraces = false; /**< include backtraces when printing unhandled exceptions */ bool abort_on_exceptions = false; /**< abort() instead of throwing HILTI exceptions */ bool keep_tmps = false; /**< do not delete any temporary files created */ bool skip_dependencies = false; /**< do not automatically compile dependencies during JIT */ bool report_resource_usage = false; /**< print summary of runtime resource usage at termination */ bool report_times = false; /**< Report break-down of driver's execution time. */ bool dump_code = false; /**< Record all final HILTI and C++ code to disk for debugging. */ bool enable_profiling = false; /**< Insert profiling instrumentation into generated C++ code */ std::vector inputs; /**< files to compile; these will be automatically pulled in by ``Driver::run()`` */ hilti::rt::filesystem::path output_path; /**< file to store output in (default if empty is printing to stdout) */ std::unique_ptr logger; /**< `Logger` instances to use for diagnostics; set to a new logger by default by constructor */ Options() { logger = std::make_unique(); } }; } // namespace driver /** * Compiler driver. * * The driver is a high-level building block for writing command-line tools * compiling HILTI source files (and more). `hiltic` is just a tiny wrapper * around this class. * * Classes can drive from the driver to expand its functionality, including * for adding support for additional types of source files (e.g., Spicy * code). * */ class Driver { public: /** * @param name descriptive name for the tool using the driver, which will * be used in usage and error messages. */ explicit Driver(std::string name); /** * @param name descriptive name for the tool using the driver, which will * be used in usage and error messages. * @param argv0 the current executable, which will change the path's that * the global options instance returns if it's inside HILTI build * directory. */ Driver(std::string name, const hilti::rt::filesystem::path& argv0); virtual ~Driver(); Driver() = delete; Driver(const Driver&) = delete; Driver(Driver&&) noexcept = delete; Driver& operator=(const Driver&) = delete; Driver& operator=(Driver&&) noexcept = delete; /** * Frontend for parsing command line options into `driver::Options` and * `hilti::Options``. See the output of `hiltic --help` for a list. * * `setDriverOptions()` and `setCompilerOptions()` provide alternative * ways to set the options directly. * * @param argc,argv command line arguments to parse * @return set if successful; otherwise the result provides an error message */ Result parseOptions(int argc, char** argv); /** * Registers a unit with the driver for compilation. If a unit with the * same UID is already known, this is a no-op. * * @param unit unit to register */ void registerUnit(const std::shared_ptr& unit) { _addUnit(unit); } /** * Looks up a previously registered by its UID. * * @param uid UID to look up * @return pointer to unit, or null if not found */ Unit* lookupUnit(const declaration::module::UID& uid) const { auto i = _units.find(uid); return i != _units.end() ? i->second.get() : nullptr; } /** * Changes the process extension of an existing unit. * * This also updates the unit's module by changing its UID accordingly. * * @param uid UID of the unit to change, which must correspond to a known * unit * @param ext new process extension; no other unit of the same name must * exist yet with that extension */ void updateProcessExtension(const declaration::module::UID& uid, const hilti::rt::filesystem::path& ext); /** * Schedules a source file for compilation. The file will be parsed * immediately, and then compiled later when `compile()` is called. If the * same source unit has been added previously, this method will have no * effect. * * The `hookAddInput()` and `hookNewASTPreCompilation()` hooks will be * called immediately for the new module. * * @param path source code to compile * @return set if successful; otherwise the result provides an error message */ Result addInput(const hilti::rt::filesystem::path& path); /** * Schedules an already existing module for compilation. This is useful * when input modules are generated in-memory on the fly. The module must * have already been added to the AST. It will now be registered as a * source unit for full processing (compilation to C++; JIT) just as any * on-disk source files added with `addInput()`. * * @param uid UID of the existing module * @return set if successful; otherwise the result provides an error message */ Result addInput(declaration::module::UID uid); /** Returns true if at least one input file has been added. */ bool hasInputs() const { return ! (_units.empty() && _libraries.empty() && _external_cxxs.empty()); } /** Returns the driver options currently in effect. */ const auto& driverOptions() const { return _driver_options; } /** Returns the HILTI compiler options currently in effect. */ const auto& hiltiOptions() const { return _compiler_options; } /** * Sets the driver's options and arguments. * * @param options the options */ void setDriverOptions(driver::Options options); /** * Sets HILTI's compiler options. * * @param options the options */ void setCompilerOptions(hilti::Options options); /** * Initializes the compilation process. Must be called after options have been set, * and before any inputs are added. * * @return set if successful; otherwise the result provides an error message */ Result initialize(); /** * Loads, compiles, and links the source files. This must be called only * after driver and compiler options have been set. Internally, it chains * the various `*Modules()` methods. * * @return set if successful; otherwise the result provides an error message */ Result compile(); /** * Returns the current HILTI context. Valid only once compilation has * started, otherwise null. */ const auto& context() const { return _ctx; } /** * Returns the current builder. Valid only once compilation has * started, otherwise null. */ auto* builder() const { return _builder.get(); } /** Shortcut to return the current context's options. */ const auto& options() const { return _ctx->options(); } /** * Initializes HILTI's runtime system to prepare for execution of * compiled code. This will already trigger execution of all * module-specific initialization code (initialization of globals; * module-level statements). The method must be called only after * `compile()` has run already. * * @return set if successful; otherwise the result provides an error message */ Result initRuntime(); /** * Executes the `hilti_main` entry function in compiled code. This must * be called only after `initRuntime()` has run already. * * @return set if successful; otherwise the result provides an error message */ Result executeMain(); /** * Shuts down HILT's runtime library after execution has concluded, * cleaning up resources. * * @return set if successful; otherwise the result provides an error message */ Result finishRuntime(); /** * Compile and executes all source files. This is a convenience wrapper * around the stages of the process provided by other methods. It * executes all of `compile()`, `initRuntime()`, `executeMain()`, and * `finishRuntime()` in that order. * * @return set if successful; otherwise the result provides an error message */ Result run(); protected: friend class ASTContext; /** * Prints a usage message to stderr. The message summarizes the options * understood by `parseOptions()`. */ void usage(); /** * Compiles all registered input files to HILTI code. * * @return set if successful; otherwise the result provides an error message */ Result compileUnits(); /** * Compiles all registered input files to C++ code. * * This function can only be invoked after `compileUnits`. * * @return set if successful; otherwise the result provides an error message */ Result codegenUnits(); /** * Runs the HILTI-side linker on all available C++ code. * * @return set if successful; otherwise the result provides an error message */ Result linkUnits(); /** * Writes out generated code if requested by driver options. * * @return set if successful; otherwise the result provides an error message */ Result outputUnits(); /** * JIT all code compiled so far. * * @return set if successful; otherwise the result provides an error message */ Result jitUnits(); /** * Helper function to create an `result::Error` with a message that * including driver name and, optionally, a file the error refers to. * * @param msg error message * @param p file to associate with the error, empty for none * @return error with an appropriately set message */ result::Error error(std::string_view msg, const hilti::rt::filesystem::path& p = ""); /** * Helper function to augment an `result::Error` with a message that * including driver name and, optionally, a file the error refers to. * * @param msg error message * @param p file to associate with the error, empty for none * @return error with an appropriately set message */ result::Error augmentError(const result::Error& err, const hilti::rt::filesystem::path& p = ""); /** * Helper function to open a file for writing. * * @param p output file * @param binary true to open in binary mode * @param append true to append to existing file * @return set if successful, or an appropriate error result */ Result openOutput(const hilti::rt::filesystem::path& p, bool binary = false, bool append = false); /** * Helper function to open a file for reading. * * @param in input stream to open with file with * @param p input file * @return set if successful, or an appropriate error result */ Result openInput(std::ifstream& in, const hilti::rt::filesystem::path& p); /** * Helper function to write data into an output file. * * @param in stream to read data to write from * @param p output file * @return set if successful, or an appropriate error result */ Result writeOutput(std::ifstream& in, const hilti::rt::filesystem::path& p); /** * Helper function to read data from an input file. * * @param p input file * @return string stream with the file's data, or an appropriate error result */ Result readInput(const hilti::rt::filesystem::path& p); /** * Copies an input stream into a temporary file on disk * * @param in stream to read from * @param name_hint a string to include into the temporary file's name * @param extension extension for the temporary file's name * @return the path to the temporary file, or an appropriate error result */ Result writeToTemp(std::ifstream& in, const std::string& name_hint, const std::string& extension = "tmp"); /** Save a unit's final HILTI and C++ code to disk for debugging. */ void dumpUnit(const Unit& unit); /** * Prints an uncaught HILTI exception to stderr. * * @param e exception to print */ void printHiltiException(const hilti::rt::Exception& e); /** * Instantiates a new builder tied to a given context. Derived class can * override this to create a builder own their own type (as long as that's * derived from `hilti::Builder`). * * @param ctx context to create builder for * @return new builder instance */ virtual std::unique_ptr createBuilder(ASTContext* ctx) const; /** * Hook for derived classes to add more options to the getopt() option * string. */ virtual std::string hookAddCommandLineOptions() { return ""; } /** Hook for derived classes for parsing additional options. */ virtual bool hookProcessCommandLineOption(int opt, const char* optarg) { return false; } /** * Hook for derived classes for adding content to the driver's usage * message (`--help`). */ virtual std::string hookAugmentUsage() { return ""; } /** * Hook for derived classes to execute custom code when a new unit * is being added as an input file. */ virtual void hookAddInput(std::shared_ptr unit) {} // NOLINT(performance-unnecessary-value-param) /** * Hook for derived classes to execute custom code when a new source code * file is being added as an input file. */ virtual void hookAddInput(const hilti::rt::filesystem::path& path) {} /** * Hook for derived classes to execute custom code when an HILTI AST has * been initially set up for processing by a plugin. This hook will run * before the AST has been processed any further by that plugin. */ virtual void hookNewASTPreCompilation(const Plugin& plugin, ASTRoot* root) {} /** * Hook for derived classes to execute custom code when an HILTI AST been * finalized by a plugin. This hook will run after the AST has been be * fully processed by that plugin, but before it's being transformed. * * The hook may modify the AST further, including adding new modules. If it * does indicate so with its return value, AST processing will start over * again to fully resolve the modified AST. Once that's done, this hook * will execute again. Note, however, that the hook cannot add anything to * the AST that depends on *previous& plugins, as they won't execute again. * * @param plugin the plugin that has processed the AST * @param root the AST that has been processed * @return true if the AST has been modified and needs to be reprocessed */ virtual bool hookNewASTPostCompilation(const Plugin& plugin, ASTRoot* root) { return false; } /** * Hook for derived classes to execute custom code when the AST has been * fully processed and transformed to its final state. */ virtual Result hookCompilationFinished(ASTRoot* root) { return Nothing(); } /** * Hook for derived classes to execute custom code when the HILTI runtime * has been initialized. */ virtual void hookInitRuntime() {} /** * Hook for derived classes to execute custom code just before the HILTI * runtime is being shut down. */ virtual void hookFinishRuntime() {} private: // Tracking the state of the compilation pipeline to catch out of order // operation. enum Stage { UNINITIALIZED, INITIALIZED, COMPILED, CODEGENED, LINKED, JITTED } _stage = UNINITIALIZED; // Backend for adding a new unit. void _addUnit(const std::shared_ptr& unit); // Run a specific plugin's AST passes on all units with the corresponding extension. Result _resolveUnitsWithPlugin(const Plugin& plugin, std::vector> units, int& round); // Runs a specific plugin's transform step on a given set of units. Result _transformUnitsWithPlugin(const Plugin& plugin, const std::vector>& units); // Run all plugins on current units, iterating until finished. Result _resolveUnits(); // Turns all HILTI units into C++. Result _codegenUnits(); // Performs global transformations on the generated code. Result _optimizeUnits(); // Sends a debug dump of a unit's AST to the global logger. void _dumpAST(const std::shared_ptr& unit, const logging::DebugStream& stream, const Plugin& plugin, const std::string& prefix, int round); // Sends a debug dump of a unit's AST to the global logger. void _dumpAST(const std::shared_ptr& unit, const logging::DebugStream& stream, const std::string& prefix); // Sends a debug dump of a unit's AST to an output stream. void _dumpAST(const std::shared_ptr& unit, std::ostream& stream, const Plugin& plugin, const std::string& prefix, int round); // Records a reduced debug dump of a unit's AST limited to just declarations. void _dumpDeclarations(const std::shared_ptr& unit, const Plugin& plugin); // Records a debug dump of a unit's AST to disk. void _saveIterationAST(const std::shared_ptr& unit, const Plugin& plugin, const std::string& prefix, int round); // Records a debug dump of a unit's AST to disk. void _saveIterationAST(const std::shared_ptr& unit, const Plugin& plugin, const std::string& prefix, const std::string& tag); // Sets the C++ namespaces for generated code from -x/-P prefix options. Result _setCxxNamespacesFromPrefix(const char* prefix); /** * Look up a symbol in the global namespace. * * @param symbol the symbol to look up * @return either a valid, not-nil pointer to the symbol or an error */ static Result _symbol(const std::string& symbol); std::string _name; driver::Options _driver_options; hilti::Options _compiler_options; std::shared_ptr _ctx; // driver's compiler context std::unique_ptr _builder; // driver builder tied to its context std::unique_ptr _jit; // driver's JIT instance std::shared_ptr _library; // Compiled code std::map> _units; std::vector _generated_cxxs; std::unordered_map _libraries; std::vector _external_cxxs; std::vector _mds; std::unordered_set _processed_paths; bool _runtime_initialized = false; // true once initRuntime() has succeeded std::set _tmp_files; // all tmp files created, so that we can clean them up. }; } // namespace hilti