diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e185fbc5..580b20461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - add support for raw strings: `r"string..."`, backslashes do not have to be escaped in them - stdlib: - new draft module `re`, using `google/re2` as the regex engine +- cli: + - new flag `--stats` when compiling code, outputting a JSON of compilation stats ### Changed - `@` can be used with dictionaries: `(@ dict key)` will behave the same as `(dict:get dict key)` diff --git a/README.md b/README.md index 108c373e6..feb79ac5c 100644 --- a/README.md +++ b/README.md @@ -204,22 +204,27 @@ DESCRIPTION ArkScript programming language SYNOPSIS - arkscript -h - arkscript -v - arkscript --dev-info - arkscript -e + arkscript -h + arkscript -v + arkscript --dev-info + arkscript -e arkscript [-d] [-L ] [-f(importsolver|no-importsolver)] [-f(macroprocessor|no-macroprocessor)] [-f(optimizer|no-optimizer)] - [-f(iroptimizer|no-iroptimizer)] [-fdebugger] [-fdump-ir] [-fno-cache] ((-c - ) | ) + [-f(irinliner|no-irinliner)] [-f(iroptimizer|no-iroptimizer)] [-fdebugger] + [-fdump-ir] [-fno-cache] -c [--stats] - arkscript -f [--(dry-run|check)] - arkscript [-d] [-L ] --ast - arkscript -bcr -on - arkscript -bcr -a [-s ] - arkscript -bcr -st [-s ] - arkscript -bcr -vt [-s ] - arkscript -bcr [-cs] [-p ] [-s ] + arkscript [-d] [-L ] [-f(importsolver|no-importsolver)] + [-f(macroprocessor|no-macroprocessor)] [-f(optimizer|no-optimizer)] + [-f(irinliner|no-irinliner)] [-f(iroptimizer|no-iroptimizer)] [-fdebugger] + [-fdump-ir] [-fno-cache] + + arkscript -f [--(dry-run|check)] + arkscript [-d] [-L ] --ast + arkscript -bcr -on + arkscript -bcr -a [-s ] + arkscript -bcr -st [-s ] + arkscript -bcr -vt [-s ] + arkscript -bcr [-cs] [-p ] [-s ] OPTIONS -h, --help Display this message @@ -238,6 +243,7 @@ OPTIONS Toggle on and off the macro processor pass -f(optimizer|no-optimizer) Toggle on and off the optimizer pass + -f(irinliner|no-irinliner) Toggle on and off the IR inliner pass -f(iroptimizer|no-iroptimizer) Toggle on and off the IR optimizer pass @@ -246,6 +252,7 @@ OPTIONS -fno-cache Disable the bytecode cache creation -c, --compile Compile the given program to bytecode, but do not run If file is -, it reads code from stdin + --stats Gather stats about each compiler pass and print them to stdout -f, --format Format the given source file in place --dry-run Do not modify the file, only print out the changes --check Check if a file formating is correctly, without modifying it. @@ -270,10 +277,10 @@ OPTIONS -s, --slice Select a slice of instructions in the bytecode VERSION - 4.2.0-94e546d6 + 4.7.2-28c5a9e6 BUILD DATE - 2026-02-21T20:42:38Z + 2026-09-04T18:22:01Z LICENSE Mozilla Public License 2.0 diff --git a/cppcheck-suppressions.txt b/cppcheck-suppressions.txt index a74036320..3a73382a4 100644 --- a/cppcheck-suppressions.txt +++ b/cppcheck-suppressions.txt @@ -4,3 +4,4 @@ unusedFunction unusedStructMember checkersReport *:include/Ark/Compiler/Instructions.x +*:include/Ark/Compiler/Statistics.x diff --git a/include/Ark/Compiler/AST/Optimizer.hpp b/include/Ark/Compiler/AST/Optimizer.hpp index ab9f9cef3..5a183be0e 100644 --- a/include/Ark/Compiler/AST/Optimizer.hpp +++ b/include/Ark/Compiler/AST/Optimizer.hpp @@ -32,8 +32,9 @@ namespace Ark::internal * @brief Construct a new Optimizer * * @param debug level of debug + * @param stats_collector optional statistics collector */ - explicit Optimizer(unsigned debug) noexcept; + explicit Optimizer(unsigned debug, Statistics* stats_collector = nullptr) noexcept; /** * @brief Send the AST to the optimizer, then run the different optimization strategies on it diff --git a/include/Ark/Compiler/AST/Parser.hpp b/include/Ark/Compiler/AST/Parser.hpp index a8285cc1c..bc95af629 100644 --- a/include/Ark/Compiler/AST/Parser.hpp +++ b/include/Ark/Compiler/AST/Parser.hpp @@ -38,6 +38,7 @@ namespace Ark::internal public: /** * @brief Constructs a new Parser object + * * @param debug debug level * @param mode how the parser should behave regarding certain nodes and errors */ @@ -45,6 +46,7 @@ namespace Ark::internal /** * @brief Parse the given code + * * @param filename can be left empty, used for error generation * @param code content of the file */ diff --git a/include/Ark/Compiler/IntermediateRepresentation/IRCompiler.hpp b/include/Ark/Compiler/IntermediateRepresentation/IRCompiler.hpp index be3943fda..d797563df 100644 --- a/include/Ark/Compiler/IntermediateRepresentation/IRCompiler.hpp +++ b/include/Ark/Compiler/IntermediateRepresentation/IRCompiler.hpp @@ -30,8 +30,9 @@ namespace Ark::internal * @brief Create a new IRCompiler * * @param debug debug level + * @param stats_collector optional statistics collector */ - explicit IRCompiler(unsigned debug); + explicit IRCompiler(unsigned debug, Statistics* stats_collector = nullptr); /** * @brief Turn a given IR into bytecode diff --git a/include/Ark/Compiler/IntermediateRepresentation/IRInliner.hpp b/include/Ark/Compiler/IntermediateRepresentation/IRInliner.hpp index 37042c9a8..ac3d2dd19 100644 --- a/include/Ark/Compiler/IntermediateRepresentation/IRInliner.hpp +++ b/include/Ark/Compiler/IntermediateRepresentation/IRInliner.hpp @@ -44,8 +44,9 @@ namespace Ark::internal * @brief Create a new IRInliner * * @param debug debug level + * @param stats_collector optional statistics collector */ - explicit IRInliner(unsigned debug); + explicit IRInliner(unsigned debug, Statistics* stats_collector = nullptr); /** * @brief Attempt to inline IR blocks to avoid function calls when possible diff --git a/include/Ark/Compiler/IntermediateRepresentation/IROptimizer.hpp b/include/Ark/Compiler/IntermediateRepresentation/IROptimizer.hpp index 2ea96ee71..1afc4a553 100644 --- a/include/Ark/Compiler/IntermediateRepresentation/IROptimizer.hpp +++ b/include/Ark/Compiler/IntermediateRepresentation/IROptimizer.hpp @@ -35,8 +35,9 @@ namespace Ark::internal * @brief Create a new IROptimizer * * @param debug debug level + * @param stats_collector optional statistics collector */ - explicit IROptimizer(unsigned debug); + explicit IROptimizer(unsigned debug, Statistics* stats_collector = nullptr); /** * @brief Turn a given IR into bytecode diff --git a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp index ed79e1abb..052dbcaa3 100644 --- a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp +++ b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp @@ -52,8 +52,9 @@ namespace Ark::internal * @brief Construct a new ASTLowerer object * * @param debug the debug level + * @param stats_collector optional statistics collector */ - explicit ASTLowerer(unsigned debug); + explicit ASTLowerer(unsigned debug, Statistics* stats_collector = nullptr); /** * @brief Pre-fill tables (used by the debugger) diff --git a/include/Ark/Compiler/Macros/Processor.hpp b/include/Ark/Compiler/Macros/Processor.hpp index b7138e5b1..6521497f1 100644 --- a/include/Ark/Compiler/Macros/Processor.hpp +++ b/include/Ark/Compiler/Macros/Processor.hpp @@ -35,8 +35,9 @@ namespace Ark::internal * @brief Construct a new Macro Processor object * * @param debug the debug level + * @param stats_collector optional statistics collector */ - explicit MacroProcessor(unsigned debug) noexcept; + explicit MacroProcessor(unsigned debug, Statistics* stats_collector = nullptr) noexcept; /** * @brief Send the complete AST and work on it diff --git a/include/Ark/Compiler/NameResolution/NameResolutionPass.hpp b/include/Ark/Compiler/NameResolution/NameResolutionPass.hpp index cc122d660..0555e2609 100644 --- a/include/Ark/Compiler/NameResolution/NameResolutionPass.hpp +++ b/include/Ark/Compiler/NameResolution/NameResolutionPass.hpp @@ -27,18 +27,22 @@ namespace Ark::internal public: /** * @brief Create a NameResolutionPass + * * @param debug debug level + * @param stats_collector optional statistics collector */ - explicit NameResolutionPass(unsigned debug); + explicit NameResolutionPass(unsigned debug, Statistics* stats_collector = nullptr); /** * @brief Start visiting the given AST, checking for mutability violation and unbound variables + * * @param ast AST to analyze */ void process(const Node& ast); /** * @brief Unused overload that return the input AST (untouched as this pass only generates errors) + * * @return const Node& ast */ [[nodiscard]] const Node& ast() const noexcept; @@ -61,6 +65,7 @@ namespace Ark::internal /** * @brief Recursively visit nodes + * * @param node node to visit * @param register_declarations whether or not the visit should register declarations */ diff --git a/include/Ark/Compiler/Package/ImportSolver.hpp b/include/Ark/Compiler/Package/ImportSolver.hpp index 5ad076f58..50b3700ec 100644 --- a/include/Ark/Compiler/Package/ImportSolver.hpp +++ b/include/Ark/Compiler/Package/ImportSolver.hpp @@ -30,13 +30,16 @@ namespace Ark::internal public: /** * @brief Create a new ImportSolver + * * @param debug debug level * @param libenv list of paths to the standard library + * @param stats_collector optional statistics collector */ - ImportSolver(unsigned debug, const std::vector& libenv); + ImportSolver(unsigned debug, const std::vector& libenv, Statistics* stats_collector = nullptr); /** * @brief Configure the ImportSolver + * * @param root path to the root file that imports all others * @param origin_imports the first imports to go through * @return ImportSolver& *this diff --git a/include/Ark/Compiler/Pass.hpp b/include/Ark/Compiler/Pass.hpp index b1320465f..ebd50d9f7 100644 --- a/include/Ark/Compiler/Pass.hpp +++ b/include/Ark/Compiler/Pass.hpp @@ -12,8 +12,7 @@ #include #include - -#include +#include namespace Ark::internal { @@ -28,8 +27,9 @@ namespace Ark::internal * * @param name the pass name, used for logging * @param debug_level debug level + * @param stats_collector optional statistics collector */ - Pass(std::string name, unsigned debug_level); + Pass(std::string name, unsigned debug_level, Statistics* stats_collector = nullptr); virtual ~Pass() = default; @@ -42,6 +42,33 @@ namespace Ark::internal protected: Logger m_logger; + + /** + * @brief Register an event with the number of times it happened + * + * @param name + * @param quantity + */ + void addStat(Stats name, long quantity) const; + + /** + * @brief Increase the number of times an event happened by `delta` + * + * @param name + * @param delta default: 1 + */ + void statIncrementCount(Stats name, long delta = 1) const; + + /** + * @brief Register an event with the time it took + * + * @param name + * @param quantity + */ + void addStat(const std::string& name, std::chrono::nanoseconds quantity) const; + + private: + Statistics* m_stats { nullptr }; }; } diff --git a/include/Ark/Compiler/Statistics.hpp b/include/Ark/Compiler/Statistics.hpp new file mode 100644 index 000000000..6fad3850f --- /dev/null +++ b/include/Ark/Compiler/Statistics.hpp @@ -0,0 +1,65 @@ +#ifndef ARK_COMPILER_STATISTICS_HPP +#define ARK_COMPILER_STATISTICS_HPP + +#include +#include +#include + +#include + +namespace Ark::internal +{ + enum class Stats : unsigned + { +#define X(name) name, +#include "Statistics.x" + +#undef X + Count + }; + + constexpr std::array StatNames = { +#define X(name) #name, +#include "Statistics.x" + +#undef X + }; + + class ARK_API Statistics + { + public: + Statistics() = default; + + /** + * @brief Register an event with the number of times it happened + * + * @param name + * @param quantity + */ + void count(Stats name, long quantity); + + [[nodiscard]] long getCount(Stats name) const; + + /** + * @brief Register an event with the time it took + * + * @param name + * @param quantity + */ + void time(const std::string& name, std::chrono::nanoseconds quantity); + + [[nodiscard]] std::string asJson() const noexcept; + + private: + struct Measure + { + std::string name; + std::chrono::nanoseconds duration; + }; + + std::vector m_measures; + std::array(Stats::Count)> m_counts { 0 }; + }; +} + +#endif // ARK_COMPILER_STATISTICS_HPP diff --git a/include/Ark/Compiler/Statistics.x b/include/Ark/Compiler/Statistics.x new file mode 100644 index 000000000..9920302c5 --- /dev/null +++ b/include/Ark/Compiler/Statistics.x @@ -0,0 +1,7 @@ +X(ASTOptimizerPrunedNodes) +X(Deprecations) +X(InlinedCalls) +X(OptimisedInstructions) +X(ExpressionsCompiled) +X(FullyQualifiedNames) +X(ProcessedImports) diff --git a/include/Ark/Compiler/Welder.hpp b/include/Ark/Compiler/Welder.hpp index 04e86a93b..01605c33d 100644 --- a/include/Ark/Compiler/Welder.hpp +++ b/include/Ark/Compiler/Welder.hpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -111,6 +112,8 @@ namespace Ark [[nodiscard]] std::string textualIR() const noexcept; [[nodiscard]] const bytecode_t& bytecode() const noexcept; + friend class Ark::State; + private: std::vector m_lib_env; uint16_t m_features; @@ -120,6 +123,7 @@ namespace Ark std::vector m_ir; bytecode_t m_bytecode; internal::Node m_computed_ast; + internal::Statistics m_stats; internal::Parser m_parser; internal::ImportSolver m_import_solver; diff --git a/include/Ark/State.hpp b/include/Ark/State.hpp index 9822f72c1..8c9cd9dba 100644 --- a/include/Ark/State.hpp +++ b/include/Ark/State.hpp @@ -115,6 +115,13 @@ namespace Ark */ void setLibDirs(const std::vector& libenv) noexcept; + /** + * @brief Toggle compiler statistics gathering + * + * @param toggle + */ + void gatherStats(bool toggle) noexcept; + /** * @brief Reset State (all member variables related to execution) * @@ -161,6 +168,7 @@ namespace Ark unsigned m_debug_level; uint16_t m_features; + bool m_gather_stats; bytecode_t m_bytecode; std::vector m_libenv; diff --git a/include/Ark/Utils/Logger.hpp b/include/Ark/Utils/Logger.hpp index c987f81c4..fc3d9aa4f 100644 --- a/include/Ark/Utils/Logger.hpp +++ b/include/Ark/Utils/Logger.hpp @@ -112,7 +112,7 @@ namespace Ark::internal m_active_traces.push_back(trace_name); } - inline void traceEnd() + inline std::chrono::nanoseconds traceEnd() { std::string trace_name = m_active_traces.back(); m_active_traces.pop_back(); @@ -120,6 +120,8 @@ namespace Ark::internal const auto time = std::chrono::high_resolution_clock::now(); const std::chrono::duration ms_double = time - m_trace_starts[trace_name]; trace("{} took {:.3f}ms", trace_name, ms_double.count()); + + return time - m_trace_starts[trace_name]; } /** diff --git a/src/arkreactor/Compiler/AST/Optimizer.cpp b/src/arkreactor/Compiler/AST/Optimizer.cpp index b6157c682..f3a636cbb 100644 --- a/src/arkreactor/Compiler/AST/Optimizer.cpp +++ b/src/arkreactor/Compiler/AST/Optimizer.cpp @@ -4,8 +4,8 @@ namespace Ark::internal { - Optimizer::Optimizer(const unsigned debug) noexcept : - Pass("Optimizer", debug), m_ast() + Optimizer::Optimizer(const unsigned debug, Statistics* stats_collector) noexcept : + Pass("Optimizer", debug, stats_collector), m_ast() {} void Optimizer::process(const Node& ast) @@ -16,11 +16,15 @@ namespace Ark::internal m_ast = ast; m_logger.traceStart("process"); + m_logger.traceStart("countAndPruneDeadCode"); countAndPruneDeadCode(m_ast); + addStat("ASTOptimizer.countAndPruneDeadCode", m_logger.traceEnd()); + m_logger.traceStart("pruneUnusedGlobalVariables"); // logic: remove piece of code with only 1 reference, if they aren't function calls pruneUnusedGlobalVariables(m_ast); - m_logger.traceEnd(); + addStat("ASTOptimizer.pruneUnusedGlobalVariables", m_logger.traceEnd()); + addStat("ASTOptimizer.process", m_logger.traceEnd()); m_logger.debug("AST after name pruning nodes"); if (m_logger.shouldDebug()) @@ -59,7 +63,10 @@ namespace Ark::internal { // replace the node by an Unused, it is either a (while cond block) or (if cond then) if (node.constList().size() == 3) + { node = Node(NodeType::Unused); + statIncrementCount(Stats::ASTOptimizerPrunedNodes); + } else // it is a (if cond then else) { const auto back = node.constList().back(); @@ -70,8 +77,8 @@ namespace Ark::internal else if (keyword == Keyword::If && condition.nodeType() == NodeType::Symbol && condition.string() == "true") node = body; - // do not try to iterate on the child nodes as they do not exist anymore, - // we performed some optimization that squashed them. + // do not try to iterate on the child nodes as they do not exist any more, + // we performed some optimisation that squashed them. if (!node.isListLike()) return; } @@ -95,14 +102,9 @@ namespace Ark::internal // eliminate nested begin blocks if (kw == Keyword::Begin) - { pruneUnusedGlobalVariables(child); - // skip let/ mut detection - continue; - } - // check if it's a let/mut declaration and perform removal - if (kw == Keyword::Let || kw == Keyword::Mut) + else if (kw == Keyword::Let || kw == Keyword::Mut) { const std::string name = child.constList()[1].string(); // a variable was only declared and never used @@ -111,6 +113,7 @@ namespace Ark::internal m_logger.debug("Removing unused variable '{}'", name); // erase the node by turning it to an Unused node child = Node(NodeType::Unused); + statIncrementCount(Stats::ASTOptimizerPrunedNodes); } else if (child.comment().find("@deprecated") != std::string::npos) { @@ -135,6 +138,7 @@ namespace Ark::internal : "value", name, advice.empty() ? "" : " " + advice); + statIncrementCount(Stats::Deprecations); } } } diff --git a/src/arkreactor/Compiler/IntermediateRepresentation/IRCompiler.cpp b/src/arkreactor/Compiler/IntermediateRepresentation/IRCompiler.cpp index 68d3f9479..249b06900 100644 --- a/src/arkreactor/Compiler/IntermediateRepresentation/IRCompiler.cpp +++ b/src/arkreactor/Compiler/IntermediateRepresentation/IRCompiler.cpp @@ -17,8 +17,8 @@ namespace Ark::internal { using namespace literals; - IRCompiler::IRCompiler(const unsigned debug) : - Pass("IRCompiler", debug) + IRCompiler::IRCompiler(const unsigned debug, Statistics* stats_collector) : + Pass("IRCompiler", debug, stats_collector) {} void IRCompiler::process(const std::vector& pages, const std::vector& symbols, const std::vector& values) @@ -62,7 +62,7 @@ namespace Ark::internal picosha2::hash256(m_bytecode.begin() + bytecode::HeaderSize, m_bytecode.end(), hash_out); m_bytecode.insert(m_bytecode.begin() + bytecode::HeaderSize, hash_out.begin(), hash_out.end()); - m_logger.traceEnd(); + addStat("IRCompiler.process", m_logger.traceEnd()); } void IRCompiler::dumpToStream(std::ostream& stream) const diff --git a/src/arkreactor/Compiler/IntermediateRepresentation/IRInliner.cpp b/src/arkreactor/Compiler/IntermediateRepresentation/IRInliner.cpp index cde3f99fd..1640d6151 100644 --- a/src/arkreactor/Compiler/IntermediateRepresentation/IRInliner.cpp +++ b/src/arkreactor/Compiler/IntermediateRepresentation/IRInliner.cpp @@ -8,8 +8,8 @@ namespace Ark::internal { - IRInliner::IRInliner(const unsigned debug) : - Pass("IRInliner", debug), + IRInliner::IRInliner(const unsigned debug, Statistics* stats_collector) : + Pass("IRInliner", debug, stats_collector), m_current_label(0) {} @@ -20,7 +20,9 @@ namespace Ark::internal m_values = values; m_current_label = last_label + 1; + m_logger.traceStart("extractPagesMetadata"); extractPagesMetadata(pages); + addStat("IRInliner.extractPagesMetadata", m_logger.traceEnd()); // TODO: some pages could be removed if they are inlined everywhere! // TODO: we'll need to move some page index if a page is removed! @@ -69,6 +71,7 @@ namespace Ark::internal } inlineBlock(inlinee, new_block); + statIncrementCount(Stats::InlinedCalls); } else new_block.data.emplace_back(entity); @@ -77,7 +80,7 @@ namespace Ark::internal m_ir.emplace_back(new_block); } - m_logger.traceEnd(); + addStat("IRInliner.process", m_logger.traceEnd()); } const std::vector& IRInliner::intermediateRepresentation() const noexcept diff --git a/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp b/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp index 6e3fda240..8c0498f4b 100644 --- a/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp +++ b/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp @@ -19,8 +19,8 @@ namespace Ark::internal return IR::Entity(FUSED_MATH, e[0].inst(), e[1].inst(), NOP); } - IROptimizer::IROptimizer(const unsigned debug) : - Pass("IROptimizer", debug) + IROptimizer::IROptimizer(const unsigned debug, Statistics* stats_collector) : + Pass("IROptimizer", debug, stats_collector) { // TODO: we could add rules to optimize ( ) to have a precomputed value instead // TODO: same for ( ) @@ -285,6 +285,7 @@ namespace Ark::internal auto [entity, offset] = maybe_compacted.value(); current_block.emplace_back(entity); i += offset; + statIncrementCount(Stats::OptimisedInstructions); } else { @@ -294,7 +295,7 @@ namespace Ark::internal } } - m_logger.traceEnd(); + addStat("IROptimizer.process", m_logger.traceEnd()); } const std::vector& IROptimizer::intermediateRepresentation() const noexcept diff --git a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp index 44c503743..64b33477d 100644 --- a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp +++ b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp @@ -25,8 +25,8 @@ namespace Ark::internal Builtin }; - ASTLowerer::ASTLowerer(const unsigned debug) : - Pass("ASTLowerer", debug) + ASTLowerer::ASTLowerer(const unsigned debug, Statistics* stats_collector) : + Pass("ASTLowerer", debug, stats_collector) {} void ASTLowerer::addToTables(const std::vector& symbols, const std::vector& constants) @@ -52,7 +52,7 @@ namespace Ark::internal /* is_result_unused= */ true, /* is_terminal= */ false, /* can_use_ref= */ true); - m_logger.traceEnd(); + addStat("ASTLowerer.process", m_logger.traceEnd()); } const std::vector& ASTLowerer::intermediateRepresentation() const noexcept @@ -211,6 +211,8 @@ namespace Ark::internal void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal, const bool can_use_ref) { + statIncrementCount(Stats::ExpressionsCompiled); + // register symbols if (x.nodeType() == NodeType::Symbol) compileSymbol(x, p, is_result_unused, /* can_use_ref= */ can_use_ref); diff --git a/src/arkreactor/Compiler/Macros/Processor.cpp b/src/arkreactor/Compiler/Macros/Processor.cpp index aad090faa..cda6f22ed 100644 --- a/src/arkreactor/Compiler/Macros/Processor.cpp +++ b/src/arkreactor/Compiler/Macros/Processor.cpp @@ -17,8 +17,9 @@ namespace Ark::internal { - MacroProcessor::MacroProcessor(const unsigned debug) noexcept : - Pass("MacroProcessor", debug), m_genned_sym(0) + MacroProcessor::MacroProcessor(const unsigned debug, Statistics* stats_collector) noexcept : + Pass("MacroProcessor", debug, stats_collector), + m_genned_sym(0) { // create executors pipeline m_conditional_executor = std::make_shared(this); @@ -36,7 +37,7 @@ namespace Ark::internal m_ast = ast; processNode(m_ast, 0); - m_logger.traceEnd(); + addStat("MacroProcessor.process", m_logger.traceEnd()); m_logger.debug("AST after processing macros"); if (m_logger.shouldDebug()) m_ast.debugPrint(std::cout) << '\n'; diff --git a/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp b/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp index 62c4d6c00..43ee59d15 100644 --- a/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp +++ b/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp @@ -6,8 +6,8 @@ namespace Ark::internal { - NameResolutionPass::NameResolutionPass(const unsigned debug) : - Pass("NameResolution", debug) + NameResolutionPass::NameResolutionPass(const unsigned debug, Statistics* stats_collector) : + Pass("NameResolution", debug, stats_collector) { for (const auto& builtin : Builtins::builtins) m_language_symbols.emplace(builtin.first); @@ -31,7 +31,7 @@ namespace Ark::internal m_ast = ast; visit(m_ast, /* register_declarations= */ true); - m_logger.traceEnd(); + addStat("NameResolution.visit", m_logger.traceEnd()); m_logger.debug("AST after name resolution"); if (m_logger.shouldDebug()) @@ -39,7 +39,7 @@ namespace Ark::internal m_logger.traceStart("checkForUndefinedSymbol"); checkForUndefinedSymbol(); - m_logger.traceEnd(); + addStat("NameResolution.checkForUndefinedSymbol", m_logger.traceEnd()); } const Node& NameResolutionPass::ast() const noexcept @@ -77,6 +77,7 @@ namespace Ark::internal // in case of field, no need to check if we can fully qualify names child.setString(m_scope_resolver.getFullyQualifiedNameInNearestScope(old_name)); addSymbolNode(child, old_name); + statIncrementCount(Stats::FullyQualifiedNames); } else addSymbolNode(child); @@ -338,6 +339,7 @@ namespace Ark::internal } symbol.setString(fqn); + statIncrementCount(Stats::FullyQualifiedNames); return fqn; } diff --git a/src/arkreactor/Compiler/Package/ImportSolver.cpp b/src/arkreactor/Compiler/Package/ImportSolver.cpp index 35df1b24a..e3aa7c410 100644 --- a/src/arkreactor/Compiler/Package/ImportSolver.cpp +++ b/src/arkreactor/Compiler/Package/ImportSolver.cpp @@ -10,8 +10,11 @@ namespace Ark::internal { - ImportSolver::ImportSolver(const unsigned debug, const std::vector& libenv) : - Pass("ImportSolver", debug), m_debug_level(debug), m_libenv(libenv), m_ast() + ImportSolver::ImportSolver(const unsigned debug, const std::vector& libenv, Statistics* stats_collector) : + Pass("ImportSolver", debug, stats_collector), + m_debug_level(debug), + m_libenv(libenv), + m_ast() {} ImportSolver& ImportSolver::setup(const std::filesystem::path& root, const std::vector& origin_imports) @@ -59,9 +62,9 @@ namespace Ark::internal m_logger.traceStart("findAndReplaceImports"); m_ast = findAndReplaceImports(origin_ast).first; - m_logger.traceEnd(); + addStat("ImportSolver.findAndReplaceImports", m_logger.traceEnd()); - m_logger.traceEnd(); + addStat("ImportSolver.process", m_logger.traceEnd()); } std::pair ImportSolver::findAndReplaceImports(const Node& ast) @@ -85,6 +88,8 @@ namespace Ark::internal // if it wasn't imported already, register it if (std::ranges::find(m_imported, package) == m_imported.end()) { + statIncrementCount(Stats::ProcessedImports); + m_imported.push_back(package); // modules are already handled, we can safely replace the node x = m_packages[package].ast; @@ -176,7 +181,7 @@ namespace Ark::internal false }; - m_logger.traceEnd(); + addStat(fmt::format("ImportSolver.parseImport({})", import.toPackageString()), m_logger.traceEnd()); auto imports = parser.imports(); std::vector output; diff --git a/src/arkreactor/Compiler/Pass.cpp b/src/arkreactor/Compiler/Pass.cpp index bd3117947..d0f302648 100644 --- a/src/arkreactor/Compiler/Pass.cpp +++ b/src/arkreactor/Compiler/Pass.cpp @@ -3,12 +3,30 @@ namespace Ark::internal { - Pass::Pass(std::string name, const unsigned debug_level) : - m_logger(std::move(name), debug_level) + Pass::Pass(std::string name, const unsigned debug_level, Statistics* stats_collector) : + m_logger(std::move(name), debug_level), m_stats(stats_collector) {} void Pass::configureLogger(std::ostream& os) { m_logger.configureOutputStream(&os); } + + void Pass::statIncrementCount(const Stats name, const long delta) const + { + if (m_stats) + m_stats->count(name, m_stats->getCount(name) + delta); + } + + void Pass::addStat(const Stats name, const long quantity) const + { + if (m_stats) + m_stats->count(name, quantity); + } + + void Pass::addStat(const std::string& name, const std::chrono::nanoseconds quantity) const + { + if (m_stats) + m_stats->time(name, quantity); + } } diff --git a/src/arkreactor/Compiler/Statistics.cpp b/src/arkreactor/Compiler/Statistics.cpp new file mode 100644 index 000000000..57efe8559 --- /dev/null +++ b/src/arkreactor/Compiler/Statistics.cpp @@ -0,0 +1,42 @@ +#include + +#include + +namespace Ark::internal +{ + void Statistics::count(const Stats name, const long quantity) + { + m_counts[static_cast(name)] = quantity; + } + + long Statistics::getCount(const Stats name) const + { + return m_counts[static_cast(name)]; + } + + void Statistics::time(const std::string& name, const std::chrono::nanoseconds quantity) + { + m_measures.emplace_back(name, quantity); + } + + std::string Statistics::asJson() const noexcept + { + std::string timings; + for (const auto& [k, v] : m_measures) + { + const auto micros = std::chrono::duration_cast(v); + timings += fmt::format("{:?}: {},", k, micros.count()); + } + + std::string counts; + for (std::size_t i = 0; i < m_counts.size(); ++i) + { + const long x = m_counts[i]; + if (i > 0) + counts += ","; + counts += fmt::format("{:?}: {}", StatNames[i], x); + } + + return fmt::format(R"({{ "timings": {{ {} "unit": "us" }}, "counts": {{ {} }} }})", timings, counts); + } +} diff --git a/src/arkreactor/Compiler/Welder.cpp b/src/arkreactor/Compiler/Welder.cpp index 5eca1e0c0..8177382f4 100644 --- a/src/arkreactor/Compiler/Welder.cpp +++ b/src/arkreactor/Compiler/Welder.cpp @@ -21,15 +21,15 @@ namespace Ark m_features(features), m_computed_ast(internal::NodeType::Unused), m_parser(debug), - m_import_solver(debug, lib_env), - m_macro_processor(debug), - m_ast_optimizer(debug), - m_name_resolver(debug), + m_import_solver(debug, lib_env, &m_stats), + m_macro_processor(debug, &m_stats), + m_ast_optimizer(debug, &m_stats), + m_name_resolver(debug, &m_stats), m_logger("Welder", debug), - m_lowerer(debug), - m_ir_inliner(debug), - m_ir_optimizer(debug), - m_ir_compiler(debug) + m_lowerer(debug, &m_stats), + m_ir_inliner(debug, &m_stats), + m_ir_optimizer(debug, &m_stats), + m_ir_compiler(debug, &m_stats) {} void Welder::registerSymbol(const std::string& name) @@ -193,8 +193,10 @@ namespace Ark { try { + const auto t = std::chrono::high_resolution_clock::now(); m_parser.process(filename, code); m_computed_ast = m_parser.ast(); + m_stats.time(fmt::format("Parser.process({})", filename), std::chrono::high_resolution_clock::now() - t); if ((m_features & FeatureImportSolver) != 0) { diff --git a/src/arkreactor/State.cpp b/src/arkreactor/State.cpp index 111421e19..1af7c2110 100644 --- a/src/arkreactor/State.cpp +++ b/src/arkreactor/State.cpp @@ -19,6 +19,7 @@ namespace Ark State::State(const std::vector& libenv) noexcept : m_debug_level(0), m_features(0), + m_gather_stats(false), m_libenv(libenv), m_filename(ARK_NO_NAME_FILE), m_max_page_size(0) @@ -71,16 +72,30 @@ namespace Ark welder.registerSymbol(key); if (!welder.computeASTFromFile(file)) + { + if (m_gather_stats) + fmt::println("{}", welder.m_stats.asJson()); return false; + } if (!welder.generateBytecode()) + { + if (m_gather_stats) + fmt::println("{}", welder.m_stats.asJson()); return false; + } const std::string destination = output.empty() ? (file.substr(0, file.find_last_of('.')) + ".arkc") : output; if ((m_features & DisableCache) == 0 && !welder.saveBytecodeToFile(destination)) return false; if (!feed(welder.bytecode())) + { + if (m_gather_stats) + fmt::println("{}", welder.m_stats.asJson()); return false; + } + if (m_gather_stats) + fmt::println("{}", welder.m_stats.asJson()); return true; } @@ -138,9 +153,20 @@ namespace Ark welder.registerSymbol(p.first); if (!welder.computeASTFromString(code)) + { + if (m_gather_stats) + fmt::println("{}", welder.m_stats.asJson()); return false; + } if (!welder.generateBytecode()) + { + if (m_gather_stats) + fmt::println("{}", welder.m_stats.asJson()); return false; + } + + if (m_gather_stats) + fmt::println("{}", welder.m_stats.asJson()); return feed(welder.bytecode()); } @@ -169,6 +195,11 @@ namespace Ark m_libenv = libenv; } + void State::gatherStats(const bool toggle) noexcept + { + m_gather_stats = toggle; + } + void State::configure(const BytecodeReader& bcr) { using namespace internal; diff --git a/src/arkscript/main.cpp b/src/arkscript/main.cpp index f88842f98..0d371851c 100644 --- a/src/arkscript/main.cpp +++ b/src/arkscript/main.cpp @@ -51,6 +51,7 @@ int main(int argc, char** argv) // Eval / Run / AST dump std::string file, eval_expression; std::string libdir; + bool output_stats = false; // Formatting bool format_dry_run = false; bool format_check = false; @@ -116,6 +117,7 @@ int main(int argc, char** argv) , ( required("-c", "--compile").set(selected, mode::compile).doc("Compile the given program to bytecode, but do not run") & value("file", file).doc("If file is -, it reads code from stdin") + , option("--stats").set(output_stats, true).doc("Gather stats about each compiler pass and print them to stdout") ) | value("file", file).set(selected, mode::run) ) @@ -248,6 +250,7 @@ int main(int argc, char** argv) { Ark::State state(lib_paths); state.setDebug(debug); + state.gatherStats(output_stats); if (!state.doFile(file, passes)) return ArkErrorExitCode;