diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d6bf59c..74d974eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,8 @@ - `PUSH_RETURN_ADDRESS` instruction now replaces the VM auto push of IP/PP - remove the stack swapping by pushing arguments in the reverse order by which they are loaded - wasm export: we can now run ArkScript code on the web! +- `GET_CURRENT_PAGE_ADDRESS` instruction to push the current page address to the stack +- `CALL_CURRENT_PAGE` super instruction, calling the current page with a given number of arguments (avoid loading a page address on the stack, then popping it to perform the call) ### Changed - instructions are on 4 bytes: 1 byte for the instruction, 1 byte of padding, 2 bytes for an immediate argument @@ -141,6 +143,7 @@ - `io:removeFiles` is now `io:removeFile` and works on a single file/path - renamed almost all builtins to prefix them with `builtin__`, to have them proxied in the standard library (to be able to import and scope them properly) - new super instruction `CALL_BUILTIN_WITHOUT_RETURN_ADDRESS` to optimize the proxied builtins, skipping the return address deletion +- the VM no longer store a reference to the current function being called in the newly created scope ### Removed - removed unused `NodeType::Closure` diff --git a/include/Ark/Compiler/AST/Node.hpp b/include/Ark/Compiler/AST/Node.hpp index 98e5a175..be9e0dac 100644 --- a/include/Ark/Compiler/AST/Node.hpp +++ b/include/Ark/Compiler/AST/Node.hpp @@ -118,6 +118,13 @@ namespace Ark::internal */ [[nodiscard]] bool isStringLike() const noexcept; + /** + * @brief Check if the node is a function + * @return true if the node is a function declaration + * @return false + */ + [[nodiscard]] bool isFunction() const noexcept; + /** * @brief Copy a node to the current one, while keeping the filename and position in the file * @@ -174,6 +181,19 @@ namespace Ark::internal */ void setAltSyntax(bool toggle); + /** + * @brief Set the m_is_anonymous_function flag on the node + * @param anonymous true to mark the node as an anonymous function + */ + void setFunctionKind(bool anonymous); + + /** + * @brief Check if a node is an anonymous function + * @return true if the node is of an anonymous function + * @return false + */ + [[nodiscard]] bool isAnonymousFunction() const noexcept; + /** * @brief Check if a node is alt syntax * @return bool @@ -236,8 +256,9 @@ namespace Ark::internal std::size_t m_line = 0, m_col = 0; std::string m_filename; std::string m_comment; - std::string m_after_comment; ///< Comment after node - bool m_alt_syntax = false; ///< Used to tell if a node uses the alternative syntax (if available), eg (begin) / {}, (list) / [] + std::string m_after_comment; ///< Comment after node + bool m_alt_syntax = false; ///< Used to tell if a node uses the alternative syntax (if available), eg (begin) / {}, (list) / [] + bool m_is_anonymous_function = true; ///< Function nodes are marked as anonymous/non-anonymous by the ASTLowerer, to enable some optimisations }; const Node& getTrueNode(); diff --git a/include/Ark/Compiler/Instructions.hpp b/include/Ark/Compiler/Instructions.hpp index 98b8decf..483111f9 100644 --- a/include/Ark/Compiler/Instructions.hpp +++ b/include/Ark/Compiler/Instructions.hpp @@ -155,251 +155,259 @@ namespace Ark::internal // @role Destroy the last local scope POP_SCOPE = 0x21, - FIRST_OPERATOR = 0x22, + // @args symbol id (function name) + // @role Push the current page address as a value on the stack + GET_CURRENT_PAGE_ADDR = 0x22, + + FIRST_OPERATOR = 0x23, // @role Push #[code TS1 + TS] - ADD = 0x22, + ADD = 0x23, // @role Push #[code TS1 - TS] - SUB = 0x23, + SUB = 0x24, // @role Push #[code TS1 * TS] - MUL = 0x24, + MUL = 0x25, // @role Push #[code TS1 / TS] - DIV = 0x25, + DIV = 0x26, // @role Push #[code TS1 > TS] - GT = 0x26, + GT = 0x27, // @role Push #[code TS1 < TS] - LT = 0x27, + LT = 0x28, // @role Push #[code TS1 <= TS] - LE = 0x28, + LE = 0x29, // @role Push #[code TS1 >= TS] - GE = 0x29, + GE = 0x2a, // @role Push #[code TS1 != TS] - NEQ = 0x2a, + NEQ = 0x2b, // @role Push #[code TS1 == TS] - EQ = 0x2b, + EQ = 0x2c, // @role Push #[code len(TS)], TS must be a list - LEN = 0x2c, + LEN = 0x2d, // @role Push #[code empty?(TS)], TS must be a list or string - EMPTY = 0x2d, + EMPTY = 0x2e, // @role Push #[code tail(TS)], all the elements of TS except the first one. TS must be a list or string - TAIL = 0x2e, + TAIL = 0x2f, // @role Push #[code head(TS)], the first element of TS or nil if empty. TS must be a list or string - HEAD = 0x2f, + HEAD = 0x30, // @role Push true if TS is nil, false otherwise - ISNIL = 0x30, + ISNIL = 0x31, // @role Throw an exception if TS1 is false, and display TS (must be a string). Do not push anything on the stack - ASSERT = 0x31, + ASSERT = 0x32, // @role Convert TS to number (must be a string) - TO_NUM = 0x32, + TO_NUM = 0x33, // @role Convert TS to string - TO_STR = 0x33, + TO_STR = 0x34, // @role Push the value at index TS (must be a number) in TS1, which must be a list or string - AT = 0x34, + AT = 0x35, // @role Push the value at index TS (must be a number), inside the list or string at index TS1 (must be a number) in the list at TS2 - AT_AT = 0x35, + AT_AT = 0x36, // @role Push #[code TS1 % TS] - MOD = 0x36, + MOD = 0x37, // @role Push the type of TS as a string - TYPE = 0x37, + TYPE = 0x38, // @role Check if TS1 is a closure field of TS. TS must be a Closure, TS1 a String - HASFIELD = 0x38, + HASFIELD = 0x39, // @role Push #[code !TS] - NOT = 0x39, + NOT = 0x3a, // @args constant id, constant id // @role Load two consts (#[code primary] then #[code secondary]) on the stack in one instruction - LOAD_CONST_LOAD_CONST = 0x3a, + LOAD_CONST_LOAD_CONST = 0x3b, // @args constant id, symbol id // @role Load const #[code primary] into the symbol #[code secondary] (create a variable) - LOAD_CONST_STORE = 0x3b, + LOAD_CONST_STORE = 0x3c, // @args constant id, symbol id // @role Load const #[code primary] into the symbol #[code secondary] (search for the variable with the given symbol id) - LOAD_CONST_SET_VAL = 0x3c, + LOAD_CONST_SET_VAL = 0x3d, // @args symbol id, symbol id // @role Store the value of the symbol #[code primary] into a new variable #[code secondary] - STORE_FROM = 0x3d, + STORE_FROM = 0x3e, // @args symbol index, symbol id // @role Store the value of the symbol #[code primary] into a new variable #[code secondary] - STORE_FROM_INDEX = 0x3e, + STORE_FROM_INDEX = 0x3f, // @args symbol id, symbol id // @role Store the value of the symbol #[code primary] into an existing variable #[code secondary] - SET_VAL_FROM = 0x3f, + SET_VAL_FROM = 0x40, // @args symbol index, symbol id // @role Store the value of the symbol #[code primary] into an existing variable #[code secondary] - SET_VAL_FROM_INDEX = 0x40, + SET_VAL_FROM_INDEX = 0x41, // @args symbol id, count // @role Increment the variable #[code primary] by #[code count] and push its value on the stack - INCREMENT = 0x41, + INCREMENT = 0x42, // @args symbol index, count // @role Increment the variable #[code primary] by #[code count] and push its value on the stack - INCREMENT_BY_INDEX = 0x42, + INCREMENT_BY_INDEX = 0x43, // @args symbol id, count // @role Increment the variable #[code primary] by #[code count] and store its value in the given symbol id - INCREMENT_STORE = 0x43, + INCREMENT_STORE = 0x44, // @args symbol id, count // @role Decrement the variable #[code primary] by #[code count] and push its value on the stack - DECREMENT = 0x44, + DECREMENT = 0x45, // @args symbol index, count // @role Decrement the variable #[code primary] by #[code count] and push its value on the stack - DECREMENT_BY_INDEX = 0x45, + DECREMENT_BY_INDEX = 0x46, // @args symbol id, count // @role Decrement the variable #[code primary] by #[code count] and store its value in the given symbol id - DECREMENT_STORE = 0x46, + DECREMENT_STORE = 0x47, // @args symbol id, symbol id // @role Load the symbol #[code primary], compute its tail, store it in a new variable #[code secondary] - STORE_TAIL = 0x47, + STORE_TAIL = 0x48, // @args symbol index, symbol id // @role Load the symbol #[code primary], compute its tail, store it in a new variable #[code secondary] - STORE_TAIL_BY_INDEX = 0x48, + STORE_TAIL_BY_INDEX = 0x49, // @args symbol id, symbol id // @role Load the symbol #[code primary], compute its head, store it in a new variable #[code secondary] - STORE_HEAD = 0x49, + STORE_HEAD = 0x4a, // @args symbol index, symbol id // @role Load the symbol #[code primary], compute its head, store it in a new variable #[code secondary] - STORE_HEAD_BY_INDEX = 0x4a, + STORE_HEAD_BY_INDEX = 0x4b, // @args number, symbol id // @role Create a list of #[code number] elements, and store it in a new variable #[code secondary] - STORE_LIST = 0x4b, + STORE_LIST = 0x4c, // @args symbol id, symbol id // @role Load the symbol #[code primary], compute its tail, store it in an existing variable #[code secondary] - SET_VAL_TAIL = 0x4c, + SET_VAL_TAIL = 0x4d, // @args symbol index, symbol id // @role Load the symbol #[code primary], compute its tail, store it in an existing variable #[code secondary] - SET_VAL_TAIL_BY_INDEX = 0x4d, + SET_VAL_TAIL_BY_INDEX = 0x4e, // @args symbol id, symbol id // @role Load the symbol #[code primary], compute its head, store it in an existing variable #[code secondary] - SET_VAL_HEAD = 0x4e, + SET_VAL_HEAD = 0x4f, // @args symbol index, symbol id // @role Load the symbol #[code primary], compute its head, store it in an existing variable #[code secondary] - SET_VAL_HEAD_BY_INDEX = 0x4f, + SET_VAL_HEAD_BY_INDEX = 0x50, // @args builtin id, argument count // @role Call a builtin by its id in #[code primary], with #[code secondary] arguments. Bypass the stack size check because we do not push IP/PP since builtins calls do not alter the stack - CALL_BUILTIN = 0x50, + CALL_BUILTIN = 0x51, // @args builtin id, argument count // @role Call a builtin by its id in #[code primary], with #[code secondary] arguments. Bypass the stack size check because we do not push IP/PP since builtins calls do not alter the stack, as well as the return address removal - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS = 0x51, + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS = 0x52, // @args constant id, absolute address to jump to // @role Compare #[code TS < constant], if the comparison fails, jump to the given address. Otherwise, does nothing - LT_CONST_JUMP_IF_FALSE = 0x52, + LT_CONST_JUMP_IF_FALSE = 0x53, // @args constant id, absolute address to jump to // @role Compare #[code TS < constant], if the comparison succeeds, jump to the given address. Otherwise, does nothing - LT_CONST_JUMP_IF_TRUE = 0x53, + LT_CONST_JUMP_IF_TRUE = 0x54, // @args symbol id, absolute address to jump to // @role Compare #[code TS < symbol], if the comparison fails, jump to the given address. Otherwise, does nothing - LT_SYM_JUMP_IF_FALSE = 0x54, + LT_SYM_JUMP_IF_FALSE = 0x55, // @args constant id, absolute address to jump to // @role Compare #[code TS > constant], if the comparison succeeds, jump to the given address. Otherwise, does nothing - GT_CONST_JUMP_IF_TRUE = 0x55, + GT_CONST_JUMP_IF_TRUE = 0x56, // @args constant id, absolute address to jump to // @role Compare #[code TS > constant], if the comparison fails, jump to the given address. Otherwise, does nothing - GT_CONST_JUMP_IF_FALSE = 0x56, + GT_CONST_JUMP_IF_FALSE = 0x57, // @args symbol id, absolute address to jump to // @role Compare #[code TS > symbol], if the comparison fails, jump to the given address. Otherwise, does nothing - GT_SYM_JUMP_IF_FALSE = 0x57, + GT_SYM_JUMP_IF_FALSE = 0x58, // @args constant id, absolute address to jump to // @role Compare #[code TS == constant], if the comparison succeeds, jump to the given address. Otherwise, does nothing - EQ_CONST_JUMP_IF_TRUE = 0x58, + EQ_CONST_JUMP_IF_TRUE = 0x59, // @args symbol index, absolute address to jump to // @role Compare #[code TS == symbol], if the comparison succeeds, jump to the given address. Otherwise, does nothing - EQ_SYM_INDEX_JUMP_IF_TRUE = 0x59, + EQ_SYM_INDEX_JUMP_IF_TRUE = 0x5a, // @args constant id, absolute address to jump to // @role Compare #[code TS != constant], if the comparison succeeds, jump to the given address. Otherwise, does nothing - NEQ_CONST_JUMP_IF_TRUE = 0x5a, + NEQ_CONST_JUMP_IF_TRUE = 0x5b, // @args symbol id, absolute address to jump to // @role Compare #[code TS != symbol], if the comparison fails, jump to the given address. Otherwise, does nothing - NEQ_SYM_JUMP_IF_FALSE = 0x5b, + NEQ_SYM_JUMP_IF_FALSE = 0x5c, // @args symbol id, argument count // @role Call a symbol by its id in #[code primary], with #[code secondary] arguments - CALL_SYMBOL = 0x5c, + CALL_SYMBOL = 0x5d, + + // @args symbol id (function name), argument count + // @role Call the current page with #[code secondary] arguments + CALL_CURRENT_PAGE = 0x5e, // @args symbol id, field id in symbols table // @role Push the field of a given symbol (which has to be a closure) on the stack - GET_FIELD_FROM_SYMBOL = 0x5d, + GET_FIELD_FROM_SYMBOL = 0x5f, // @args symbol index, field id in symbols table // @role Push the field of a given symbol (which has to be a closure) on the stack - GET_FIELD_FROM_SYMBOL_INDEX = 0x5e, + GET_FIELD_FROM_SYMBOL_INDEX = 0x60, // @args symbol id, symbol id2 // @role Push symbol[symbol2] - AT_SYM_SYM = 0x5f, + AT_SYM_SYM = 0x61, // @args symbol index, symbol index2 // @role Push symbol[symbol2] - AT_SYM_INDEX_SYM_INDEX = 0x60, + AT_SYM_INDEX_SYM_INDEX = 0x62, // @args symbol id, constant id // @role Check that the type of symbol is the given constant, push true if so, false otherwise - CHECK_TYPE_OF = 0x61, + CHECK_TYPE_OF = 0x63, // @args symbol index, constant id // @role Check that the type of symbol is the given constant, push true if so, false otherwise - CHECK_TYPE_OF_BY_INDEX = 0x62, + CHECK_TYPE_OF_BY_INDEX = 0x64, // @args symbol id, number of elements // @role Append N elements to a reference to a list (symbol id), the list is being mutated in-place, no new object created. Elements are stored in TS(1)..TS(N). Follows the function calling convention - APPEND_IN_PLACE_SYM = 0x63, + APPEND_IN_PLACE_SYM = 0x65, // @args symbol index, number of elements // @role Append N elements to a reference to a list (symbol index), the list is being mutated in-place, no new object created. Elements are stored in TS(1)..TS(N). Follows the function calling convention - APPEND_IN_PLACE_SYM_INDEX = 0x64, + APPEND_IN_PLACE_SYM_INDEX = 0x66, InstructionsCount }; @@ -439,6 +447,7 @@ namespace Ark::internal "CREATE_SCOPE", "RESET_SCOPE_JUMP", "POP_SCOPE", + "GET_CURRENT_PAGE_ADDR", // operators "ADD", "SUB", @@ -500,6 +509,7 @@ namespace Ark::internal "NEQ_CONST_JUMP_IF_TRUE", "NEQ_SYM_JUMP_IF_FALSE", "CALL_SYMBOL", + "CALL_CURRENT_PAGE", "GET_FIELD_FROM_SYMBOL", "GET_FIELD_FROM_SYMBOL_INDEX", "AT_SYM_SYM", diff --git a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp index c919c9aa..3ef10f6f 100644 --- a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp +++ b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp @@ -11,6 +11,7 @@ #ifndef ARK_COMPILER_LOWERER_ASTLOWERER_HPP #define ARK_COMPILER_LOWERER_ASTLOWERER_HPP +#include #include #include #include @@ -48,7 +49,7 @@ namespace Ark::internal * * @param ast */ - void process(const Node& ast); + void process(Node& ast); /** * @brief Return the IR blocks (one per scope) @@ -86,6 +87,7 @@ namespace Ark::internal std::vector m_code_pages; std::vector m_temp_pages; ///< we need temporary code pages for some compilations passes IR::label_t m_current_label = 0; + std::stack m_opened_vars; ///< stack of vars we are currently declaring Logger m_logger; @@ -175,19 +177,18 @@ namespace Ark::internal * @param p the current page number we're on * @param is_result_unused * @param is_terminal - * @param var_name - */ - void compileExpression(const Node& x, Page p, bool is_result_unused, bool is_terminal, const std::string& var_name = ""); - - void compileSymbol(const Node& x, Page p, bool is_result_unused); - void compileListInstruction(const Node& c0, const Node& x, Page p, bool is_result_unused); - void compileIf(const Node& x, Page p, bool is_result_unused, bool is_terminal, const std::string& var_name); - void compileFunction(const Node& x, Page p, bool is_result_unused, const std::string& var_name); - void compileLetMutSet(Keyword n, const Node& x, Page p); - void compileWhile(const Node& x, Page p); - void compilePluginImport(const Node& x, Page p); - void pushFunctionCallArguments(const Node& call, Page p, bool is_tail_call); - void handleCalls(const Node& x, Page p, bool is_result_unused, bool is_terminal, const std::string& var_name); + */ + void compileExpression(Node& x, Page p, bool is_result_unused, bool is_terminal); + + void compileSymbol(Node& x, Page p, bool is_result_unused); + void compileListInstruction(Node& x, Page p, bool is_result_unused); + void compileIf(Node& x, Page p, bool is_result_unused, bool is_terminal); + void compileFunction(Node& x, Page p, bool is_result_unused); + void compileLetMutSet(Keyword n, Node& x, Page p); + void compileWhile(Node& x, Page p); + void compilePluginImport(Node& x, Page p); + void pushFunctionCallArguments(Node& call, Page p, bool is_tail_call); + void handleCalls(Node& x, Page p, bool is_result_unused, bool is_terminal); /** * @brief Register a given node in the symbol table diff --git a/include/Ark/VM/VM.hpp b/include/Ark/VM/VM.hpp index 3d4c586d..49655603 100644 --- a/include/Ark/VM/VM.hpp +++ b/include/Ark/VM/VM.hpp @@ -371,9 +371,10 @@ namespace Ark * * @param context * @param argc number of arguments already sent - * @param function_ptr optional pointer to the function to call. If not provided, obtain it from the stack + * @param function_ptr optional pointer to the function to call. If not provided, obtain it from the stack (unless or_address is not 0) + * @param or_address optional page address, used if non-zero and function_ptr is nullptr */ - inline void call(internal::ExecutionContext& context, uint16_t argc, Value* function_ptr = nullptr); + inline void call(internal::ExecutionContext& context, uint16_t argc, Value* function_ptr = nullptr, internal::PageAddr_t or_address = 0); /** * @brief Builtin called when the CALL_BUILTIN instruction is met in the bytecode diff --git a/include/Ark/VM/VM.inl b/include/Ark/VM/VM.inl index d3cbf1ea..71bd16f2 100644 --- a/include/Ark/VM/VM.inl +++ b/include/Ark/VM/VM.inl @@ -225,7 +225,7 @@ inline void VM::returnFromFuncCall(internal::ExecutionContext& context) context.locals.pop_back(); } -inline void VM::call(internal::ExecutionContext& context, const uint16_t argc, Value* function_ptr) +inline void VM::call(internal::ExecutionContext& context, const uint16_t argc, Value* function_ptr, internal::PageAddr_t or_address) { /* Argument: number of arguments when calling the function @@ -243,30 +243,41 @@ inline void VM::call(internal::ExecutionContext& context, const uint16_t argc, V "Maximum recursion depth exceeded. You could consider rewriting your function `{}' to make use of tail-call optimization.", m_state.m_symbols[context.last_symbol])); - Value function = function_ptr == nullptr ? *popAndResolveAsPtr(context) : *function_ptr; + ValueType call_type; + Value* maybe_value_ptr = nullptr; + if (function_ptr == nullptr && or_address == 0) + maybe_value_ptr = popAndResolveAsPtr(context); + else if (or_address != 0) + call_type = ValueType::PageAddr; + else + maybe_value_ptr = function_ptr; + + if (maybe_value_ptr != nullptr) + { + call_type = maybe_value_ptr->valueType(); + if (call_type == ValueType::PageAddr) + or_address = maybe_value_ptr->pageAddr(); + } + context.stacked_closure_scopes.emplace_back(nullptr); - switch (function.valueType()) + switch (call_type) { // is it a builtin function name? case ValueType::CProc: { - callBuiltin(context, function, argc); + callBuiltin(context, *maybe_value_ptr, argc); return; } // is it a user defined function? case ValueType::PageAddr: { - const PageAddr_t new_page_pointer = function.pageAddr(); + const PageAddr_t new_page_pointer = or_address; // create dedicated scope context.locals.emplace_back(context.scopes_storage.data(), context.locals.back().storageEnd()); - - // store "reference" to the function to speed the recursive functions - if (context.last_symbol < m_state.m_symbols.size()) [[likely]] - context.locals.back().push_back(context.last_symbol, function); - + // set up pointers (frame counter, page, instruction) context.fc++; context.pp = new_page_pointer; context.ip = 0; @@ -276,7 +287,7 @@ inline void VM::call(internal::ExecutionContext& context, const uint16_t argc, V // is it a user defined closure? case ValueType::Closure: { - const Closure& c = function.closure(); + const Closure& c = maybe_value_ptr->closure(); const PageAddr_t new_page_pointer = c.pageAddr(); // create dedicated scope @@ -297,7 +308,7 @@ inline void VM::call(internal::ExecutionContext& context, const uint16_t argc, V ErrorKind::Type, fmt::format( "{} is not a Function but a {}", - function.toString(*this), types_to_str[static_cast(function.valueType())])); + maybe_value_ptr->toString(*this), types_to_str[static_cast(call_type)])); } } diff --git a/src/arkreactor/Compiler/AST/Node.cpp b/src/arkreactor/Compiler/AST/Node.cpp index 674ec7d9..2518a87e 100644 --- a/src/arkreactor/Compiler/AST/Node.cpp +++ b/src/arkreactor/Compiler/AST/Node.cpp @@ -90,6 +90,14 @@ namespace Ark::internal return m_type == NodeType::Symbol || m_type == NodeType::String || m_type == NodeType::Spread; } + bool Node::isFunction() const noexcept + { + return m_type == NodeType::List && + !constList().empty() && + constList()[0].nodeType() == NodeType::Keyword && + constList()[0].keyword() == Keyword::Fun; + } + void Node::updateValueAndType(const Node& source) noexcept { m_type = source.m_type; @@ -138,6 +146,16 @@ namespace Ark::internal m_alt_syntax = toggle; } + void Node::setFunctionKind(bool anonymous) + { + m_is_anonymous_function = anonymous; + } + + bool Node::isAnonymousFunction() const noexcept + { + return m_is_anonymous_function; + } + bool Node::isAltSyntax() const { return m_alt_syntax; diff --git a/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp b/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp index 4e2c7947..9b2185b2 100644 --- a/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp +++ b/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp @@ -42,6 +42,7 @@ namespace Ark::internal return Builtins::builtins[entities[0].primaryArg()].second.isFunction(); } }, Rule { { LOAD_SYMBOL, CALL }, CALL_SYMBOL }, + Rule { { GET_CURRENT_PAGE_ADDR, CALL }, CALL_CURRENT_PAGE }, Rule { { LOAD_SYMBOL, GET_FIELD }, GET_FIELD_FROM_SYMBOL }, Rule { { LOAD_SYMBOL_BY_INDEX, GET_FIELD }, GET_FIELD_FROM_SYMBOL_INDEX }, Rule { { LIST, STORE }, STORE_LIST }, diff --git a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp index a5aef72d..22f2a4d4 100644 --- a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp +++ b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp @@ -19,7 +19,7 @@ namespace Ark::internal m_logger("ASTLowerer", debug) {} - void ASTLowerer::process(const Node& ast) + void ASTLowerer::process(Node& ast) { m_logger.traceStart("process"); m_code_pages.emplace_back(); // create empty page @@ -135,7 +135,7 @@ namespace Ark::internal throw CodeError(message, CodeErrorContext(node.filename(), node.line(), node.col(), node.repr())); } - void ASTLowerer::compileExpression(const Node& x, const Page p, const bool is_result_unused, const bool is_terminal, const std::string& var_name) + void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal) { // register symbols if (x.nodeType() == NodeType::Symbol) @@ -143,7 +143,7 @@ namespace Ark::internal else if (x.nodeType() == NodeType::Field) { // the parser guarantees us that there is at least 2 elements (eg: a.b) - compileSymbol(x.constList()[0], p, is_result_unused); + compileSymbol(x.list()[0], p, is_result_unused); for (auto it = x.constList().begin() + 1, end = x.constList().end(); it != end; ++it) { uint16_t i = addSymbol(*it); @@ -161,7 +161,7 @@ namespace Ark::internal } // namespace nodes else if (x.nodeType() == NodeType::Namespace) - compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal, var_name); + compileExpression(*x.constArkNamespace().ast, p, is_result_unused, is_terminal); else if (x.nodeType() == NodeType::Unused) { // do nothing, explicitly @@ -176,15 +176,15 @@ namespace Ark::internal } } // list instructions - else if (const auto c0 = x.constList()[0]; c0.nodeType() == NodeType::Symbol && getListInstruction(c0.string()).has_value()) - compileListInstruction(c0, x, p, is_result_unused); + else if (const auto head = x.constList()[0]; head.nodeType() == NodeType::Symbol && getListInstruction(head.string()).has_value()) + compileListInstruction(x, p, is_result_unused); // registering structures else if (x.constList()[0].nodeType() == NodeType::Keyword) { switch (const Keyword keyword = x.constList()[0].keyword()) { case Keyword::If: - compileIf(x, p, is_result_unused, is_terminal, var_name); + compileIf(x, p, is_result_unused, is_terminal); break; case Keyword::Set: @@ -196,20 +196,19 @@ namespace Ark::internal break; case Keyword::Fun: - compileFunction(x, p, is_result_unused, var_name); + compileFunction(x, p, is_result_unused); break; case Keyword::Begin: { - for (std::size_t i = 1, size = x.constList().size(); i < size; ++i) + for (std::size_t i = 1, size = x.list().size(); i < size; ++i) compileExpression( - x.constList()[i], + x.list()[i], p, // All the nodes in a begin (except for the last one) are producing a result that we want to drop. (i != size - 1) || is_result_unused, // If the begin is a terminal node, only its last node is terminal. - is_terminal && (i == size - 1), - var_name); + is_terminal && (i == size - 1)); break; } @@ -229,9 +228,9 @@ namespace Ark::internal } else if (x.nodeType() == NodeType::List) { - // if we are here, we should have a function name - // push arguments first, then function name, then call it - handleCalls(x, p, is_result_unused, is_terminal, var_name); + // If we are here, we should have a function name via the m_opened_vars. + // Push arguments first, then function name, then call it. + handleCalls(x, p, is_result_unused, is_terminal); } else buildAndThrowError( @@ -241,7 +240,7 @@ namespace Ark::internal x); } - void ASTLowerer::compileSymbol(const Node& x, const Page p, const bool is_result_unused) + void ASTLowerer::compileSymbol(Node& x, const Page p, const bool is_result_unused) { const std::string& name = x.string(); @@ -265,27 +264,28 @@ namespace Ark::internal } } - void ASTLowerer::compileListInstruction(const Node& c0, const Node& x, const Page p, const bool is_result_unused) + void ASTLowerer::compileListInstruction(Node& x, const Page p, const bool is_result_unused) { - std::string name = c0.string(); + const Node head = x.constList()[0]; + std::string name = x.constList()[0].string(); Instruction inst = getListInstruction(name).value(); // length of at least 1 since we got a symbol name const auto argc = x.constList().size() - 1u; // error, can not use append/concat/pop (and their in place versions) with a <2 length argument list if (argc < 2 && APPEND <= inst && inst <= POP) - buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), c0); + buildAndThrowError(fmt::format("Can not use {} with less than 2 arguments", name), head); if (inst <= POP && std::cmp_greater(argc, std::numeric_limits::max())) buildAndThrowError(fmt::format("Too many arguments ({}), exceeds 65'535", argc), x); if (argc != 3 && inst == SET_AT_INDEX) - buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), c0); + buildAndThrowError(fmt::format("Expected 3 arguments (list, index, value) for {}, got {}", name, argc), head); if (argc != 4 && inst == SET_AT_2_INDEX) - buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), c0); + buildAndThrowError(fmt::format("Expected 4 arguments (list, y, x, value) for {}, got {}", name, argc), head); // compile arguments in reverse order for (std::size_t i = x.constList().size() - 1u; i > 0; --i) { - const auto node = x.constList()[i]; + Node& node = x.list()[i]; if (nodeProducesOutput(node)) compileExpression(node, p, false, false); else @@ -316,7 +316,7 @@ namespace Ark::internal break; } page(p).emplace_back(inst, static_cast(inst_argc)); - page(p).back().setSourceLocation(c0.filename(), c0.line()); + page(p).back().setSourceLocation(head.filename(), head.line()); if (is_result_unused && name.back() != '!' && inst <= POP_LIST_IN_PLACE) // in-place functions never push a value { @@ -325,10 +325,10 @@ namespace Ark::internal } } - void ASTLowerer::compileIf(const Node& x, const Page p, const bool is_result_unused, const bool is_terminal, const std::string& var_name) + void ASTLowerer::compileIf(Node& x, const Page p, const bool is_result_unused, const bool is_terminal) { // compile condition - compileExpression(x.constList()[1], p, false, false); + compileExpression(x.list()[1], p, false, false); page(p).back().setSourceLocation(x.constList()[1].filename(), x.constList()[1].line()); // jump only if needed to the "true" branch @@ -339,7 +339,7 @@ namespace Ark::internal if (x.constList().size() == 4) // we have an else clause { m_locals_locator.saveScopeLengthForBranch(); - compileExpression(x.constList()[3], p, is_result_unused, is_terminal, var_name); + compileExpression(x.list()[3], p, is_result_unused, is_terminal); page(p).back().setSourceLocation(x.constList()[3].filename(), x.constList()[3].line()); m_locals_locator.dropVarsForBranch(); } @@ -352,14 +352,14 @@ namespace Ark::internal page(p).emplace_back(label_then); // if code m_locals_locator.saveScopeLengthForBranch(); - compileExpression(x.constList()[2], p, is_result_unused, is_terminal, var_name); + compileExpression(x.list()[2], p, is_result_unused, is_terminal); page(p).back().setSourceLocation(x.constList()[2].filename(), x.constList()[2].line()); m_locals_locator.dropVarsForBranch(); // set jump to end pos page(p).emplace_back(label_end); } - void ASTLowerer::compileFunction(const Node& x, const Page p, const bool is_result_unused, const std::string& var_name) + void ASTLowerer::compileFunction(Node& x, const Page p, const bool is_result_unused) { if (const auto args = x.constList()[1]; args.nodeType() != NodeType::List) buildAndThrowError(fmt::format("Expected a well formed argument(s) list, got a {}", typeToString(args)), args); @@ -399,8 +399,16 @@ namespace Ark::internal } } + // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls. + // This way we can continue to safely apply optimisations on + // (let name (fun (e) (map lst (fun (e) (name e))))) + // Otherwise, `name` would have been optimized to a GET_CURRENT_PAGE_ADDRESS, which would have returned the wrong page. + if (x.isAnonymousFunction()) + m_opened_vars.push("#anonymous"); // push body of the function - compileExpression(x.constList()[2], function_body_page, false, true, var_name); + compileExpression(x.list()[2], function_body_page, false, true); + if (x.isAnonymousFunction()) + m_opened_vars.pop(); // return last value on the stack page(function_body_page).emplace_back(RET); @@ -414,7 +422,7 @@ namespace Ark::internal } } - void ASTLowerer::compileLetMutSet(const Keyword n, const Node& x, const Page p) + void ASTLowerer::compileLetMutSet(const Keyword n, Node& x, const Page p) { if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol) buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym); @@ -424,10 +432,17 @@ namespace Ark::internal const std::string name = x.constList()[1].string(); uint16_t i = addSymbol(x.constList()[1]); + const bool is_function = x.constList()[2].isFunction(); + if (is_function) + { + m_opened_vars.push(name); + x.list()[2].setFunctionKind(/* anonymous= */ false); + } + // put value before symbol id // starting at index = 2 because x is a (let|mut|set variable ...) node for (std::size_t idx = 2, end = x.constList().size(); idx < end; ++idx) - compileExpression(x.constList()[idx], p, false, false, name); + compileExpression(x.list()[idx], p, false, false); if (n == Keyword::Let || n == Keyword::Mut) { @@ -437,10 +452,12 @@ namespace Ark::internal else page(p).emplace_back(SET_VAL, i); + if (is_function) + m_opened_vars.pop(); page(p).back().setSourceLocation(x.filename(), x.line()); } - void ASTLowerer::compileWhile(const Node& x, const Page p) + void ASTLowerer::compileWhile(Node& x, const Page p) { if (x.constList().size() != 3) buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x); @@ -453,12 +470,12 @@ namespace Ark::internal const auto label_loop = IR::Entity::Label(m_current_label++); page(p).emplace_back(label_loop); // push condition - compileExpression(x.constList()[1], p, false, false); + compileExpression(x.list()[1], p, false, false); // absolute jump to end of block if condition is false const auto label_end = IR::Entity::Label(m_current_label++); page(p).emplace_back(IR::Entity::GotoIf(label_end, false)); // push code to page - compileExpression(x.constList()[2], p, true, false); + compileExpression(x.list()[2], p, true, false); // reset the scope at the end of the loop so that indices are still valid // otherwise, (while true { (let a 5) (print a) (let b 6) (print b) }) @@ -473,7 +490,7 @@ namespace Ark::internal m_locals_locator.deleteScope(); } - void ASTLowerer::compilePluginImport(const Node& x, const Page p) + void ASTLowerer::compilePluginImport(Node& x, const Page p) { std::string path; const Node package_node = x.constList()[1]; @@ -492,7 +509,7 @@ namespace Ark::internal page(p).back().setSourceLocation(x.filename(), x.line()); } - void ASTLowerer::pushFunctionCallArguments(const Node& call, const Page p, const bool is_tail_call) + void ASTLowerer::pushFunctionCallArguments(Node& call, const Page p, const bool is_tail_call) { const auto node = call.constList()[0]; @@ -503,7 +520,7 @@ namespace Ark::internal // Eg (let foo (fun (a b c) (if (> a 0) (foo (- a 1) c (+ b c)) 1))) (foo 12 0 1) // On the second self-call, b and c would have the same value, since we set c to (+ b c), and we pushed c as the // value for argument b, but loaded it as a reference. - for (auto&& value : std::ranges::drop_view(call.constList(), 1) | std::views::reverse) + for (Node& value : std::ranges::drop_view(call.list(), 1) | std::views::reverse) { if (nodeProducesOutput(value)) compileExpression(value, p, false, false); @@ -519,11 +536,11 @@ namespace Ark::internal } } - void ASTLowerer::handleCalls(const Node& x, const Page p, bool is_result_unused, const bool is_terminal, const std::string& var_name) + void ASTLowerer::handleCalls(Node& x, const Page p, bool is_result_unused, const bool is_terminal) { constexpr std::size_t start_index = 1; - const auto node = x.constList()[0]; + Node& node = x.list()[0]; const std::optional maybe_operator = node.nodeType() == NodeType::Symbol ? getOperator(node.string()) : std::nullopt; const std::optional maybe_shortcircuit = @@ -546,7 +563,7 @@ namespace Ark::internal x.constList().size() - 1), x); - compileExpression(x.constList()[1], p, false, false); + compileExpression(x.list()[1], p, false, false); const auto label_shortcircuit = IR::Entity::Label(m_current_label++); auto shortcircuit_entity = IR::Entity::Goto(label_shortcircuit, maybe_shortcircuit.value()); @@ -554,7 +571,7 @@ namespace Ark::internal for (std::size_t i = 2, end = x.constList().size(); i < end; ++i) { - compileExpression(x.constList()[i], p, false, false); + compileExpression(x.list()[i], p, false, false); if (i + 1 != end) page(p).emplace_back(shortcircuit_entity); } @@ -563,7 +580,7 @@ namespace Ark::internal } else if (!maybe_operator.has_value()) { - if (is_terminal && x.constList()[0].nodeType() == NodeType::Symbol && var_name == x.constList()[0].string()) + if (is_terminal && node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string()) { pushFunctionCallArguments(x, p, /* is_tail_call= */ true); @@ -576,8 +593,23 @@ namespace Ark::internal { m_temp_pages.emplace_back(); const auto proc_page = Page { .index = m_temp_pages.size() - 1u, .is_temp = true }; - // closure chains have been handled (eg: closure.field.field.function) - compileExpression(node, proc_page, false, false); // storing proc + + // compile the function resolution to a separate page + if (node.nodeType() == NodeType::Symbol && !m_opened_vars.empty() && m_opened_vars.top() == node.string()) + { + // The function is trying to call itself, but this isn't a tail call. + // We can skip the LOAD_SYMBOL function_name and directly push the current + // function page, which will be quicker than a local variable resolution. + // We set its argument to the symbol id of the function we are calling, + // so that the VM knowns the name of the last called function. + page(proc_page).emplace_back(GET_CURRENT_PAGE_ADDR, addSymbol(node)); + } + else + { + // closure chains have been handled (eg: closure.field.field.function) + compileExpression(node, proc_page, false, false); // storing proc + } + if (m_temp_pages.back().empty()) buildAndThrowError(fmt::format("Can not call {}", x.constList()[0].repr()), x); @@ -618,7 +650,7 @@ namespace Ark::internal for (std::size_t index = start_index, size = x.constList().size(); index < size; ++index) { if (nodeProducesOutput(x.constList()[index])) - compileExpression(x.constList()[index], p, false, false); + compileExpression(x.list()[index], p, false, false); else buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]); diff --git a/src/arkreactor/VM/VM.cpp b/src/arkreactor/VM/VM.cpp index 988dfc6c..4d80b137 100644 --- a/src/arkreactor/VM/VM.cpp +++ b/src/arkreactor/VM/VM.cpp @@ -520,6 +520,7 @@ namespace Ark &&TARGET_CREATE_SCOPE, &&TARGET_RESET_SCOPE_JUMP, &&TARGET_POP_SCOPE, + &&TARGET_GET_CURRENT_PAGE_ADDR, &&TARGET_ADD, &&TARGET_SUB, &&TARGET_MUL, @@ -579,6 +580,7 @@ namespace Ark &&TARGET_NEQ_CONST_JUMP_IF_TRUE, &&TARGET_NEQ_SYM_JUMP_IF_FALSE, &&TARGET_CALL_SYMBOL, + &&TARGET_CALL_CURRENT_PAGE, &&TARGET_GET_FIELD_FROM_SYMBOL, &&TARGET_GET_FIELD_FROM_SYMBOL_INDEX, &&TARGET_AT_SYM_SYM, @@ -1054,6 +1056,13 @@ namespace Ark DISPATCH(); } + TARGET(GET_CURRENT_PAGE_ADDR) + { + context.last_symbol = arg; + push(Value(static_cast(context.pp)), context); + DISPATCH(); + } + #pragma endregion #pragma region "Operators" @@ -1764,6 +1773,16 @@ namespace Ark DISPATCH(); } + TARGET(CALL_CURRENT_PAGE) + { + UNPACK_ARGS(); + context.last_symbol = primary_arg; + call(context, secondary_arg, /* function_ptr= */ nullptr, /* or_address= */ static_cast(context.pp)); + if (!m_running) + GOTO_HALT(); + DISPATCH(); + } + TARGET(GET_FIELD_FROM_SYMBOL) { UNPACK_ARGS(); diff --git a/tests/benchmarks/results/0-684ea758.csv b/tests/benchmarks/results/000-684ea758.csv similarity index 100% rename from tests/benchmarks/results/0-684ea758.csv rename to tests/benchmarks/results/000-684ea758.csv diff --git a/tests/benchmarks/results/1-d45d7ea1.csv b/tests/benchmarks/results/001-d45d7ea1.csv similarity index 100% rename from tests/benchmarks/results/1-d45d7ea1.csv rename to tests/benchmarks/results/001-d45d7ea1.csv diff --git a/tests/benchmarks/results/2-abb043b4.csv b/tests/benchmarks/results/002-abb043b4.csv similarity index 100% rename from tests/benchmarks/results/2-abb043b4.csv rename to tests/benchmarks/results/002-abb043b4.csv diff --git a/tests/benchmarks/results/3-75161de7.csv b/tests/benchmarks/results/003-75161de7.csv similarity index 100% rename from tests/benchmarks/results/3-75161de7.csv rename to tests/benchmarks/results/003-75161de7.csv diff --git a/tests/benchmarks/results/4-ad889963.csv b/tests/benchmarks/results/004-ad889963.csv similarity index 100% rename from tests/benchmarks/results/4-ad889963.csv rename to tests/benchmarks/results/004-ad889963.csv diff --git a/tests/benchmarks/results/5-57d0e0cd.csv b/tests/benchmarks/results/005-57d0e0cd.csv similarity index 100% rename from tests/benchmarks/results/5-57d0e0cd.csv rename to tests/benchmarks/results/005-57d0e0cd.csv diff --git a/tests/benchmarks/results/6-c7f632ff.csv b/tests/benchmarks/results/006-c7f632ff.csv similarity index 100% rename from tests/benchmarks/results/6-c7f632ff.csv rename to tests/benchmarks/results/006-c7f632ff.csv diff --git a/tests/benchmarks/results/7-28999c0f.csv b/tests/benchmarks/results/007-28999c0f.csv similarity index 100% rename from tests/benchmarks/results/7-28999c0f.csv rename to tests/benchmarks/results/007-28999c0f.csv diff --git a/tests/benchmarks/results/8-eb9ada44.csv b/tests/benchmarks/results/008-eb9ada44.csv similarity index 100% rename from tests/benchmarks/results/8-eb9ada44.csv rename to tests/benchmarks/results/008-eb9ada44.csv diff --git a/tests/benchmarks/results/9-d1be6b9f.csv b/tests/benchmarks/results/009-d1be6b9f.csv similarity index 100% rename from tests/benchmarks/results/9-d1be6b9f.csv rename to tests/benchmarks/results/009-d1be6b9f.csv diff --git a/tests/benchmarks/results/10-eaf4ada5.csv b/tests/benchmarks/results/010-eaf4ada5.csv similarity index 100% rename from tests/benchmarks/results/10-eaf4ada5.csv rename to tests/benchmarks/results/010-eaf4ada5.csv diff --git a/tests/benchmarks/results/11-eaf4ada5.csv b/tests/benchmarks/results/011-eaf4ada5.csv similarity index 100% rename from tests/benchmarks/results/11-eaf4ada5.csv rename to tests/benchmarks/results/011-eaf4ada5.csv diff --git a/tests/unittests/resources/CompilerSuite/ir/ackermann.expected b/tests/unittests/resources/CompilerSuite/ir/ackermann.expected index cbe8de45..7759fc53 100644 --- a/tests/unittests/resources/CompilerSuite/ir/ackermann.expected +++ b/tests/unittests/resources/CompilerSuite/ir/ackermann.expected @@ -35,7 +35,7 @@ page_1 LOAD_CONST 2 SUB 0 LOAD_SYMBOL_BY_INDEX 1 - LOAD_SYMBOL 0 + GET_CURRENT_PAGE_ADDR 0 CALL 2 .L3: LOAD_SYMBOL_BY_INDEX 1 diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/ackermann.expected b/tests/unittests/resources/CompilerSuite/optimized_ir/ackermann.expected index 9ed66806..1f9e6da5 100644 --- a/tests/unittests/resources/CompilerSuite/optimized_ir/ackermann.expected +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/ackermann.expected @@ -24,7 +24,7 @@ page_1 PUSH_RETURN_ADDRESS L3 DECREMENT_BY_INDEX 0, 1 LOAD_SYMBOL_BY_INDEX 1 - CALL_SYMBOL 0, 2 + CALL_CURRENT_PAGE 0, 2 .L3: DECREMENT_BY_INDEX 1, 1 JUMP 0 diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/recursion_depth.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/recursion_depth.expected index a7c0df76..2f78d0b8 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/recursion_depth.expected +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/recursion_depth.expected @@ -13,5 +13,4 @@ In file tests/unittests/resources/DiagnosticsSuite/runtime/recursion_depth.ark:3 [ 0] In global scope (tests/unittests/resources/DiagnosticsSuite/runtime/recursion_depth.ark:7) Current scope variables values: -foo = Function@1 a = 2045 diff --git a/tools/ark_benchmarks b/tools/ark_benchmarks index a0482cb6..1a9206aa 100755 --- a/tools/ark_benchmarks +++ b/tools/ark_benchmarks @@ -1,6 +1,6 @@ #!/usr/bin/env bash -result="tests/benchmarks/results/$(set -- tests/benchmarks/results/*.csv; echo $#)-$(git rev-parse --short HEAD).csv" +result="tests/benchmarks/results/$(set -- tests/benchmarks/results/*.csv; printf "%03u" $#)-$(git rev-parse --short HEAD).csv" cmake-build-release/bench \ --benchmark_min_warmup_time=1 \ --benchmark_format=csv \