diff --git a/.gitignore b/.gitignore index a80088a57..4cacba7a4 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ include/Ark/Constants.hpp .cache/ build/ build_emscripten/ +cache-cppcheck/ ninja/ cmake-build-*/ out/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb6c8d8ec..a7914ef33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,8 +23,9 @@ repos: "--suppressions-list=cppcheck-suppressions.txt", "--check-level=exhaustive", "--force", - "-I include", - "-j", "8" + "-Iinclude", + "-j", "8", + "--cppcheck-build-dir=cache-cppcheck" ] - repo: https://github.com/compilerla/conventional-pre-commit rev: 'v3.2.0' diff --git a/.run/a.ark.run.xml b/.run/a.ark.run.xml index 0a85746a5..4ca054426 100644 --- a/.run/a.ark.run.xml +++ b/.run/a.ark.run.xml @@ -1,5 +1,5 @@ - + diff --git a/CHANGELOG.md b/CHANGELOG.md index edbcfc517..74de7964b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,17 @@ - list:intersection - list:difference - sys:version, has the version of the ArkScript VM +- IR inliner, running after the AST lowerer, before the IR optimiser +- new debugger command `scopes`, printing the last `n` scopes (default: 5) +- new CLI toggle `-f(no-)irinliner` to toggle the IR inliner pass (default: On) ### Changed - fix a bug related to recursive closures: once a closure was referenced in its own scope, we couldn't convert it to string or compare it against another closure/itself - bytecode reader: the length of code segments is counted in instructions, not bytes +- the compiler can track the origin of an IR block, in preparation for inlining +- `POP_SCOPE` can take an argument to have a mode: by default, `0`, only pop the current scope ; if `1`, materialise the top of the stack if it's a reference +- the debugger does not trace the instructions of the code being run inside it (only instructions from the script are traced) +- the AST lowerer checks for `(breakpoint)` at the end of `begin` nodes to determine if a node is unused or terminal ### Removed diff --git a/CMakeLists.txt b/CMakeLists.txt index e0b0ef511..6663bb373 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,9 @@ else () set(ARK_BUILD_DATE "2026-01-02T17:56:00Z") endif () +# needed so that cppcheck has a cache folder, when launched through pre-commit +file(MAKE_DIRECTORY cache-cppcheck) + option(ARK_BUILD_EXE "Build a standalone arkscript executable" Off) option(ARK_ENABLE_SYSTEM "Enable sys:exec" On) # enable use of (sys:exec "command here") option(ARK_STATIC "Build ArkScript statically" Off) diff --git a/include/Ark/Compiler/Instructions.hpp b/include/Ark/Compiler/Instructions.hpp index b7dd9365b..399dad718 100644 --- a/include/Ark/Compiler/Instructions.hpp +++ b/include/Ark/Compiler/Instructions.hpp @@ -171,6 +171,7 @@ namespace Ark::internal // @role Reset the current scope so that it is empty, and jump to a given location RESET_SCOPE_JUMP = 0x25, + // @args mode: if 1: materialise the top of the stack if it's a reference, else: just destroy the scope // @role Destroy the last local scope POP_SCOPE = 0x26, diff --git a/include/Ark/Compiler/IntermediateRepresentation/Entity.hpp b/include/Ark/Compiler/IntermediateRepresentation/Entity.hpp index 6f7bae73d..0f16bd53b 100644 --- a/include/Ark/Compiler/IntermediateRepresentation/Entity.hpp +++ b/include/Ark/Compiler/IntermediateRepresentation/Entity.hpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include #include @@ -37,61 +39,253 @@ namespace Ark::internal::IR constexpr uint16_t MaxValueForSmallNumber = 0x0800; static_assert(MaxValueForSmallNumber + MaxValueForSmallNumber - 1 == MaxValueForDualArg); + constexpr std::string_view AnonymousBlockName = "#anonymous"; + class Entity { public: + /** + * @brief Create a new IR Entity + * + * @param kind kind of entity (label, jump, instruction...) + */ explicit Entity(Kind kind); + /** + * @brief Create a new IR Entity + * + * @param inst instruction + * @param arg optional argument, default to 0 + */ explicit Entity(Instruction inst, uint16_t arg = 0); + /** + * @brief Create a new IR Entity + * + * @param inst instruction that takes two arguments + * @param primary_arg first argument on 12 bits + * @param secondary_arg second argument on 12 bits + */ Entity(Instruction inst, uint16_t primary_arg, uint16_t secondary_arg); + /** + * @brief Create a new IR Entity + * + * @param inst instruction that takes three arguments + * @param inst2 first argument on 8 bits + * @param inst3 second argument on 8 bits + * @param inst4 third argument on 8 bits + */ Entity(Instruction inst, uint8_t inst2, uint8_t inst3, uint8_t inst4); void replaceInstruction(Instruction replacement); + void replaceLabel(label_t replacement); + + /** + * @brief Create a new Label IR Entity + * + * @param value value for the label + * @return Entity + */ static Entity Label(label_t value); + /** + * @brief Create a new Goto IR Entity + * + * @param label label the goto relates to + * @param inst jump instruction to use, default to JUMP + * @return Entity + */ static Entity Goto(const Entity& label, Instruction inst = Instruction::JUMP); + /** + * @brief Create a new Goto IR Entity + * + * @param label label the goto relates to + * @param inst jump instruction to use + * @param primary_arg argument for the jump instruction + * @return Entity + */ static Entity GotoWithArg(const Entity& label, Instruction inst, uint16_t primary_arg); + /** + * @brief Create a new Goto IR Entity + * + * @param label label the goto relates to + * @param cond true to use POP_JUMP_IF_TRUE, false to use POP_JUMP_IF_FALSE + * @return Entity + */ static Entity GotoIf(const Entity& label, bool cond); + /** + * @brief Return the bytecode representation of the IR Entity if it's an Opcode + * + * @return Word + */ [[nodiscard]] Word bytecode() const; + /** + * @brief Check if the Entity has a label attached + * + * @return bool + */ + [[nodiscard]] bool hasLabel() const + { + switch (m_kind) + { + case Kind::Label: + [[fallthrough]]; + case Kind::Goto: + [[fallthrough]]; + case Kind::GotoWithArg: + return true; + + case Kind::Opcode: + [[fallthrough]]; + case Kind::Opcode2Args: + [[fallthrough]]; + case Kind::Opcode3Args: + return false; + } + + return false; + } + + /** + * @brief Return the label of the IR Entity + * + * @return label_t + */ [[nodiscard]] label_t label() const { return m_label; } + /** + * @brief Return the kind of IR Entity + * @see Kind + * @return Kind + */ [[nodiscard]] Kind kind() const { return m_kind; } + /** + * @brief Return the underlying instruction of the IR Entity + * + * @return Instruction + */ [[nodiscard]] Instruction inst() const { return m_inst; } + /** + * @brief Return the primary argument of the IR Entity (can be 0 if the argument isn't used) + * @details The argument is on 16 bits for standard instructions ; for super instructions, only the first 12 bits are used + * @return uint16_t + */ [[nodiscard]] uint16_t primaryArg() const { return m_primary_arg; } + /** + * @brief Return the second argument of the IR Entity + * @details The argument is for super instructions, where only the first 12 bits are used + * @return uint16_t + */ [[nodiscard]] uint16_t secondaryArg() const { return m_secondary_arg; } + /** + * @brief Return the third argument of the IR Entity + * @details The argument is for special super instructions, where only the first 8 bits are used (for all arguments) + * @return uint16_t + */ [[nodiscard]] uint16_t tertiaryArg() const { return m_tertiary_arg; } + /** + * @brief Set the source location for an IR Entity, which is used to generate the file loc table + * + * @param filename + * @param line + */ void setSourceLocation(const std::string& filename, std::size_t line); - [[nodiscard]] bool hasValidSourceLocation() const { return !m_source_file.empty(); } + void setRelatedResourceId(std::optional id); - [[nodiscard]] const std::string& filename() const { return m_source_file; } + [[nodiscard]] bool hasValidSourceLocation() const { return !m_metadata.source_file.empty(); } - [[nodiscard]] std::size_t sourceLine() const { return m_source_line; } + [[nodiscard]] const std::string& filename() const { return m_metadata.source_file; } + + [[nodiscard]] std::size_t sourceLine() const { return m_metadata.source_line; } + + /** + * @brief Return the related constant/symbol id an IR Entity refers to (only populated for LOAD_FAST_BY_INDEX, CALL_SYMBOL_BY_INDEX, CALL_SYMBOL, and CALL) + * + * @return std::optional + */ + [[nodiscard]] std::optional relatedResourceId() const { return m_metadata.related_res_id; } private: Kind m_kind; label_t m_label { 0 }; + Instruction m_inst { NOP }; uint16_t m_primary_arg { 0 }; uint16_t m_secondary_arg { 0 }; uint16_t m_tertiary_arg { 0 }; - std::string m_source_file; - std::size_t m_source_line { 0 }; + + struct + { + std::string source_file; + std::size_t source_line { 0 }; + std::optional related_res_id; ///< Used by a few instructions to know the original symbol/constant id and deoptimize when necessary + } m_metadata; }; - using Block = std::vector; + /** + * @brief Block of IR entities, with attached metadata + */ + struct Block + { + using vec_t = std::vector; + + struct Metadata + { + std::optional name; + std::size_t argument_count { 0 }; + std::size_t addr { 0 }; + bool is_closure { false }; + bool is_recursive { false }; + bool is_simple { false }; ///< Calls only builtin and operators, no user functions/C++ functions + bool is_mutating_args { false }; + } metadata; + vec_t data; + + [[nodiscard]] std::string debugName() const + { + return metadata.name.value_or(std::string(AnonymousBlockName)); + } + + [[nodiscard]] std::string metadataRepr() const + { + std::string flags; + if (metadata.is_recursive) + flags += "recursive"; + if (metadata.is_simple) + flags += std::string(flags.empty() ? "" : " ") + "simple"; + if (metadata.is_mutating_args) + flags += std::string(flags.empty() ? "" : " ") + "mutating"; + + if (metadata.is_closure) + flags += std::string(flags.empty() ? "" : " ") + "closure"; + else + flags += std::string(flags.empty() ? "" : " ") + "function"; + return flags; + } + + [[nodiscard]] std::size_t instructionCount() const + { + const auto length = std::ranges::count_if(data, [](const auto& a) { + return a.kind() != IR::Kind::Label; + }); + + if (length <= 0) + return 0; + return static_cast(length); + } + }; } #endif // ARK_COMPILER_INTERMEDIATEREPRESENTATION_ENTITY_HPP diff --git a/include/Ark/Compiler/IntermediateRepresentation/IRInliner.hpp b/include/Ark/Compiler/IntermediateRepresentation/IRInliner.hpp new file mode 100644 index 000000000..37042c9a8 --- /dev/null +++ b/include/Ark/Compiler/IntermediateRepresentation/IRInliner.hpp @@ -0,0 +1,137 @@ +/** + * @file IRInliner.hpp + * @author Lexy Plateau (lexplt.dev@gmail.com) + * @brief Try to inline IR blocks + * @date 2026-07-11 + * + * @copyright Copyright (c) 2026 + * + */ + +#ifndef ARK_COMPILER_INTERMEDIATEREPRESENTATION_IRINLINER_HPP +#define ARK_COMPILER_INTERMEDIATEREPRESENTATION_IRINLINER_HPP + +#include +#include +#include +#include + +#include +#include +#include + +namespace Ark::internal +{ + struct BlockInfo + { + long constant_id; + std::size_t addr; + std::string name; + std::optional symbol_id; + }; + + struct SymbolData + { + std::string name; + std::size_t declarations_count; + std::size_t use_count; + }; + + class ARK_API IRInliner final : public Pass + { + public: + /** + * @brief Create a new IRInliner + * + * @param debug debug level + */ + explicit IRInliner(unsigned debug); + + /** + * @brief Attempt to inline IR blocks to avoid function calls when possible + * + * @param pages list of lists of IR entities generated by the compiler + * @param symbols symbol table generated by the compiler + * @param values value table generated by the compiler + * @param last_label last label generated by the AST lowerer + */ + void process(const std::vector& pages, const std::vector& symbols, const std::vector& values, IR::label_t last_label); + + /** + * @brief Return the IR blocks (one per scope) + * + * @return const std::vector& + */ + [[nodiscard]] const std::vector& intermediateRepresentation() const noexcept; + + private: + std::vector m_ir; + std::vector m_symbols; + std::vector m_values; + std::vector m_funcs; + std::unordered_map m_symbols_data; + IR::label_t m_current_label; + + enum class CallKind + { + Symbol, + Constant + }; + + /** + * @brief Check if a block can be inlined in another one + * + * @param candidate block to inline + * @param source where the inlining will take place + * @param argc argument count in source to candidate + * @return true if the candidate can be inlined + */ + [[nodiscard]] static bool canBeInlined(const IR::Block& candidate, const IR::Block& source, std::size_t argc) noexcept; + + /** + * @brief See if a block can be inlined in the current call site + * + * @param kind kind of call instruction being used (by symbol or constant) + * @param pages + * @param maybe_id optional identifier of a block (constant id or symbol id) + * @param current current block where inlining could take place + * @param argc argument count in source to candidate + * @return std::optional candidate to inlining + */ + [[nodiscard]] std::optional blockToInlineInCall(CallKind kind, const std::vector& pages, std::optional maybe_id, const IR::Block& current, std::size_t argc) const noexcept; + + /** + * @brief Check if an IR block represents a builtin proxy, and return its CALL instruction if it is + * + * @param block IR block + * @return std::optional + */ + [[nodiscard]] static std::optional isBuiltinProxy(const IR::Block& block); + + /** + * @brief Perform the inlining + * + * @param inlinee IR block to inline + * @param destination new IR block to write instructions to + */ + void inlineBlock(const IR::Block& inlinee, IR::Block& destination); + + /** + * @brief Extract metadata from the IR entities pages, to have a name, constant id, and potentially symbol id per page + * + * @param pages pages of IR entities + */ + void extractPagesMetadata(const std::vector& pages); + + /** + * @brief Search for a block by one of its IDs + * + * @param kind kind of call instruction being used (by symbol or constant) + * @param id + * @return std::optional + */ + [[nodiscard]] std::optional findBlockBy(CallKind kind, uint16_t id) const noexcept; + }; +} + +#endif // ARK_COMPILER_INTERMEDIATEREPRESENTATION_IRINLINER_HPP diff --git a/include/Ark/Compiler/IntermediateRepresentation/IROptimizer.hpp b/include/Ark/Compiler/IntermediateRepresentation/IROptimizer.hpp index 4bfd41e5e..2ea96ee71 100644 --- a/include/Ark/Compiler/IntermediateRepresentation/IROptimizer.hpp +++ b/include/Ark/Compiler/IntermediateRepresentation/IROptimizer.hpp @@ -93,8 +93,8 @@ namespace Ark::internal std::vector m_values; [[nodiscard]] bool match(const std::vector& expected_insts, std::span entities) const; - [[nodiscard]] bool canBeOptimizedSafely(std::span entities, std::size_t window_size) const; - std::optional replaceWithRules(std::span entities, const std::size_t position_in_block); + [[nodiscard]] static bool canBeOptimisedSafely(std::span entities, std::size_t window_size, std::size_t position_in_block); + std::optional replaceWithRules(std::span entities, std::size_t position_in_block); [[nodiscard]] bool isPositiveNumberInlinable(uint16_t id) const; [[nodiscard]] bool isSmallerNumberInlinable(uint16_t id) const; diff --git a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp index 26c08889b..ed79e1abb 100644 --- a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp +++ b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp @@ -34,6 +34,13 @@ namespace Ark namespace Ark::internal { + struct PageCreationData + { + bool temp { false }; + bool closure { false }; + std::optional name { std::nullopt }; + }; + /** * @brief The ArkScript AST to IR compiler * @@ -91,6 +98,11 @@ namespace Ark::internal */ [[nodiscard]] const std::vector& values() const noexcept; + [[nodiscard]] IR::label_t lastLabel() const noexcept + { + return m_current_label; + } + private: struct Page { @@ -123,29 +135,43 @@ namespace Ark::internal InvalidNodeInTailCallNoReturnValue }; - Page createNewCodePage(const bool temp = false) noexcept + Page createNewCodePage(PageCreationData&& args = PageCreationData {}) noexcept { - if (!temp) + if (!args.temp) { - m_code_pages.emplace_back(); - return Page { .index = m_start_page_at_offset + m_code_pages.size() - 1u, .is_temp = false }; + const std::size_t new_page_addr = m_start_page_at_offset + m_code_pages.size(); + m_code_pages.emplace_back( + IR::Block::Metadata { + .name = args.name, + .argument_count = 0, + .addr = new_page_addr, + .is_closure = args.closure }, + IR::Block::vec_t {}); + return Page { .index = new_page_addr, .is_temp = false }; } m_temp_pages.emplace_back(); return Page { .index = m_temp_pages.size() - 1u, .is_temp = true }; } + IR::Block& block(const Page page) noexcept + { + if (!page.is_temp) + return m_code_pages[page.index - m_start_page_at_offset]; + return m_temp_pages[page.index]; + } + /** - * @brief helper functions to get a temp or finalized code page + * @brief helper functions to get a temp or finalised code page * * @param page page descriptor - * @return std::vector& + * @return std::vector& */ - IR::Block& page(const Page page) noexcept + IR::Block::vec_t& page(const Page page) noexcept { if (!page.is_temp) - return m_code_pages[page.index - m_start_page_at_offset]; - return m_temp_pages[page.index]; + return m_code_pages[page.index - m_start_page_at_offset].data; + return m_temp_pages[page.index].data; } /** @@ -264,6 +290,7 @@ namespace Ark::internal void compileApplyInstruction(Node& x, Page p, bool is_result_unused); void compileIf(Node& x, Page p, bool is_result_unused, bool is_terminal, bool can_use_ref); void compileFunction(Node& x, Page p, bool is_result_unused); + void setFunctionMetadata(Page p, std::size_t arg_count, bool mutates_args); void compileLetMutSet(Keyword n, Node& x, Page p, bool is_result_unused); void compileWhile(Node& x, Page p); void compilePluginImport(const Node& x, Page p); diff --git a/include/Ark/Compiler/Lowerer/LocalsLocator.hpp b/include/Ark/Compiler/Lowerer/LocalsLocator.hpp index 6972bd9de..c294795b9 100644 --- a/include/Ark/Compiler/Lowerer/LocalsLocator.hpp +++ b/include/Ark/Compiler/Lowerer/LocalsLocator.hpp @@ -70,7 +70,7 @@ namespace Ark::internal * @return true if vars were dropped, meaning that we created at least one variable in a branch * @return false if nothing was dropped */ - bool dropVarsForBranch(); + [[nodiscard]] bool dropVarsForBranch(); /** * @brief Mark the last variable of a scope as unreachable, blocking lookupLastScopeByName(..) from returning a value past that point diff --git a/include/Ark/Compiler/NameResolution/ScopeResolver.hpp b/include/Ark/Compiler/NameResolution/ScopeResolver.hpp index e8d63619d..0bd03dc4f 100644 --- a/include/Ark/Compiler/NameResolution/ScopeResolver.hpp +++ b/include/Ark/Compiler/NameResolution/ScopeResolver.hpp @@ -119,7 +119,7 @@ namespace Ark::internal private: std::vector> m_scopes; - std::string currentNamespace() const; + [[nodiscard]] std::string currentNamespace() const; }; } diff --git a/include/Ark/Compiler/ValTableElem.hpp b/include/Ark/Compiler/ValTableElem.hpp index 53f78c849..3851c35e4 100644 --- a/include/Ark/Compiler/ValTableElem.hpp +++ b/include/Ark/Compiler/ValTableElem.hpp @@ -19,7 +19,7 @@ namespace Ark::internal { /** - * @brief Enumeration to keep track of the type of a Compiler Value + * @brief Enumeration to keep track of the type of Compiler Value * */ enum class ValTableElemType diff --git a/include/Ark/Compiler/Welder.hpp b/include/Ark/Compiler/Welder.hpp index 49339e41c..c43471c7a 100644 --- a/include/Ark/Compiler/Welder.hpp +++ b/include/Ark/Compiler/Welder.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -128,6 +129,7 @@ namespace Ark internal::Logger m_logger; internal::ASTLowerer m_lowerer; + internal::IRInliner m_ir_inliner; internal::IROptimizer m_ir_optimizer; internal::IRCompiler m_ir_compiler; diff --git a/include/Ark/Constants.hpp.in b/include/Ark/Constants.hpp.in index 1976d1675..0558e35ee 100644 --- a/include/Ark/Constants.hpp.in +++ b/include/Ark/Constants.hpp.in @@ -59,11 +59,12 @@ namespace Ark // Compiler options constexpr uint16_t FeatureImportSolver = 1 << 0; constexpr uint16_t FeatureMacroProcessor = 1 << 1; - constexpr uint16_t FeatureASTOptimizer = 1 << 2; ///< Disabled by default because embedding ArkScript should not prune nodes from the AST ; it is active in the `arkscript` executable - constexpr uint16_t FeatureIROptimizer = 1 << 3; - constexpr uint16_t FeatureNameResolver = 1 << 4; - constexpr uint16_t FeatureVMDebugger = 1 << 5; ///< Disabled by default because embedding ArkScript should not launch the debugger on every error when running code - constexpr uint16_t DisableCache = 1 << 6; + constexpr uint16_t FeatureASTOptimiser = 1 << 2; ///< Disabled by default because embedding ArkScript should not prune nodes from the AST ; it is active in the `arkscript` executable + constexpr uint16_t FeatureIROptimiser = 1 << 3; + constexpr uint16_t FeatureIRInliner = 1 << 4; + constexpr uint16_t FeatureNameResolver = 1 << 5; + constexpr uint16_t FeatureVMDebugger = 1 << 6; ///< Disabled by default because embedding ArkScript should not launch the debugger on every error when running code + constexpr uint16_t DisableCache = 1 << 7; constexpr uint16_t FeatureDumpIR = 1 << 14; /// This feature should only be used in tests, to disable diagnostics generation and enable exceptions to be thrown @@ -73,7 +74,8 @@ namespace Ark constexpr uint16_t DefaultFeatures = FeatureImportSolver | FeatureMacroProcessor - | FeatureIROptimizer + | FeatureIRInliner + | FeatureIROptimiser | FeatureNameResolver; constexpr uint16_t MaxValue16Bits = std::numeric_limits::max(); diff --git a/include/Ark/VM/Debugger.hpp b/include/Ark/VM/Debugger.hpp index 0515675c5..fd75e0530 100644 --- a/include/Ark/VM/Debugger.hpp +++ b/include/Ark/VM/Debugger.hpp @@ -172,8 +172,11 @@ namespace Ark::internal void showContext(const VM& vm, const ExecutionContext& context) const; void showStack(VM& vm, const ExecutionContext& context, std::size_t count) const; void showLocals(VM& vm, ExecutionContext& context, std::size_t count) const; + void showScopes(VM& vm, ExecutionContext& context, std::size_t count) const; void showPreviousInstructions(const VM& vm, std::size_t count) const; + void showLocals(const ScopeView& scope, VM& vm, std::optional limit = std::nullopt) const; + std::optional prompt(std::size_t ip, std::size_t pp, VM& vm, ExecutionContext& context); /** diff --git a/lib/std b/lib/std index 7b6e61154..b5e2b4c44 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 7b6e61154b2ae021460c17a958ce31438cb9346d +Subproject commit b5e2b4c448a71dd5a5ddc7d4f6e3551ebf937add diff --git a/src/arkreactor/Compiler/IntermediateRepresentation/Entity.cpp b/src/arkreactor/Compiler/IntermediateRepresentation/Entity.cpp index 972de2e56..b0d82076a 100644 --- a/src/arkreactor/Compiler/IntermediateRepresentation/Entity.cpp +++ b/src/arkreactor/Compiler/IntermediateRepresentation/Entity.cpp @@ -27,6 +27,11 @@ namespace Ark::internal::IR m_inst = replacement; } + void Entity::replaceLabel(const label_t replacement) + { + m_label = replacement; + } + Entity Entity::Label(const label_t value) { auto entity = Entity(Kind::Label); @@ -74,9 +79,14 @@ namespace Ark::internal::IR return Word(0, 0); } - void Entity::setSourceLocation(const std::string& filename, std::size_t line) + void Entity::setSourceLocation(const std::string& filename, const std::size_t line) + { + m_metadata.source_file = filename; + m_metadata.source_line = line; + } + + void Entity::setRelatedResourceId(const std::optional id) { - m_source_file = filename; - m_source_line = line; + m_metadata.related_res_id = id; } } diff --git a/src/arkreactor/Compiler/IntermediateRepresentation/IRCompiler.cpp b/src/arkreactor/Compiler/IntermediateRepresentation/IRCompiler.cpp index 03fead37d..68d3f9479 100644 --- a/src/arkreactor/Compiler/IntermediateRepresentation/IRCompiler.cpp +++ b/src/arkreactor/Compiler/IntermediateRepresentation/IRCompiler.cpp @@ -31,7 +31,7 @@ namespace Ark::internal // compute a list of unique filenames for (const auto& page : pages) { - for (const auto& inst : page) + for (const auto& inst : page.data) { if (std::ranges::find(m_filenames, inst.filename()) == m_filenames.end() && inst.hasValidSourceLocation()) m_filenames.push_back(inst.filename()); @@ -70,8 +70,23 @@ namespace Ark::internal std::size_t index = 0; for (const auto& block : m_ir) { - fmt::println(stream, "page_{}", index); - for (const auto& entity : block) + if (index == 0) + // global scope + fmt::println(stream, "global"); + else + { + fmt::println( + stream, + "page_{} ({} ({} argument{}) {}, {} instructions)", + index, + block.debugName(), + block.metadata.argument_count, + block.metadata.argument_count == 1 ? "" : "s", + block.metadataRepr(), + block.data.size()); + } + + for (const auto& entity : block.data) { switch (entity.kind()) { @@ -119,14 +134,22 @@ namespace Ark::internal IR::Block& page = m_ir[i]; // just in case we got too far, always add a HALT to be sure the // VM won't do anything crazy - page.emplace_back(HALT); + page.data.emplace_back(HALT); // push number of elements - const auto page_size = std::ranges::count_if(page, [](const auto& a) { - return a.kind() != IR::Kind::Label; - }); + const std::size_t page_size = page.instructionCount(); if (std::cmp_greater(page_size, MaxValue16Bits)) - throw std::overflow_error(fmt::format("Size of page {} exceeds the maximum size of {}", i, MaxValue16Bits)); + { + std::string message; + if (i == 0) + message = fmt::format("Global scope exceeds the maximum number of instructions ({})", MaxValue16Bits); + else if (page.metadata.name.has_value()) + message = fmt::format("Function {} exceeds the maximum number of instructions ({})", page.metadata.name.value(), MaxValue16Bits); + else + message = fmt::format("Anonymous function at page {} exceeds the maximum number of instructions ({})", i, MaxValue16Bits); + + throw std::overflow_error(message); + } m_bytecode.push_back(CODE_SEGMENT_START); serializeOn2BytesToVecBE(page_size, m_bytecode); @@ -134,7 +157,7 @@ namespace Ark::internal // register labels position uint16_t pos = 0; std::unordered_map label_to_position; - for (auto& inst : page) + for (const auto& inst : page.data) { switch (inst.kind()) { @@ -147,7 +170,7 @@ namespace Ark::internal } } - for (auto& inst : page) + for (const auto& inst : page.data) { switch (inst.kind()) { @@ -304,7 +327,7 @@ namespace Ark::internal const auto& page = pages[i]; uint16_t ip = 0; - for (const auto& inst : page) + for (const auto& inst : page.data) { if (inst.hasValidSourceLocation()) { diff --git a/src/arkreactor/Compiler/IntermediateRepresentation/IRInliner.cpp b/src/arkreactor/Compiler/IntermediateRepresentation/IRInliner.cpp new file mode 100644 index 000000000..fa422152b --- /dev/null +++ b/src/arkreactor/Compiler/IntermediateRepresentation/IRInliner.cpp @@ -0,0 +1,361 @@ +#include + +#include +#include +#include + +namespace Ark::internal +{ + IRInliner::IRInliner(const unsigned debug) : + Pass("IRInliner", debug), + m_current_label(0) + {} + + void IRInliner::process(const std::vector& pages, const std::vector& symbols, const std::vector& values, const IR::label_t last_label) + { + m_logger.traceStart("process"); + m_symbols = symbols; + m_values = values; + m_current_label = last_label + 1; + + extractPagesMetadata(pages); + + // 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! + for (const auto& block : pages) + { + IR::Block new_block { + .metadata = { + .name = block.metadata.name, + .argument_count = block.metadata.argument_count, + .addr = block.metadata.addr, + .is_closure = block.metadata.is_closure, + .is_recursive = block.metadata.is_recursive, + .is_simple = block.metadata.is_simple }, + .data = {} + }; + + // We only have to deal with CALL_SYMBOL, CALL_SYMBOL_BY_INDEX, which deal with symbols, + // and CALL which can deal with constant ids (eg `((fun (a) (print a)) 5)`) + for (std::size_t i = 0, end = block.data.size(); i < end; ++i) + { + const auto& entity = block.data[i]; + + std::optional maybe_id; + std::size_t argc = std::numeric_limits::max(); + CallKind kind = CallKind::Symbol; + + if (entity.inst() == CALL_SYMBOL || entity.inst() == CALL_SYMBOL_BY_INDEX) + { + maybe_id = entity.relatedResourceId(); + argc = entity.secondaryArg(); + } + else if (entity.inst() == CALL) + { + maybe_id = entity.relatedResourceId(); + argc = entity.primaryArg(); + kind = CallKind::Constant; + } + + if (const auto maybe_block = blockToInlineInCall(kind, pages, maybe_id, block, argc); maybe_block.has_value()) + { + const IR::Block& inlinee = pages[maybe_block->addr]; + + // retrieve the return label of the call instruction, to know which PUSH_RETURN_ADDRESS instruction we'll have to remove + if (i + 1 < end) + { + assert(block.data[i + 1].kind() == IR::Kind::Label && "Expected a label right after the CALL instruction! The AST lowerer messed up somewhere"); + + const IR::label_t return_label = block.data[i + 1].label(); + const std::size_t removed = std::erase_if(new_block.data, [return_label](const IR::Entity& e) -> bool { + return e.kind() == IR::Kind::Goto && e.inst() == PUSH_RETURN_ADDRESS && e.label() == return_label; + }); + + if (removed == 0) + throw std::runtime_error(fmt::format("No PUSH_RETURN_ADDRESS L{} instruction removed, even though one was expected", return_label)); + } + + inlineBlock(inlinee, new_block); + } + else + new_block.data.emplace_back(entity); + } + + m_ir.emplace_back(new_block); + } + + m_logger.traceEnd(); + } + + const std::vector& IRInliner::intermediateRepresentation() const noexcept + { + return m_ir; + } + + bool IRInliner::canBeInlined(const IR::Block& candidate, const IR::Block& source, const std::size_t argc) noexcept + { + const std::size_t candidate_inst_count = candidate.instructionCount(), + source_inst_count = source.instructionCount(); + + if (candidate.metadata.is_closure || + candidate.metadata.is_recursive || + candidate.metadata.is_mutating_args || + candidate.metadata.argument_count != argc || + candidate.metadata.name.value_or(std::string(IR::AnonymousBlockName)) == IR::AnonymousBlockName || + std::cmp_greater_equal(candidate_inst_count + source_inst_count, MaxValue16Bits)) + return false; + // TODO: create a proper constant and make this less arbitrary? + return candidate.metadata.is_simple && candidate_inst_count < 24; + } + + std::optional IRInliner::blockToInlineInCall( + const CallKind kind, + const std::vector& pages, + const std::optional maybe_id, + const IR::Block& current, + const std::size_t argc) const noexcept + { + if (!maybe_id.has_value()) + return std::nullopt; + + const uint16_t id = maybe_id.value(); + std::optional maybe_block = findBlockBy(kind, id); + if (!maybe_block.has_value()) + return std::nullopt; + + // If we are trying to inline a function call that seems to have multiple declarations, + // abort. We can't be sure that we are inlining the correct version at the moment. + if (kind == CallKind::Symbol && m_symbols_data.contains(id) && m_symbols_data.at(id).declarations_count != 1) + return std::nullopt; + + const std::size_t block_addr = maybe_block->addr; + if (canBeInlined(pages[block_addr], current, argc)) + return maybe_block; + return std::nullopt; + } + + std::optional IRInliner::isBuiltinProxy(const IR::Block& block) + { + /* + Expected instructions to be a builtin proxy: + STORE... + PUSH_RETURN_ADDRESS + LOAD_FAST_BY_INDEX... + CALL_BUILTIN +