From aac4738a3d0b745a0dcf0cd62255b7a782f32ebf Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Fri, 5 Dec 2025 13:07:01 +0100 Subject: [PATCH 1/9] chore: update stdlib and tests, adding list:sortByKey --- lib/std | 2 +- .../runtime/backtrace_builtin.expected | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/std b/lib/std index 0734436be..9add02797 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 0734436be5de2a7bdfe141f3b07b7c4f131b7ccc +Subproject commit 9add02797a063203d61c9f86b30f9ed954913df1 diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.expected index 109484e65..f011910db 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.expected +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.expected @@ -1,16 +1,16 @@ Function list:sort expected 1 argument -> list (List) was of type String -In file /lib/std/List.ark:40 - 37 | # (list:sort [4 2 3]) # [1 2 4] - 38 | # =end - 39 | # @author https://github.com/SuperFola - 40 | (let sort (fun (_L) (builtin__list:sort _L))) +In file /lib/std/List.ark:51 + 48 | # (list:sort [4 2 3]) # [1 2 4] + 49 | # =end + 50 | # @author https://github.com/SuperFola + 51 | (let sort (fun (_L) (builtin__list:sort _L))) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 41 | - 42 | # @brief Generate a List of n copies of an element + 52 | + 53 | # @brief Generate a List of n copies of an element -[ 2] In function `list:sort' (/lib/std/List.ark:40) +[ 2] In function `list:sort' (/lib/std/List.ark:51) [ 1] In global scope (tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.ark:3) Current scope variables values: From c70cf2695afcf3d2484ca53190149740e0efc592 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Fri, 5 Dec 2025 13:07:35 +0100 Subject: [PATCH 2/9] feat(parser): adding parsing of arguments' attributes in function declarations --- include/Ark/Compiler/Common.hpp | 6 +++- src/arkreactor/Compiler/AST/Node.cpp | 16 ++++++++++ src/arkreactor/Compiler/AST/Parser.cpp | 29 +++++++++++++++++++ .../success/fun_arg_attributes.ark | 8 +++++ .../success/fun_arg_attributes.expected | 8 +++++ 5 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/resources/ParserSuite/success/fun_arg_attributes.ark create mode 100644 tests/unittests/resources/ParserSuite/success/fun_arg_attributes.expected diff --git a/include/Ark/Compiler/Common.hpp b/include/Ark/Compiler/Common.hpp index f3d382f8b..5eeb3d3ae 100644 --- a/include/Ark/Compiler/Common.hpp +++ b/include/Ark/Compiler/Common.hpp @@ -43,6 +43,8 @@ namespace Ark::internal enum class NodeType { Symbol, + MutArg, + RefArg, Capture, Keyword, String, @@ -56,8 +58,10 @@ namespace Ark::internal }; /// Node types as string, in the same order as the enum NodeType - constexpr std::array nodeTypes = { + constexpr std::array nodeTypes = { "Symbol", + "MutArg", + "RefArg", "Capture", "Keyword", "String", diff --git a/src/arkreactor/Compiler/AST/Node.cpp b/src/arkreactor/Compiler/AST/Node.cpp index b71691a9b..5216bfd68 100644 --- a/src/arkreactor/Compiler/AST/Node.cpp +++ b/src/arkreactor/Compiler/AST/Node.cpp @@ -185,6 +185,14 @@ namespace Ark::internal data += string(); break; + case NodeType::MutArg: + data += "(mut " + string() + ")"; + break; + + case NodeType::RefArg: + data += "(ref " + string() + ")"; + break; + case NodeType::Capture: data += "&" + string(); break; @@ -284,6 +292,14 @@ namespace Ark::internal os << "Symbol:" << string(); break; + case NodeType::MutArg: + os << "MutArg:" << string(); + break; + + case NodeType::RefArg: + os << "RefArg:" << string(); + break; + case NodeType::Capture: os << "Capture:" << string(); break; diff --git a/src/arkreactor/Compiler/AST/Parser.cpp b/src/arkreactor/Compiler/AST/Parser.cpp index 28d15bd3d..ecc982bf4 100644 --- a/src/arkreactor/Compiler/AST/Parser.cpp +++ b/src/arkreactor/Compiler/AST/Parser.cpp @@ -477,6 +477,35 @@ namespace Ark::internal args->push_back(positioned(Node(NodeType::Capture, capture), pos)); } + else if (accept(IsChar('('))) + { + // attribute modifiers: mut, ref + std::string modifier; + std::ignore = newlineOrComment(); + if (!oneOf({ "mut", "ref" }, &modifier)) + // We cannot return an error like this: + // error("Expected an attribute modifier, either `mut' or `ref'", pos); + // Because it would break on macro instantiations like (fun ((suffix-dup a 3)) ()) + return std::nullopt; + + NodeType type = NodeType::Unused; + if (modifier == "mut") + type = NodeType::MutArg; + else if (modifier == "ref") + type = NodeType::RefArg; + + Node arg_with_attr = Node(type); + std::string comment2 = newlineOrComment(); + arg_with_attr.attachCommentAfter(comment2); + + std::string symbol_name; + if (!name(&symbol_name)) + error(fmt::format("Expected a symbol name for the attribute with modifier `{}'", modifier), pos); + arg_with_attr.setString(symbol_name); + + args->push_back(positioned(arg_with_attr, pos)); + expect(IsChar(')')); + } else { std::string symbol_name; diff --git a/tests/unittests/resources/ParserSuite/success/fun_arg_attributes.ark b/tests/unittests/resources/ParserSuite/success/fun_arg_attributes.ark new file mode 100644 index 000000000..6778c3794 --- /dev/null +++ b/tests/unittests/resources/ParserSuite/success/fun_arg_attributes.ark @@ -0,0 +1,8 @@ +(fun (a (mut b) (ref c)) ()) +(fun ((mut b) (ref c)) ()) +(fun ((ref c)) ()) +(fun ((mut c)) ()) +(fun (a &cap (mut b) (ref c)) ()) +(fun (&cap (mut b) (ref c)) ()) +(fun (&cap (ref c)) ()) +(fun (&cap (mut c)) ()) diff --git a/tests/unittests/resources/ParserSuite/success/fun_arg_attributes.expected b/tests/unittests/resources/ParserSuite/success/fun_arg_attributes.expected new file mode 100644 index 000000000..c2ac8c0d8 --- /dev/null +++ b/tests/unittests/resources/ParserSuite/success/fun_arg_attributes.expected @@ -0,0 +1,8 @@ +( Keyword:Fun ( Symbol:a MutArg:b RefArg:c ) Symbol:nil ) +( Keyword:Fun ( MutArg:b RefArg:c ) Symbol:nil ) +( Keyword:Fun ( RefArg:c ) Symbol:nil ) +( Keyword:Fun ( MutArg:c ) Symbol:nil ) +( Keyword:Fun ( Symbol:a Capture:cap MutArg:b RefArg:c ) Symbol:nil ) +( Keyword:Fun ( Capture:cap MutArg:b RefArg:c ) Symbol:nil ) +( Keyword:Fun ( Capture:cap RefArg:c ) Symbol:nil ) +( Keyword:Fun ( Capture:cap MutArg:c ) Symbol:nil ) From 441c51977cf0bfa1d97b2f0bb2dd8b93ed01f787 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Fri, 5 Dec 2025 14:22:34 +0100 Subject: [PATCH 3/9] feat(formatter): adding formatting test for attribute modifiers --- src/arkreactor/Compiler/AST/Parser.cpp | 1 + src/arkreactor/Compiler/Macros/Processor.cpp | 2 ++ src/arkscript/Formatter.cpp | 6 ++++++ .../unittests/resources/FormatterSuite/functions.ark | 11 +++++++++++ .../resources/FormatterSuite/functions.expected | 7 +++++++ 5 files changed, 27 insertions(+) diff --git a/src/arkreactor/Compiler/AST/Parser.cpp b/src/arkreactor/Compiler/AST/Parser.cpp index ecc982bf4..570f4445e 100644 --- a/src/arkreactor/Compiler/AST/Parser.cpp +++ b/src/arkreactor/Compiler/AST/Parser.cpp @@ -504,6 +504,7 @@ namespace Ark::internal arg_with_attr.setString(symbol_name); args->push_back(positioned(arg_with_attr, pos)); + std::ignore = newlineOrComment(); expect(IsChar(')')); } else diff --git a/src/arkreactor/Compiler/Macros/Processor.cpp b/src/arkreactor/Compiler/Macros/Processor.cpp index 9b4991468..d0a9e9299 100644 --- a/src/arkreactor/Compiler/Macros/Processor.cpp +++ b/src/arkreactor/Compiler/Macros/Processor.cpp @@ -740,6 +740,8 @@ namespace Ark::internal return isConstEval(child); }); + case NodeType::MutArg: + case NodeType::RefArg: case NodeType::Capture: case NodeType::Field: return false; diff --git a/src/arkscript/Formatter.cpp b/src/arkscript/Formatter.cpp index 63c94dc2a..f9782042e 100644 --- a/src/arkscript/Formatter.cpp +++ b/src/arkscript/Formatter.cpp @@ -186,6 +186,12 @@ std::string Formatter::format(const Node& node, std::size_t indent, bool after_n case NodeType::Symbol: result += node.string(); break; + case NodeType::MutArg: + result += fmt::format("(mut {})", node.string()); + break; + case NodeType::RefArg: + result += fmt::format("(ref {})", node.string()); + break; case NodeType::Capture: result += "&" + node.string(); break; diff --git a/tests/unittests/resources/FormatterSuite/functions.ark b/tests/unittests/resources/FormatterSuite/functions.ark index 2a148fb95..0897e0922 100644 --- a/tests/unittests/resources/FormatterSuite/functions.ark +++ b/tests/unittests/resources/FormatterSuite/functions.ark @@ -18,3 +18,14 @@ a &c) # body {}) (let foo (fun () (if true false nil))) + +(fun ( +# mutable +( +mut +a +) +# readonly ref +( +ref b +) &c) ()) diff --git a/tests/unittests/resources/FormatterSuite/functions.expected b/tests/unittests/resources/FormatterSuite/functions.expected index 1bd1d9afe..a967659bc 100644 --- a/tests/unittests/resources/FormatterSuite/functions.expected +++ b/tests/unittests/resources/FormatterSuite/functions.expected @@ -22,3 +22,10 @@ (if true false nil))) + +(fun ( + # mutable + (mut a) + # readonly ref + (ref b) + &c) ()) From f331980fa877698428a7523204216c88b107860a Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Fri, 5 Dec 2025 19:20:52 +0100 Subject: [PATCH 4/9] feat!: fully implementing 'mut' argument attribute, down to the ast lowerer, json compiler and formatter --- examples/closures.ark | 2 +- lib/std | 2 +- src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp | 3 ++- .../NameResolution/NameResolutionPass.cpp | 5 +++-- src/arkscript/JsonCompiler.cpp | 16 ++++++++++++++++ tests/benchmarks/resources/parser/bigger.ark | 2 +- tests/unittests/resources/ASTSuite/closures.ark | 2 +- tests/unittests/resources/ASTSuite/closures.json | 2 +- .../resources/CompilerSuite/ir/closures.ark | 2 +- .../CompilerSuite/optimized_ir/closures.ark | 2 +- .../runtime/stackoverflow_recur.ark | 2 +- .../runtime/stackoverflow_recur.expected | 2 +- .../resources/LangSuite/async-tests.ark | 2 +- tests/unittests/resources/LangSuite/vm-tests.ark | 2 +- .../RosettaSuite/call_an_object_method.ark | 2 +- 15 files changed, 33 insertions(+), 15 deletions(-) diff --git a/examples/closures.ark b/examples/closures.ark index fc060ab51..a4f06b259 100644 --- a/examples/closures.ark +++ b/examples/closures.ark @@ -1,7 +1,7 @@ # Inspired by # Closures and object are equivalent: http://wiki.c2.com/?ClosuresAndObjectsAreEquivalent # this will construct a closure capturing the 3 arguments, plus a function to set the age -(let create-human (fun (name age weight) { +(let create-human (fun (name (mut age) weight) { # functions can be invoked in the closure scope (let set-age (fun (new-age) (set age new-age))) diff --git a/lib/std b/lib/std index 9add02797..c736318d1 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 9add02797a063203d61c9f86b30f9ed954913df1 +Subproject commit c736318d19ba878086dc39388542712899d923f0 diff --git a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp index f6c341041..bc53bebd7 100644 --- a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp +++ b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp @@ -413,7 +413,8 @@ namespace Ark::internal // pushing arguments from the stack into variables in the new scope for (const auto& node : x.constList()[1].constList()) { - if (node.nodeType() == NodeType::Symbol) + // TODO: handle refarg with a new kind of instruction? + if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg || node.nodeType() == NodeType::RefArg) { page(function_body_page).emplace_back(STORE, addSymbol(node)); m_locals_locator.addLocal(node.string()); diff --git a/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp b/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp index 0cdcb5b39..329ce11ca 100644 --- a/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp +++ b/src/arkreactor/Compiler/NameResolution/NameResolutionPass.cpp @@ -243,10 +243,11 @@ namespace Ark::internal // this will prevent name conflicts, and handle scope resolution std::string old_name = child.string(); updateSymbolWithFullyQualifiedName(child); - // FIXME: addDefinedSymbol(fqn, true); ? addDefinedSymbol(old_name, true); } - else if (child.nodeType() == NodeType::Symbol) + else if (child.nodeType() == NodeType::Symbol || child.nodeType() == NodeType::RefArg) + addDefinedSymbol(child.string(), /* is_mutable= */ false); + else if (child.nodeType() == NodeType::MutArg) addDefinedSymbol(child.string(), /* is_mutable= */ true); } } diff --git a/src/arkscript/JsonCompiler.cpp b/src/arkscript/JsonCompiler.cpp index 1af94c2bb..0bb080daa 100644 --- a/src/arkscript/JsonCompiler.cpp +++ b/src/arkscript/JsonCompiler.cpp @@ -38,6 +38,22 @@ std::string JsonCompiler::_compile(const Node& node) break; } + case NodeType::MutArg: + { + json += fmt::format( + R"({{"type": "MutArg", "name": "{}"}})", + node.string()); + break; + } + + case NodeType::RefArg: + { + json += fmt::format( + R"({{"type": "RefArg", "name": "{}"}})", + node.string()); + break; + } + case NodeType::Spread: { json += fmt::format( diff --git a/tests/benchmarks/resources/parser/bigger.ark b/tests/benchmarks/resources/parser/bigger.ark index 6d53575cc..2d08074b4 100644 --- a/tests/benchmarks/resources/parser/bigger.ark +++ b/tests/benchmarks/resources/parser/bigger.ark @@ -673,7 +673,7 @@ # (print (list:insert b 1 [1 2])) # [0 1 2 9] # =end # @author https://github.com/SuperFola -(let insert (fun (_L _index _value) { +(let insert (fun (_L _index (mut _value)) { (let _size (len _L)) (assert (<= _index _size) "list:insert can not insert a value outside the list") diff --git a/tests/unittests/resources/ASTSuite/closures.ark b/tests/unittests/resources/ASTSuite/closures.ark index f818e5632..a0161e123 100644 --- a/tests/unittests/resources/ASTSuite/closures.ark +++ b/tests/unittests/resources/ASTSuite/closures.ark @@ -2,7 +2,7 @@ # Closures and object are equivalent: http://wiki.c2.com/?ClosuresAndObjectsAreEquivalent # this will construct a closure capturing the 3 arguments, plus a function to set the age -(let create-human (fun (name age weight) { +(let create-human (fun (name (mut age) weight) { # functions can be invoked in the closure scope (let set-age (fun (new-age) (set age new-age))) diff --git a/tests/unittests/resources/ASTSuite/closures.json b/tests/unittests/resources/ASTSuite/closures.json index 29c4565f4..15807051f 100644 --- a/tests/unittests/resources/ASTSuite/closures.json +++ b/tests/unittests/resources/ASTSuite/closures.json @@ -1 +1 @@ -{"type": "Begin", "children": [{"type": "Let", "name": {"type": "Symbol", "name": "create-human"}, "value": {"type": "Fun", "args": [{"type": "Symbol", "name": "name"}, {"type": "Symbol", "name": "age"}, {"type": "Symbol", "name": "weight"}], "body": {"type": "Begin", "children": [{"type": "Let", "name": {"type": "Symbol", "name": "set-age"}, "value": {"type": "Fun", "args": [{"type": "Symbol", "name": "new-age"}], "body": {"type": "Set", "name": {"type": "Symbol", "name": "age"}, "value": {"type": "Symbol", "name": "new-age"}}}}, {"type": "Fun", "args": [{"type": "Capture", "name": "set-age"}, {"type": "Capture", "name": "name"}, {"type": "Capture", "name": "age"}, {"type": "Capture", "name": "weight"}], "body": {"type": "Symbol", "name": "nil"}}]}}}, {"type": "Let", "name": {"type": "Symbol", "name": "bob"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "create-human"}, "args": [{"type": "String", "value": "Bob"}, {"type": "Number", "value": 0}, {"type": "Number", "value": 144}]}}, {"type": "Let", "name": {"type": "Symbol", "name": "john"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "create-human"}, "args": [{"type": "String", "value": "John"}, {"type": "Number", "value": 12}, {"type": "Number", "value": 15}]}}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Bob's age: "}, {"type": "Field", "children": [{"type": "Symbol", "name": "bob"}, {"type": "Symbol", "name": "age"}]}]}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Setting Bob's age to 10"}]}, [{"type": "Field", "children": [{"type": "Symbol", "name": "bob"}, {"type": "Symbol", "name": "set-age"}]}, {"type": "Number", "value": 10}], {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "New age: "}, {"type": "Field", "children": [{"type": "Symbol", "name": "bob"}, {"type": "Symbol", "name": "age"}]}]}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "John's age, didn't change: "}, {"type": "Field", "children": [{"type": "Symbol", "name": "john"}, {"type": "Symbol", "name": "age"}]}]}, {"type": "Let", "name": {"type": "Symbol", "name": "countdown-from"}, "value": {"type": "Fun", "args": [{"type": "Symbol", "name": "number"}], "body": {"type": "Fun", "args": [{"type": "Capture", "name": "number"}], "body": {"type": "Begin", "children": [{"type": "Set", "name": {"type": "Symbol", "name": "number"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "-"}, "args": [{"type": "Symbol", "name": "number"}, {"type": "Number", "value": 1}]}}, {"type": "Symbol", "name": "number"}]}}}}, {"type": "Let", "name": {"type": "Symbol", "name": "countdown-from-3"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "countdown-from"}, "args": [{"type": "Number", "value": 3}]}}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Countdown "}, [{"type": "Symbol", "name": "countdown-from-3"}]]}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Countdown "}, [{"type": "Symbol", "name": "countdown-from-3"}]]}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Countdown "}, [{"type": "Symbol", "name": "countdown-from-3"}]]}]} \ No newline at end of file +{"type": "Begin", "children": [{"type": "Let", "name": {"type": "Symbol", "name": "create-human"}, "value": {"type": "Fun", "args": [{"type": "Symbol", "name": "name"}, {"type": "MutArg", "name": "age"}, {"type": "Symbol", "name": "weight"}], "body": {"type": "Begin", "children": [{"type": "Let", "name": {"type": "Symbol", "name": "set-age"}, "value": {"type": "Fun", "args": [{"type": "Symbol", "name": "new-age"}], "body": {"type": "Set", "name": {"type": "Symbol", "name": "age"}, "value": {"type": "Symbol", "name": "new-age"}}}}, {"type": "Fun", "args": [{"type": "Capture", "name": "set-age"}, {"type": "Capture", "name": "name"}, {"type": "Capture", "name": "age"}, {"type": "Capture", "name": "weight"}], "body": {"type": "Symbol", "name": "nil"}}]}}}, {"type": "Let", "name": {"type": "Symbol", "name": "bob"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "create-human"}, "args": [{"type": "String", "value": "Bob"}, {"type": "Number", "value": 0}, {"type": "Number", "value": 144}]}}, {"type": "Let", "name": {"type": "Symbol", "name": "john"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "create-human"}, "args": [{"type": "String", "value": "John"}, {"type": "Number", "value": 12}, {"type": "Number", "value": 15}]}}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Bob's age: "}, {"type": "Field", "children": [{"type": "Symbol", "name": "bob"}, {"type": "Symbol", "name": "age"}]}]}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Setting Bob's age to 10"}]}, [{"type": "Field", "children": [{"type": "Symbol", "name": "bob"}, {"type": "Symbol", "name": "set-age"}]}, {"type": "Number", "value": 10}], {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "New age: "}, {"type": "Field", "children": [{"type": "Symbol", "name": "bob"}, {"type": "Symbol", "name": "age"}]}]}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "John's age, didn't change: "}, {"type": "Field", "children": [{"type": "Symbol", "name": "john"}, {"type": "Symbol", "name": "age"}]}]}, {"type": "Let", "name": {"type": "Symbol", "name": "countdown-from"}, "value": {"type": "Fun", "args": [{"type": "Symbol", "name": "number"}], "body": {"type": "Fun", "args": [{"type": "Capture", "name": "number"}], "body": {"type": "Begin", "children": [{"type": "Set", "name": {"type": "Symbol", "name": "number"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "-"}, "args": [{"type": "Symbol", "name": "number"}, {"type": "Number", "value": 1}]}}, {"type": "Symbol", "name": "number"}]}}}}, {"type": "Let", "name": {"type": "Symbol", "name": "countdown-from-3"}, "value": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "countdown-from"}, "args": [{"type": "Number", "value": 3}]}}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Countdown "}, [{"type": "Symbol", "name": "countdown-from-3"}]]}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Countdown "}, [{"type": "Symbol", "name": "countdown-from-3"}]]}, {"type": "FunctionCall", "name": {"type": "Symbol", "name": "print"}, "args": [{"type": "String", "value": "Countdown "}, [{"type": "Symbol", "name": "countdown-from-3"}]]}]} \ No newline at end of file diff --git a/tests/unittests/resources/CompilerSuite/ir/closures.ark b/tests/unittests/resources/CompilerSuite/ir/closures.ark index f818e5632..a0161e123 100644 --- a/tests/unittests/resources/CompilerSuite/ir/closures.ark +++ b/tests/unittests/resources/CompilerSuite/ir/closures.ark @@ -2,7 +2,7 @@ # Closures and object are equivalent: http://wiki.c2.com/?ClosuresAndObjectsAreEquivalent # this will construct a closure capturing the 3 arguments, plus a function to set the age -(let create-human (fun (name age weight) { +(let create-human (fun (name (mut age) weight) { # functions can be invoked in the closure scope (let set-age (fun (new-age) (set age new-age))) diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/closures.ark b/tests/unittests/resources/CompilerSuite/optimized_ir/closures.ark index cfef323a4..b320e0b05 100644 --- a/tests/unittests/resources/CompilerSuite/optimized_ir/closures.ark +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/closures.ark @@ -2,7 +2,7 @@ # Closures and object are equivalent: http://wiki.c2.com/?ClosuresAndObjectsAreEquivalent # this will construct a closure capturing the 3 arguments, plus a function to set the age -(let create-human (fun (name age weight) { +(let create-human (fun (name (mut age) weight) { # functions can be invoked in the closure scope (let set-age (fun (new-age) (set age new-age))) diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.ark b/tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.ark index e94097637..a81548daa 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.ark +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.ark @@ -1,4 +1,4 @@ -(let A (fun (k x1 x0 x3 x4 x5) { +(let A (fun ((mut k) x1 x0 x3 x4 x5) { (let B (fun () { (set k (- k 1)) (B k A x1 x0 x3 x4) })) diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.expected index 4aca87f80..ffe39c724 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.expected +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.expected @@ -1,7 +1,7 @@ Stack overflow. You could consider rewriting your function to make use of tail-call optimization. In file tests/unittests/resources/DiagnosticsSuite/runtime/stackoverflow_recur.ark:4 - 1 | (let A (fun (k x1 x0 x3 x4 x5) { + 1 | (let A (fun ((mut k) x1 x0 x3 x4 x5) { 2 | (let B (fun () { 3 | (set k (- k 1)) 4 | (B k A x1 x0 x3 x4) })) diff --git a/tests/unittests/resources/LangSuite/async-tests.ark b/tests/unittests/resources/LangSuite/async-tests.ark index 5765de8e7..c82c285ba 100644 --- a/tests/unittests/resources/LangSuite/async-tests.ark +++ b/tests/unittests/resources/LangSuite/async-tests.ark @@ -9,7 +9,7 @@ (let size 1000) (let data (list:fill size 1)) -(let sum (fun (a b src) { +(let sum (fun ((mut a) b src) { (mut acc 0) (while (< a b) { (set acc (+ acc (@ src a))) diff --git a/tests/unittests/resources/LangSuite/vm-tests.ark b/tests/unittests/resources/LangSuite/vm-tests.ark index a03f5a669..0a80d170c 100644 --- a/tests/unittests/resources/LangSuite/vm-tests.ark +++ b/tests/unittests/resources/LangSuite/vm-tests.ark @@ -22,7 +22,7 @@ (mut val 1) (let parent (fun (&val &child) ())) -(let create-human (fun (name age) { +(let create-human (fun (name (mut age)) { (let set-age (fun (new-age) (set age new-age))) (fun (&set-age &name &age) ()) })) (let bob (create-human "Bob" 38)) diff --git a/tests/unittests/resources/RosettaSuite/call_an_object_method.ark b/tests/unittests/resources/RosettaSuite/call_an_object_method.ark index 7052c3c4b..974a26ce0 100644 --- a/tests/unittests/resources/RosettaSuite/call_an_object_method.ark +++ b/tests/unittests/resources/RosettaSuite/call_an_object_method.ark @@ -1,4 +1,4 @@ -(let create_user (fun (username password age) { +(let create_user (fun ((mut username) password age) { (let change_username (fun (new_name) (set username new_name))) (let check_password (fun (pass) From 84d909ba4b970fc5d3ad1281d994b19a979f4fb8 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Sun, 7 Dec 2025 20:45:31 +0100 Subject: [PATCH 5/9] feat(vm)!: implementing the ref argument attribute, to avoid reference unboxing when calling a function, yielding better performances for the user --- include/Ark/Compiler/Instructions.hpp | 209 +++++++++--------- include/Ark/VM/VM.inl | 5 +- src/arkreactor/Compiler/BytecodeReader.cpp | 1 + .../Compiler/Lowerer/ASTLowerer.cpp | 10 +- src/arkreactor/VM/VM.cpp | 13 +- tests/benchmarks/main.cpp | 1 + tests/benchmarks/resources/parser/big.ark | 2 +- tests/benchmarks/resources/parser/bigger.ark | 2 +- .../runtime/create_list_with_ref.ark | 31 +++ 9 files changed, 165 insertions(+), 109 deletions(-) create mode 100644 tests/benchmarks/resources/runtime/create_list_with_ref.ark diff --git a/include/Ark/Compiler/Instructions.hpp b/include/Ark/Compiler/Instructions.hpp index 134d1b718..cc551beb9 100644 --- a/include/Ark/Compiler/Instructions.hpp +++ b/include/Ark/Compiler/Instructions.hpp @@ -56,374 +56,378 @@ namespace Ark::internal // @role Take the value on top of the stack and create a variable in the current scope, named following the given symbol id (cf symbols table) STORE = 0x05, + // @args symbol id + // @role Store a value in a symbol without dereferencing it (used by functions only) + STORE_REF = 0x06, + // @args symbol id // @role Take the value on top of the stack and put it inside a variable named following the symbol id (cf symbols table), in the nearest scope. Raise an error if it couldn't find a scope where the variable exists - SET_VAL = 0x06, + SET_VAL = 0x07, // @args absolute address to jump to // @role Jump to the provided address if the last value on the stack was equal to false. Remove the value from the stack no matter what it is - POP_JUMP_IF_FALSE = 0x07, + POP_JUMP_IF_FALSE = 0x08, // @args absolute address to jump to // @role Jump to the provided address - JUMP = 0x08, + JUMP = 0x09, // @role If in a code segment other than the main one, quit it, and push the value on top of the stack to the new stack; should as well delete the current environment. Otherwise, acts as a `HALT` - RET = 0x09, + RET = 0x0a, // @role Stop the Virtual Machine - HALT = 0x0a, + HALT = 0x0b, // @role push pp, then ip on the stack, preparing for a call instruction - PUSH_RETURN_ADDRESS = 0x0b, + PUSH_RETURN_ADDRESS = 0x0c, // @args argument count // @role Call function from its symbol id located on top of the stack. Take the given number of arguments from the top of stack and give them to the function (the first argument taken from the stack will be the last one of the function). The stack of the function is now composed of its arguments, from the first to the last one - CALL = 0x0c, + CALL = 0x0d, // @args symbol id // @role Tell the Virtual Machine to capture the variable from the current environment. Main goal is to be able to handle closures, which need to save the environment in which they were created - CAPTURE = 0x0d, + CAPTURE = 0x0e, // @args symbol id // @role Tell the VM to use the given symbol for the next capture - RENAME_NEXT_CAPTURE = 0x0e, + RENAME_NEXT_CAPTURE = 0x0f, // @args builtin id // @role Push the corresponding builtin function object on the stack - BUILTIN = 0x0f, + BUILTIN = 0x10, // @args symbol id // @role Remove a variable/constant named following the given symbol id (cf symbols table) - DEL = 0x10, + DEL = 0x11, // @args constant id // @role Push a Closure with the page address pointed by the constant, along with the saved scope created by CAPTURE instruction(s) - MAKE_CLOSURE = 0x11, + MAKE_CLOSURE = 0x12, // @args symbol id // @role Read the field named following the given symbol id (cf symbols table) of a `Closure` stored in TS. Pop TS and push the value of field read on the stack - GET_FIELD = 0x12, + GET_FIELD = 0x13, // @args constant id // @role Load a plugin dynamically, plugin name is stored as a string in the constants table - PLUGIN = 0x13, + PLUGIN = 0x14, // @args number of elements // @role Create a list from the N elements pushed on the stack. Follows the function calling convention - LIST = 0x14, + LIST = 0x15, // @args number of elements // @role Append N elements to a list (TS). Elements are stored in TS(1)..TS(N). Follows the function calling convention - APPEND = 0x15, + APPEND = 0x16, // @args number of elements // @role Concatenate N lists to a list (TS). Lists to concat to TS are stored in TS(1)..TS(N). Follows the function calling convention - CONCAT = 0x16, + CONCAT = 0x17, // @args number of elements // @role Append N elements to a reference to a list (TS), 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 = 0x17, + APPEND_IN_PLACE = 0x18, // @args number of elements // @role Concatenate N lists to a reference to a list (TS), the list is being mutated in-place, no new object created. Lists to concat to TS are stored in TS(1)..TS(N). Follows the function calling convention - CONCAT_IN_PLACE = 0x18, + CONCAT_IN_PLACE = 0x19, // @role Remove an element from a list (TS), given an index (TS1). Push a new list without the removed element to the stack - POP_LIST = 0x19, + POP_LIST = 0x1a, // @role Remove an element from a reference to a list (TS), given an index (TS1). The list is mutated in-place, no new object created - POP_LIST_IN_PLACE = 0x1a, + POP_LIST_IN_PLACE = 0x1b, // @role Modify a reference to a list or string (TS) by replacing the element at TS1 (must be a number) by the value in TS2. The object is mutated in-place, no new object created - SET_AT_INDEX = 0x1b, + SET_AT_INDEX = 0x1c, // @role Modify a reference to a list (TS) by replacing TS[TS2][TS1] by the value in TS3. TS[TS2] can be a string (if it is, TS3 must be a string). The object is mutated in-place, no new object created - SET_AT_2_INDEX = 0x1c, + SET_AT_2_INDEX = 0x1d, // @role Remove the top of the stack - POP = 0x1d, + POP = 0x1e, // @role Pop the top of the stack, if it's false, jump to an address - SHORTCIRCUIT_AND = 0x1e, + SHORTCIRCUIT_AND = 0x1f, // @role Pop the top of the stack, if it's true, jump to an address - SHORTCIRCUIT_OR = 0x1f, + SHORTCIRCUIT_OR = 0x20, // @role Create a new local scope - CREATE_SCOPE = 0x20, + CREATE_SCOPE = 0x21, // @role Reset the current scope so that it is empty, and jump to a given location - RESET_SCOPE_JUMP = 0x21, + RESET_SCOPE_JUMP = 0x22, // @role Destroy the last local scope - POP_SCOPE = 0x22, + POP_SCOPE = 0x23, // @args symbol id (function name) // @role Push the current page address as a value on the stack - GET_CURRENT_PAGE_ADDR = 0x23, + GET_CURRENT_PAGE_ADDR = 0x24, - FIRST_OPERATOR = 0x24, + FIRST_OPERATOR = 0x25, // @role Push `TS1 + TS` - ADD = 0x24, + ADD = 0x25, // @role Push `TS1 - TS` - SUB = 0x25, + SUB = 0x26, // @role Push `TS1 * TS` - MUL = 0x26, + MUL = 0x27, // @role Push `TS1 / TS` - DIV = 0x27, + DIV = 0x28, // @role Push `TS1 > TS` - GT = 0x28, + GT = 0x29, // @role Push `TS1 < TS` - LT = 0x29, + LT = 0x2a, // @role Push `TS1 <= TS` - LE = 0x2a, + LE = 0x2b, // @role Push `TS1 >= TS` - GE = 0x2b, + GE = 0x2c, // @role Push `TS1 != TS` - NEQ = 0x2c, + NEQ = 0x2d, // @role Push `TS1 == TS` - EQ = 0x2d, + EQ = 0x2e, // @role Push `len(TS)`, TS must be a list - LEN = 0x2e, + LEN = 0x2f, // @role Push `empty?(TS)`, TS must be a list or string - EMPTY = 0x2f, + EMPTY = 0x30, // @role Push `tail(TS)`, all the elements of TS except the first one. TS must be a list or string - TAIL = 0x30, + TAIL = 0x31, // @role Push `head(TS)`, the first element of TS or nil if empty. TS must be a list or string - HEAD = 0x31, + HEAD = 0x32, // @role Push true if TS is nil, false otherwise - ISNIL = 0x32, + ISNIL = 0x33, // @role Throw an exception if TS1 is false, and display TS (must be a string). Do not push anything on the stack - ASSERT = 0x33, + ASSERT = 0x34, // @role Convert TS to number (must be a string) - TO_NUM = 0x34, + TO_NUM = 0x35, // @role Convert TS to string - TO_STR = 0x35, + TO_STR = 0x36, // @role Push the value at index TS (must be a number) in TS1, which must be a list or string - AT = 0x36, + AT = 0x37, // @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 = 0x37, + AT_AT = 0x38, // @role Push `TS1 % TS` - MOD = 0x38, + MOD = 0x39, // @role Push the type of TS as a string - TYPE = 0x39, + TYPE = 0x3a, // @role Check if TS1 is a closure field of TS. TS must be a Closure, TS1 a String - HASFIELD = 0x3a, + HASFIELD = 0x3b, // @role Push `!TS` - NOT = 0x3b, + NOT = 0x3c, // @args constant id, constant id // @role Load two consts (`primary` then `secondary`) on the stack in one instruction - LOAD_CONST_LOAD_CONST = 0x3c, + LOAD_CONST_LOAD_CONST = 0x3d, // @args constant id, symbol id // @role Load const `primary` into the symbol `secondary` (create a variable) - LOAD_CONST_STORE = 0x3d, + LOAD_CONST_STORE = 0x3e, // @args constant id, symbol id // @role Load const `primary` into the symbol `secondary` (search for the variable with the given symbol id) - LOAD_CONST_SET_VAL = 0x3e, + LOAD_CONST_SET_VAL = 0x3f, // @args symbol id, symbol id // @role Store the value of the symbol `primary` into a new variable `secondary` - STORE_FROM = 0x3f, + STORE_FROM = 0x40, // @args symbol index, symbol id // @role Store the value of the symbol `primary` into a new variable `secondary` - STORE_FROM_INDEX = 0x40, + STORE_FROM_INDEX = 0x41, // @args symbol id, symbol id // @role Store the value of the symbol `primary` into an existing variable `secondary` - SET_VAL_FROM = 0x41, + SET_VAL_FROM = 0x42, // @args symbol index, symbol id // @role Store the value of the symbol `primary` into an existing variable `secondary` - SET_VAL_FROM_INDEX = 0x42, + SET_VAL_FROM_INDEX = 0x43, // @args symbol id, count // @role Increment the variable `primary` by `count` and push its value on the stack - INCREMENT = 0x43, + INCREMENT = 0x44, // @args symbol index, count // @role Increment the variable `primary` by `count` and push its value on the stack - INCREMENT_BY_INDEX = 0x44, + INCREMENT_BY_INDEX = 0x45, // @args symbol id, count // @role Increment the variable `primary` by `count` and store its value in the given symbol id - INCREMENT_STORE = 0x45, + INCREMENT_STORE = 0x46, // @args symbol id, count // @role Decrement the variable `primary` by `count` and push its value on the stack - DECREMENT = 0x46, + DECREMENT = 0x47, // @args symbol index, count // @role Decrement the variable `primary` by `count` and push its value on the stack - DECREMENT_BY_INDEX = 0x47, + DECREMENT_BY_INDEX = 0x48, // @args symbol id, count // @role Decrement the variable `primary` by `count` and store its value in the given symbol id - DECREMENT_STORE = 0x48, + DECREMENT_STORE = 0x49, // @args symbol id, symbol id // @role Load the symbol `primary`, compute its tail, store it in a new variable `secondary` - STORE_TAIL = 0x49, + STORE_TAIL = 0x4a, // @args symbol index, symbol id // @role Load the symbol `primary`, compute its tail, store it in a new variable `secondary` - STORE_TAIL_BY_INDEX = 0x4a, + STORE_TAIL_BY_INDEX = 0x4b, // @args symbol id, symbol id // @role Load the symbol `primary`, compute its head, store it in a new variable `secondary` - STORE_HEAD = 0x4b, + STORE_HEAD = 0x4c, // @args symbol index, symbol id // @role Load the symbol `primary`, compute its head, store it in a new variable `secondary` - STORE_HEAD_BY_INDEX = 0x4c, + STORE_HEAD_BY_INDEX = 0x4d, // @args number, symbol id // @role Create a list of `number` elements, and store it in a new variable `secondary` - STORE_LIST = 0x4d, + STORE_LIST = 0x4e, // @args symbol id, symbol id // @role Load the symbol `primary`, compute its tail, store it in an existing variable `secondary` - SET_VAL_TAIL = 0x4e, + SET_VAL_TAIL = 0x4f, // @args symbol index, symbol id // @role Load the symbol `primary`, compute its tail, store it in an existing variable `secondary` - SET_VAL_TAIL_BY_INDEX = 0x4f, + SET_VAL_TAIL_BY_INDEX = 0x50, // @args symbol id, symbol id // @role Load the symbol `primary`, compute its head, store it in an existing variable `secondary` - SET_VAL_HEAD = 0x50, + SET_VAL_HEAD = 0x51, // @args symbol index, symbol id // @role Load the symbol `primary`, compute its head, store it in an existing variable `secondary` - SET_VAL_HEAD_BY_INDEX = 0x51, + SET_VAL_HEAD_BY_INDEX = 0x52, // @args builtin id, argument count // @role Call a builtin by its id in `primary`, with `secondary` arguments. Bypass the stack size check because we do not push IP/PP since builtins calls do not alter the stack - CALL_BUILTIN = 0x52, + CALL_BUILTIN = 0x53, // @args builtin id, argument count // @role Call a builtin by its id in `primary`, with `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 = 0x53, + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS = 0x54, // @args constant id, absolute address to jump to // @role Compare `TS < constant`, if the comparison fails, jump to the given address. Otherwise, does nothing - LT_CONST_JUMP_IF_FALSE = 0x54, + LT_CONST_JUMP_IF_FALSE = 0x55, // @args constant id, absolute address to jump to // @role Compare `TS < constant`, if the comparison succeeds, jump to the given address. Otherwise, does nothing - LT_CONST_JUMP_IF_TRUE = 0x55, + LT_CONST_JUMP_IF_TRUE = 0x56, // @args symbol id, absolute address to jump to // @role Compare `TS < symbol`, if the comparison fails, jump to the given address. Otherwise, does nothing - LT_SYM_JUMP_IF_FALSE = 0x56, + LT_SYM_JUMP_IF_FALSE = 0x57, // @args constant id, absolute address to jump to // @role Compare `TS > constant`, if the comparison succeeds, jump to the given address. Otherwise, does nothing - GT_CONST_JUMP_IF_TRUE = 0x57, + GT_CONST_JUMP_IF_TRUE = 0x58, // @args constant id, absolute address to jump to // @role Compare `TS > constant`, if the comparison fails, jump to the given address. Otherwise, does nothing - GT_CONST_JUMP_IF_FALSE = 0x58, + GT_CONST_JUMP_IF_FALSE = 0x59, // @args symbol id, absolute address to jump to // @role Compare `TS > symbol`, if the comparison fails, jump to the given address. Otherwise, does nothing - GT_SYM_JUMP_IF_FALSE = 0x59, + GT_SYM_JUMP_IF_FALSE = 0x5a, // @args constant id, absolute address to jump to // @role Compare `TS == constant`, if the comparison succeeds, jump to the given address. Otherwise, does nothing - EQ_CONST_JUMP_IF_TRUE = 0x5a, + EQ_CONST_JUMP_IF_TRUE = 0x5b, // @args symbol index, absolute address to jump to // @role Compare `TS == symbol`, if the comparison succeeds, jump to the given address. Otherwise, does nothing - EQ_SYM_INDEX_JUMP_IF_TRUE = 0x5b, + EQ_SYM_INDEX_JUMP_IF_TRUE = 0x5c, // @args constant id, absolute address to jump to // @role Compare `TS != constant`, if the comparison succeeds, jump to the given address. Otherwise, does nothing - NEQ_CONST_JUMP_IF_TRUE = 0x5c, + NEQ_CONST_JUMP_IF_TRUE = 0x5d, // @args symbol id, absolute address to jump to // @role Compare `TS != symbol`, if the comparison fails, jump to the given address. Otherwise, does nothing - NEQ_SYM_JUMP_IF_FALSE = 0x5d, + NEQ_SYM_JUMP_IF_FALSE = 0x5e, // @args symbol id, argument count // @role Call a symbol by its id in `primary`, with `secondary` arguments - CALL_SYMBOL = 0x5e, + CALL_SYMBOL = 0x5f, // @args symbol id (function name), argument count // @role Call the current page with `secondary` arguments - CALL_CURRENT_PAGE = 0x5f, + CALL_CURRENT_PAGE = 0x60, // @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 = 0x60, + GET_FIELD_FROM_SYMBOL = 0x61, // @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 = 0x61, + GET_FIELD_FROM_SYMBOL_INDEX = 0x62, // @args symbol id, symbol id2 // @role Push symbol[symbol2] - AT_SYM_SYM = 0x62, + AT_SYM_SYM = 0x63, // @args symbol index, symbol index2 // @role Push symbol[symbol2] - AT_SYM_INDEX_SYM_INDEX = 0x63, + AT_SYM_INDEX_SYM_INDEX = 0x64, // @args symbol index, constant id // @role Push symbol[constant] - AT_SYM_INDEX_CONST = 0x64, + AT_SYM_INDEX_CONST = 0x65, // @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 = 0x65, + CHECK_TYPE_OF = 0x66, // @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 = 0x66, + CHECK_TYPE_OF_BY_INDEX = 0x67, // @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 = 0x67, + APPEND_IN_PLACE_SYM = 0x68, // @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 = 0x68, + APPEND_IN_PLACE_SYM_INDEX = 0x69, // @args symbol index, symbol id // @role Compute the length of the list or string at symbol index, and store it in a variable (symbol id) - STORE_LEN = 0x69, + STORE_LEN = 0x6a, // @args symbol id, absolute address to jump to // @role Compute the length of a symbol (list or string), and pop TS to compare it, then jump if false - LT_LEN_SYM_JUMP_IF_FALSE = 0x6a, + LT_LEN_SYM_JUMP_IF_FALSE = 0x6b, InstructionsCount }; @@ -435,6 +439,7 @@ namespace Ark::internal "LOAD_CONST", "POP_JUMP_IF_TRUE", "STORE", + "STORE_REF", "SET_VAL", "POP_JUMP_IF_FALSE", "JUMP", diff --git a/include/Ark/VM/VM.inl b/include/Ark/VM/VM.inl index 0752f9d52..fb1d6313b 100644 --- a/include/Ark/VM/VM.inl +++ b/include/Ark/VM/VM.inl @@ -117,6 +117,8 @@ inline Value* VM::loadSymbolFromIndex(const uint16_t index, internal::ExecutionC // treatment only for function calls. auto& [id, value] = context.locals.back().atPosReverse(index); context.last_symbol = id; + if (value.valueType() == ValueType::Reference) + return value.reference(); return &value; } @@ -323,7 +325,8 @@ inline void VM::call(internal::ExecutionContext& context, const uint16_t argc, V needed_argc = 0; // every argument is a MUT declaration in the bytecode - while (m_state.inst(context.pp, index) == STORE) + while (m_state.inst(context.pp, index) == STORE || + m_state.inst(context.pp, index) == STORE_REF) { needed_argc += 1; index += 4; // instructions are on 4 bytes diff --git a/src/arkreactor/Compiler/BytecodeReader.cpp b/src/arkreactor/Compiler/BytecodeReader.cpp index 07abc2b81..d37f0a8be 100644 --- a/src/arkreactor/Compiler/BytecodeReader.cpp +++ b/src/arkreactor/Compiler/BytecodeReader.cpp @@ -494,6 +494,7 @@ namespace Ark { LOAD_CONST, ArgKind::Constant }, { POP_JUMP_IF_TRUE, ArgKind::Raw }, { STORE, ArgKind::Symbol }, + { STORE_REF, ArgKind::Symbol }, { SET_VAL, ArgKind::Symbol }, { POP_JUMP_IF_FALSE, ArgKind::Raw }, { JUMP, ArgKind::Raw }, diff --git a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp index bc53bebd7..4a0b2b527 100644 --- a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp +++ b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp @@ -413,12 +413,16 @@ namespace Ark::internal // pushing arguments from the stack into variables in the new scope for (const auto& node : x.constList()[1].constList()) { - // TODO: handle refarg with a new kind of instruction? - if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg || node.nodeType() == NodeType::RefArg) + if (node.nodeType() == NodeType::Symbol || node.nodeType() == NodeType::MutArg) { page(function_body_page).emplace_back(STORE, addSymbol(node)); m_locals_locator.addLocal(node.string()); } + else if (node.nodeType() == NodeType::RefArg) + { + page(function_body_page).emplace_back(STORE_REF, addSymbol(node)); + m_locals_locator.addLocal(node.string()); + } } // Register an opened variable as "#anonymous", which won't match any valid names inside ASTLowerer::handleCalls. @@ -426,7 +430,7 @@ namespace Ark::internal // (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"); + m_opened_vars.emplace("#anonymous"); // push body of the function compileExpression(x.list()[2], function_body_page, false, true); if (x.isAnonymousFunction()) diff --git a/src/arkreactor/VM/VM.cpp b/src/arkreactor/VM/VM.cpp index 96eafc52b..1f8f7d30d 100644 --- a/src/arkreactor/VM/VM.cpp +++ b/src/arkreactor/VM/VM.cpp @@ -543,6 +543,7 @@ namespace Ark &&TARGET_LOAD_CONST, &&TARGET_POP_JUMP_IF_TRUE, &&TARGET_STORE, + &&TARGET_STORE_REF, &&TARGET_SET_VAL, &&TARGET_POP_JUMP_IF_FALSE, &&TARGET_JUMP, @@ -705,6 +706,15 @@ namespace Ark DISPATCH(); } + TARGET(STORE_REF) + { + // Not resolving a potential ref is on purpose! + // This instruction is only used by functions when storing arguments + Value* tmp = pop(context); + store(arg, tmp, context); + DISPATCH(); + } + TARGET(SET_VAL) { setVal(arg, popAndResolveAsPtr(context), context); @@ -2037,7 +2047,8 @@ namespace Ark arg_names.emplace_back(""); // for formatting, so that we have a space between the function and the args std::size_t index = 0; - while (m_state.inst(context.pp, index) == STORE) + while (m_state.inst(context.pp, index) == STORE || + m_state.inst(context.pp, index) == STORE_REF) { const auto id = static_cast((m_state.inst(context.pp, index + 2) << 8) + m_state.inst(context.pp, index + 3)); arg_names.push_back(m_state.m_symbols[id]); diff --git a/tests/benchmarks/main.cpp b/tests/benchmarks/main.cpp index ca511f7f0..b518a7d6e 100644 --- a/tests/benchmarks/main.cpp +++ b/tests/benchmarks/main.cpp @@ -35,6 +35,7 @@ ARK_CREATE_RUNTIME_BENCH(binary_trees); ARK_CREATE_RUNTIME_BENCH(for_sum); ARK_CREATE_RUNTIME_BENCH(create_closure)->Iterations(500); ARK_CREATE_RUNTIME_BENCH(create_list)->Iterations(500); +ARK_CREATE_RUNTIME_BENCH(create_list_with_ref)->Iterations(500); ARK_CREATE_RUNTIME_BENCH(n_queens)->Iterations(50); // -------------------------------------------- diff --git a/tests/benchmarks/resources/parser/big.ark b/tests/benchmarks/resources/parser/big.ark index 1addbb6b1..43ff2f8b5 100644 --- a/tests/benchmarks/resources/parser/big.ark +++ b/tests/benchmarks/resources/parser/big.ark @@ -194,7 +194,7 @@ # (print (list:take [1 2 3 4 5 6 7 8 9] 4)) # [1 2 3 4] # =end # @author https://github.com/rstefanic -(let list:take (fun (_L _n) { +(let list:take (fun (_L (mut _n)) { (mut _index 0) (mut _output []) (set _n (math:min _n (len _L))) diff --git a/tests/benchmarks/resources/parser/bigger.ark b/tests/benchmarks/resources/parser/bigger.ark index 2d08074b4..767aa3257 100644 --- a/tests/benchmarks/resources/parser/bigger.ark +++ b/tests/benchmarks/resources/parser/bigger.ark @@ -413,7 +413,7 @@ # (print (take [1 2 3 4 5 6 7 8 9] 4)) # [1 2 3 4] # =end # @author https://github.com/rstefanic -(let take (fun (_L _n) { +(let take (fun (_L (mut _n)) { (mut _index 0) (mut _output []) (set _n (min _n (len _L))) diff --git a/tests/benchmarks/resources/runtime/create_list_with_ref.ark b/tests/benchmarks/resources/runtime/create_list_with_ref.ark new file mode 100644 index 000000000..b5ff87209 --- /dev/null +++ b/tests/benchmarks/resources/runtime/create_list_with_ref.ark @@ -0,0 +1,31 @@ +(let sum (fun ((ref data)) + (+ + (if (= "List" (type (@ data 0))) (sum (@ data 0)) (@ data 0)) + (if (= "List" (type (@ data 1))) (sum (@ data 1)) (@ data 1))))) + +(mut i 0) +(while (< i 100) { + (let tree + [ + [ + [1 2] + [ + 3 + [ + 4 + [5 6]]]] + [ + [ + [ + [7 8] + 9] + [10 11]] + [ + [ + 12 + [ + 13 + [14 15]]] + [16 17]]]]) + (sum tree) + (set i (+ 1 i) )}) From dadd560b166a5800e18e101264482207d784c9a8 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Mon, 8 Dec 2025 19:27:43 +0100 Subject: [PATCH 6/9] feat(builtins): adding io:readLinesFile --- include/Ark/Builtins/Builtins.hpp | 1 + lib/std | 2 +- src/arkreactor/Builtins/Builtins.cpp | 1 + src/arkreactor/Builtins/IO.cpp | 21 +++++++++++++++++++ .../CompilerSuite/ir/99bottles.expected | 4 ++-- .../optimized_ir/99bottles.expected | 12 +++++------ .../optimized_ir/builtins.expected | 2 +- 7 files changed, 33 insertions(+), 10 deletions(-) diff --git a/include/Ark/Builtins/Builtins.hpp b/include/Ark/Builtins/Builtins.hpp index 96d673b23..329a4877c 100644 --- a/include/Ark/Builtins/Builtins.hpp +++ b/include/Ark/Builtins/Builtins.hpp @@ -52,6 +52,7 @@ namespace Ark::internal::Builtins ARK_BUILTIN(writeFile); ARK_BUILTIN(appendToFile); ARK_BUILTIN(readFile); + ARK_BUILTIN(readLinesFile); ARK_BUILTIN(fileExists); ARK_BUILTIN(listFiles); ARK_BUILTIN(isDirectory); diff --git a/lib/std b/lib/std index c736318d1..93d2b42fe 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit c736318d19ba878086dc39388542712899d923f0 +Subproject commit 93d2b42fede8ba23214dd20a5f9631ed598a023d diff --git a/src/arkreactor/Builtins/Builtins.cpp b/src/arkreactor/Builtins/Builtins.cpp index 55b5b9198..d689f83e6 100644 --- a/src/arkreactor/Builtins/Builtins.cpp +++ b/src/arkreactor/Builtins/Builtins.cpp @@ -42,6 +42,7 @@ namespace Ark::internal::Builtins { "builtin__io:writeFile", Value(IO::writeFile) }, { "builtin__io:appendToFile", Value(IO::appendToFile) }, { "builtin__io:readFile", Value(IO::readFile) }, + { "builtin__io:readLinesFile", Value(IO::readLinesFile) }, { "builtin__io:fileExists?", Value(IO::fileExists) }, { "builtin__io:listFiles", Value(IO::listFiles) }, { "builtin__io:dir?", Value(IO::isDirectory) }, diff --git a/src/arkreactor/Builtins/IO.cpp b/src/arkreactor/Builtins/IO.cpp index 0813493af..87e6a7d2a 100644 --- a/src/arkreactor/Builtins/IO.cpp +++ b/src/arkreactor/Builtins/IO.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -145,6 +146,26 @@ namespace Ark::internal::Builtins::IO return Value(Utils::readFile(filename)); } + // cppcheck-suppress constParameterReference + Value readLinesFile(std::vector& n, VM* vm [[maybe_unused]]) + { + if (!types::check(n, ValueType::String)) + throw types::TypeCheckingError( + "io:readLinesFile", + { { types::Contract { { types::Typedef("filename", ValueType::String) } } } }, + n); + + std::string filename = n[0].string(); + if (!Utils::fileExists(filename)) + throw std::runtime_error( + fmt::format("io:readLinesFile: couldn't read file \"{}\" because it doesn't exist", filename)); + + Value out = Value(ValueType::List); + for (auto&& s : Utils::splitString(Utils::readFile(filename), '\n')) + out.push_back(Value(s)); + return out; + } + // cppcheck-suppress constParameterReference Value fileExists(std::vector& n, VM* vm [[maybe_unused]]) { diff --git a/tests/unittests/resources/CompilerSuite/ir/99bottles.expected b/tests/unittests/resources/CompilerSuite/ir/99bottles.expected index 7d8fdf0dc..b583a6e76 100644 --- a/tests/unittests/resources/CompilerSuite/ir/99bottles.expected +++ b/tests/unittests/resources/CompilerSuite/ir/99bottles.expected @@ -37,7 +37,7 @@ page_0 LOAD_SYMBOL 4 LOAD_SYMBOL 4 LOAD_CONST 3 - BUILTIN 25 + BUILTIN 26 CALL 3 .L7: BUILTIN 9 @@ -52,7 +52,7 @@ page_0 PUSH_RETURN_ADDRESS L9 LOAD_SYMBOL 4 LOAD_CONST 4 - BUILTIN 25 + BUILTIN 26 CALL 2 .L9: BUILTIN 9 diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.expected b/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.expected index c2f94e80f..844c28dc3 100644 --- a/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.expected +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/99bottles.expected @@ -2,7 +2,7 @@ page_0 LOAD_CONST_STORE 0, 0 LOAD_CONST_STORE 1, 2 LOAD_CONST_STORE 2, 4 - BUILTIN 21 + BUILTIN 22 STORE 6 STORE_FROM 8, 7 STORE_FROM 10, 9 @@ -37,7 +37,7 @@ page_0 LOAD_SYMBOL 13 LOAD_SYMBOL 13 LOAD_CONST 6 - CALL_BUILTIN 25, 3 + CALL_BUILTIN 26, 3 .L10: CALL_BUILTIN 9, 1 .L9: @@ -47,7 +47,7 @@ page_0 PUSH_RETURN_ADDRESS L12 LOAD_SYMBOL 13 LOAD_CONST 7 - CALL_BUILTIN 25, 2 + CALL_BUILTIN 26, 2 .L12: CALL_BUILTIN 9, 1 .L11: @@ -58,19 +58,19 @@ page_0 HALT 0 page_1 - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 22, 1 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 23, 1 .L0: RET 0 HALT 0 page_2 - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 23, 1 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 24, 1 .L1: RET 0 HALT 0 page_3 - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 24, 1 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 25, 1 .L2: RET 0 HALT 0 diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/builtins.expected b/tests/unittests/resources/CompilerSuite/optimized_ir/builtins.expected index cc0932c95..8453e41f4 100644 --- a/tests/unittests/resources/CompilerSuite/optimized_ir/builtins.expected +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/builtins.expected @@ -5,7 +5,7 @@ page_0 HALT 0 page_1 - CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 53, 1 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 54, 1 .L0: RET 0 HALT 0 From 61f7c8c9def81759b116d384d95455941a8cf0b6 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Mon, 8 Dec 2025 19:32:01 +0100 Subject: [PATCH 7/9] chore: adding tests for io:readLinesFile --- CHANGELOG.md | 1 + lib/std | 2 +- .../DiagnosticsSuite/typeChecking/ioreadlinesfile_num.ark | 1 + .../typeChecking/ioreadlinesfile_num.expected | 7 +++++++ 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.expected diff --git a/CHANGELOG.md b/CHANGELOG.md index 604cd159a..9f12f9b13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased version] - 20XX-XX-XX ### Added - new builtin `disassemble` to print the bytecode of a function +- new builtin `io:readFileLines` to read lines from a file as a list of strings ### Changed - the formatter properly formats dictionaries (key-value pairs on their own line, always) diff --git a/lib/std b/lib/std index 93d2b42fe..4860f9d4d 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 93d2b42fede8ba23214dd20a5f9631ed598a023d +Subproject commit 4860f9d4d76fec2740acd51450bf56361b7c0049 diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.ark b/tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.ark new file mode 100644 index 000000000..3efb77d84 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.ark @@ -0,0 +1 @@ +(builtin__io:readLinesFile 1) diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.expected new file mode 100644 index 000000000..b9b9f2350 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.expected @@ -0,0 +1,7 @@ +Function io:readLinesFile expected 1 argument + -> filename (String) was of type Number + +In file tests/unittests/resources/DiagnosticsSuite/typeChecking/ioreadlinesfile_num.ark:1 + 1 | (builtin__io:readLinesFile 1) + | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ + 2 | From 92d1e68bfab35eb8e028be82aa2977602c1d7f10 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Mon, 8 Dec 2025 19:40:18 +0100 Subject: [PATCH 8/9] chore(tests): adding tests for argument attributes in ASTSuite and ParserSuite --- tests/unittests/resources/ASTSuite/args_attr.ark | 1 + tests/unittests/resources/ASTSuite/args_attr.json | 1 + .../resources/ParserSuite/failure/incomplete_arg_attr.ark | 1 + .../ParserSuite/failure/incomplete_arg_attr.expected | 5 +++++ 4 files changed, 8 insertions(+) create mode 100644 tests/unittests/resources/ASTSuite/args_attr.ark create mode 100644 tests/unittests/resources/ASTSuite/args_attr.json create mode 100644 tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.ark create mode 100644 tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.expected diff --git a/tests/unittests/resources/ASTSuite/args_attr.ark b/tests/unittests/resources/ASTSuite/args_attr.ark new file mode 100644 index 000000000..aba8aa8f7 --- /dev/null +++ b/tests/unittests/resources/ASTSuite/args_attr.ark @@ -0,0 +1 @@ +(fun (a (mut b) (ref c)) (+ a b c)) diff --git a/tests/unittests/resources/ASTSuite/args_attr.json b/tests/unittests/resources/ASTSuite/args_attr.json new file mode 100644 index 000000000..eb8f77fbb --- /dev/null +++ b/tests/unittests/resources/ASTSuite/args_attr.json @@ -0,0 +1 @@ +{"type": "Begin", "children": [{"type": "Fun", "args": [{"type": "Symbol", "name": "a"}, {"type": "MutArg", "name": "b"}, {"type": "RefArg", "name": "c"}], "body": {"type": "FunctionCall", "name": {"type": "Symbol", "name": "+"}, "args": [{"type": "Symbol", "name": "a"}, {"type": "Symbol", "name": "b"}, {"type": "Symbol", "name": "c"}]}}]} diff --git a/tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.ark b/tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.ark new file mode 100644 index 000000000..da9795f7c --- /dev/null +++ b/tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.ark @@ -0,0 +1 @@ +(fun ((mut)) ()) diff --git a/tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.expected b/tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.expected new file mode 100644 index 000000000..428fe3468 --- /dev/null +++ b/tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.expected @@ -0,0 +1,5 @@ +In file tests/unittests/resources/ParserSuite/failure/incomplete_arg_attr.ark:1 + 1 | (fun ((mut)) ()) + | ^~~~ + 2 | + Expected a symbol name for the attribute with modifier `mut' From 673cbc59a5359424d63f85370ff65547067b3eb3 Mon Sep 17 00:00:00 2001 From: Alexandre Plateau Date: Mon, 8 Dec 2025 19:42:45 +0100 Subject: [PATCH 9/9] chore(tests): adding IR generation tests for arg attributes --- .../resources/CompilerSuite/ir/args_attr.ark | 6 ++++ .../CompilerSuite/ir/args_attr.expected | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/unittests/resources/CompilerSuite/ir/args_attr.ark create mode 100644 tests/unittests/resources/CompilerSuite/ir/args_attr.expected diff --git a/tests/unittests/resources/CompilerSuite/ir/args_attr.ark b/tests/unittests/resources/CompilerSuite/ir/args_attr.ark new file mode 100644 index 000000000..f2839daa5 --- /dev/null +++ b/tests/unittests/resources/CompilerSuite/ir/args_attr.ark @@ -0,0 +1,6 @@ +(let foo (fun ((mut a) (ref b)) { + (set a (len b)) + a })) + +(let data [1 2 3]) +(print (foo 1 data)) diff --git a/tests/unittests/resources/CompilerSuite/ir/args_attr.expected b/tests/unittests/resources/CompilerSuite/ir/args_attr.expected new file mode 100644 index 000000000..fa58aa6d7 --- /dev/null +++ b/tests/unittests/resources/CompilerSuite/ir/args_attr.expected @@ -0,0 +1,29 @@ +page_0 + LOAD_CONST 0 + STORE 0 + LOAD_CONST 1 + LOAD_CONST 2 + LOAD_CONST 3 + LIST 3 + STORE 3 + PUSH_RETURN_ADDRESS L0 + PUSH_RETURN_ADDRESS L1 + LOAD_SYMBOL_BY_INDEX 0 + LOAD_CONST 3 + LOAD_SYMBOL_BY_INDEX 1 + CALL 2 +.L1: + BUILTIN 9 + CALL 1 +.L0: + HALT 0 + +page_1 + STORE 1 + STORE_REF 2 + LOAD_SYMBOL_BY_INDEX 0 + LEN 0 + SET_VAL 1 + LOAD_SYMBOL_BY_INDEX 1 + RET 0 + HALT 0