From 105daefc07ff57d7d88c2a873da9dd90332ce92f Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Thu, 12 Feb 2026 12:35:51 +0100 Subject: [PATCH 01/10] refactor(ast lowerer): uniformize error messages for invalid nodes --- include/Ark/Compiler/Lowerer/ASTLowerer.hpp | 10 ++++ .../Compiler/Lowerer/ASTLowerer.cpp | 47 +++++++++++++------ .../compileTime/apply_invalid_node.ark | 2 + .../compileTime/apply_invalid_node.expected | 6 +++ .../compileTime/apply_not_enough_args.ark | 1 + .../apply_not_enough_args.expected | 5 ++ .../function_shadowing_from_inside.expected | 2 +- .../compileTime/invalid_node_in_call.expected | 2 +- .../compileTime/invalid_node_in_list.expected | 2 +- .../compileTime/invalid_node_in_ope.expected | 2 +- .../invalid_node_in_tail_call.expected | 2 +- 11 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.expected diff --git a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp index d78ca766..8311769d 100644 --- a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp +++ b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp @@ -110,6 +110,14 @@ namespace Ark::internal Logger m_logger; + enum class ErrorKind + { + InvalidNodeMacro, + InvalidNodeNoReturnValue, + InvalidNodeInOperatorNoReturnValue, + InvalidNodeInTailCallNoReturnValue + }; + Page createNewCodePage(const bool temp = false) noexcept { if (!temp) @@ -226,6 +234,8 @@ namespace Ark::internal */ [[noreturn]] static void buildAndThrowError(const std::string& message, const Node& node); + [[noreturn]] static void makeError(ErrorKind kind, const Node& node, const std::string& additional_ctx); + /** * @brief Compile an expression (a node) recursively * diff --git a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp index 79f3e7fb..025fe83f 100644 --- a/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp +++ b/src/arkreactor/Compiler/Lowerer/ASTLowerer.cpp @@ -168,6 +168,30 @@ namespace Ark::internal throw CodeError(message, CodeErrorContext(node.filename(), node.position())); } + void ASTLowerer::makeError(const ErrorKind kind, const Node& node, const std::string& additional_ctx) + { + const std::string invalid_node_msg = "The given node doesn't return a value, and thus can't be used as an expression."; + + switch (kind) + { + case ErrorKind::InvalidNodeMacro: + buildAndThrowError(fmt::format("Invalid node ; if it was computed by a macro, check that a node is returned"), node); + break; + + case ErrorKind::InvalidNodeNoReturnValue: + buildAndThrowError(fmt::format("Invalid node inside call to `{}'. {}", additional_ctx, invalid_node_msg), node); + break; + + case ErrorKind::InvalidNodeInTailCallNoReturnValue: + buildAndThrowError(fmt::format("Invalid node inside tail call to `{}'. {}", additional_ctx, invalid_node_msg), node); + break; + + case ErrorKind::InvalidNodeInOperatorNoReturnValue: + buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'. {}", additional_ctx, invalid_node_msg), node); + break; + } + } + void ASTLowerer::compileExpression(Node& x, const Page p, const bool is_result_unused, const bool is_terminal) { // register symbols @@ -331,7 +355,7 @@ namespace Ark::internal if (nodeProducesOutput(node)) compileExpression(node, p, false, false); else - buildAndThrowError(fmt::format("Invalid node inside call to {}", name), node); + makeError(ErrorKind::InvalidNodeNoReturnValue, node, name); } // put inst and number of arguments @@ -383,7 +407,7 @@ namespace Ark::internal if (nodeProducesOutput(node)) compileExpression(node, p, false, false); else - buildAndThrowError("Invalid node inside call to apply", node); + makeError(ErrorKind::InvalidNodeNoReturnValue, node, "apply"); } page(p).emplace_back(APPLY); // patch the PUSH_RETURN_ADDRESS instruction with the return location (IP=CALL instruction IP) @@ -437,7 +461,7 @@ namespace Ark::internal 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); if (x.constList().size() != 3) - buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x); + makeError(ErrorKind::InvalidNodeMacro, x, ""); // capture, if needed std::size_t capture_inst_count = 0; @@ -517,13 +541,13 @@ namespace Ark::internal if (const auto sym = x.constList()[1]; sym.nodeType() != NodeType::Symbol) buildAndThrowError(fmt::format("Expected a symbol, got a {}", typeToString(sym)), sym); if (x.constList().size() != 3) - buildAndThrowError("Invalid node ; if it was computed by a macro, check that a node is returned", x); + makeError(ErrorKind::InvalidNodeMacro, x, ""); const std::string name = x.constList()[1].string(); uint16_t i = addSymbol(x.constList()[1]); if (!m_opened_vars.empty() && m_opened_vars.top() == name) - buildAndThrowError("Can not define a variable using the same name as the function it is defined inside", x); + buildAndThrowError("Can not define a variable using the same name as the function it is defined inside. You need to rename the function or the variable", x); const bool is_function = x.constList()[2].isFunction(); if (is_function) @@ -553,7 +577,7 @@ namespace Ark::internal 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); + makeError(ErrorKind::InvalidNodeMacro, x, ""); m_locals_locator.createScope(); page(p).emplace_back(CREATE_SCOPE); @@ -625,14 +649,7 @@ namespace Ark::internal compileExpression(value, p, false, false); } else - { - std::string message; - if (is_tail_call) - message = fmt::format("Invalid node inside tail call to `{}'", node.repr()); - else - message = fmt::format("Invalid node inside call to `{}'", node.repr()); - buildAndThrowError(message, value); - } + makeError(is_tail_call ? ErrorKind::InvalidNodeInTailCallNoReturnValue : ErrorKind::InvalidNodeNoReturnValue, value, node.repr()); } } @@ -727,7 +744,7 @@ namespace Ark::internal if (nodeProducesOutput(x.constList()[index]) || is_breakpoint) compileExpression(x.list()[index], p, false, false); else - buildAndThrowError(fmt::format("Invalid node inside call to operator `{}'", node.repr()), x.constList()[index]); + makeError(ErrorKind::InvalidNodeInOperatorNoReturnValue, x.constList()[index], node.repr()); if (!is_breakpoint) exp_count++; diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.ark new file mode 100644 index 00000000..182d8853 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.ark @@ -0,0 +1,2 @@ +(mut b []) +(apply print (append! b 5)) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.expected new file mode 100644 index 00000000..db280353 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.expected @@ -0,0 +1,6 @@ +In file tests/unittests/resources/DiagnosticsSuite/compileTime/apply_invalid_node.ark:2 + 1 | (mut b []) + 2 | (apply print (append! b 5)) + | ^~~~~~~~~~~~~ + 3 | + Invalid node inside call to `apply'. The given node doesn't return a value, and thus can't be used as an expression. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.ark new file mode 100644 index 00000000..beeec442 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.ark @@ -0,0 +1 @@ +(apply) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.expected new file mode 100644 index 00000000..19fadf0a --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.expected @@ -0,0 +1,5 @@ +In file tests/unittests/resources/DiagnosticsSuite/compileTime/apply_not_enough_args.ark:1 + 1 | (apply) + | ^~~~~ + 2 | + Expected 2 arguments (function, arguments) for apply, got 0 diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/function_shadowing_from_inside.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/function_shadowing_from_inside.expected index 2367e87c..649caddc 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/function_shadowing_from_inside.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/function_shadowing_from_inside.expected @@ -6,4 +6,4 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/function_shadowin | ^~~~~~~~~~ 7 | 8 | (foo) - Can not define a variable using the same name as the function it is defined inside + Can not define a variable using the same name as the function it is defined inside. You need to rename the function or the variable diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_call.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_call.expected index a4a2e856..acf4b3a8 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_call.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_call.expected @@ -3,4 +3,4 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_c 2 | (foo {}) | ^~ 3 | - Invalid node inside call to `foo' + Invalid node inside call to `foo'. The given node doesn't return a value, and thus can't be used as an expression. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_list.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_list.expected index f34c06e6..6c22d23d 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_list.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_list.expected @@ -2,4 +2,4 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_l 1 | [(mut c 1)] | ^~~~~~~~ 2 | - Invalid node inside call to list + Invalid node inside call to `list'. The given node doesn't return a value, and thus can't be used as an expression. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_ope.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_ope.expected index cf32a8c9..33b27214 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_ope.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_ope.expected @@ -2,4 +2,4 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_o 1 | (+ {} 1) | ^~ 2 | - Invalid node inside call to operator `+' + Invalid node inside call to operator `+'. The given node doesn't return a value, and thus can't be used as an expression. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_tail_call.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_tail_call.expected index e902d20f..cc4ee741 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_tail_call.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_tail_call.expected @@ -3,4 +3,4 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/invalid_node_in_t | ^~ 2 | (foo 1) 3 | - Invalid node inside tail call to `foo' + Invalid node inside tail call to `foo'. The given node doesn't return a value, and thus can't be used as an expression. From df58e5dce174a72c0c0ae8dec7349e8acc1932b2 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Thu, 12 Feb 2026 13:00:21 +0100 Subject: [PATCH 02/10] chore(tests): add more tests for empty? and add builtins --- .../typeChecking/builtin_add_nothing.ark | 1 + .../typeChecking/builtin_add_nothing.expected | 18 ++++++++++++++++++ .../resources/LangSuite/operators-tests.ark | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.expected diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.ark b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.ark new file mode 100644 index 00000000..a572d4fa --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.ark @@ -0,0 +1 @@ +(print (apply + [])) diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.expected new file mode 100644 index 00000000..cd6a36e8 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.expected @@ -0,0 +1,18 @@ +Function + expected at least 1 argument but got 0 +Call + ↳ (+) +Signature + ↳ (+ a) +Arguments + → variadic `a' (expected Number) was not provided + +Alternative 2: +Signature + ↳ (+ a) +Arguments + → variadic `a' (expected String) was not provided + +In file tests/unittests/resources/DiagnosticsSuite/typeChecking/builtin_add_nothing.ark:1 + 1 | (print (apply + [])) + | ^~~~~~~~~~~~~~~~~~~ + 2 | diff --git a/tests/unittests/resources/LangSuite/operators-tests.ark b/tests/unittests/resources/LangSuite/operators-tests.ark index 504a5f6c..954d82e9 100644 --- a/tests/unittests/resources/LangSuite/operators-tests.ark +++ b/tests/unittests/resources/LangSuite/operators-tests.ark @@ -133,6 +133,8 @@ (test:eq (apply empty? ["abc"]) false) (test:eq (apply empty? [""]) (empty? "")) (test:eq (apply empty? [""]) true) + (test:eq (apply empty? [nil]) (empty? nil)) + (test:eq (apply empty? [nil]) true) (test:eq (apply empty? [(dict)]) ($as-is (empty? (dict)))) (test:eq (apply empty? [(dict)]) true) (test:eq (apply empty? [(dict "a" 1)]) ($as-is (empty? (dict "a" 1)))) From 8ab7c7ba6dba7d7199b2e447107efaf0e60a7491 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Thu, 12 Feb 2026 13:55:37 +0100 Subject: [PATCH 03/10] feat(builtins): adding slice --- CHANGELOG.md | 1 + include/Ark/Builtins/Builtins.hpp | 2 + lib/std | 2 +- src/arkreactor/Builtins/Builtins.cpp | 91 +++++++++++++++++++ src/arkreactor/Compiler/AST/Parser.cpp | 3 +- .../ir/operators_as_builtins.expected | 2 +- .../compileTime/forbidden_name.expected | 2 +- .../compileTime/forbidden_name_bis.expected | 2 +- .../name_collision_with_builtin.expected | 2 +- .../compileTime/redefine.expected | 4 +- .../compileTime/set_const.expected | 4 +- .../runtime/backtrace_builtin.expected | 16 ++-- .../resources/LangSuite/builtins-tests.ark | 21 ++++- .../RosettaSuite/abbreviations_easy.ark | 2 +- .../RosettaSuite/middle_three_digits.ark | 5 +- 15 files changed, 133 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f27e804f..58d1f72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added - `apply` function: `(apply func [args...])`, to call a function with a set of arguments stored in a list. Works with functions, closures and builtins - `+`, `-`, `*`, `/` and many other operators can now be passed around, like builtins. This now works: `(list:reduce [1 2 3] +)`, where before we would get a compile time error about a "freestanding operator '+'" +- `slice` builtin, for strings and lists: `(slice data start end [step=1])` ## [4.2.0] - 2026-02-04 ### Breaking changes diff --git a/include/Ark/Builtins/Builtins.hpp b/include/Ark/Builtins/Builtins.hpp index 9ce02da2..2f7f0e15 100644 --- a/include/Ark/Builtins/Builtins.hpp +++ b/include/Ark/Builtins/Builtins.hpp @@ -137,6 +137,8 @@ namespace Ark::internal::Builtins ARK_BUILTIN(disassemble); } + ARK_BUILTIN(slice); + namespace Operators { ARK_BUILTIN(add); diff --git a/lib/std b/lib/std index 69ab3c56..13c7149d 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 69ab3c56405f15300cdc36b9190946dfcd24c77e +Subproject commit 13c7149dda94c60fcdb91ad4b325139b7a077966 diff --git a/src/arkreactor/Builtins/Builtins.cpp b/src/arkreactor/Builtins/Builtins.cpp index f532194b..4f0be8ab 100644 --- a/src/arkreactor/Builtins/Builtins.cpp +++ b/src/arkreactor/Builtins/Builtins.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include namespace Ark::internal::Builtins { @@ -21,6 +23,93 @@ namespace Ark::internal::Builtins extern const Value nan_ = Value(std::numeric_limits::signaling_NaN()); } + /** + * @name slice + * @brief Slice a list or string given a start, an end, and an optional step size + * @param container list or string + * @param start number, included + * @param end number, excluded + * @param step number, default 1 + * =begin + * (let d (dict "key" "value" 5 12)) + * (print d) # {key: value, 5: 12} + * =end + * @author https://github.com/SuperFola + */ + // cppcheck-suppress constParameterReference + Value slice(std::vector& n, VM* vm [[maybe_unused]]) + { + if (n.size() < 3 || n.size() > 4 || + (n[1].valueType() != ValueType::Number && n[1].valueType() != ValueType::Nil) || + (n[2].valueType() != ValueType::Number && n[2].valueType() != ValueType::Nil) || + (n.size() == 4 && n[3].valueType() != ValueType::Number)) + throw types::TypeCheckingError( + "slice", + { { types::Contract { + { types::Typedef("container", { ValueType::List, ValueType::String }), + types::Typedef("start", ValueType::Number), + types::Typedef("end", ValueType::Number) } }, + types::Contract { + { types::Typedef("container", { ValueType::List, ValueType::String }), + types::Typedef("start", ValueType::Number), + types::Typedef("end", ValueType::Number), + types::Typedef("step", ValueType::Number) } } } }, + n); + + const bool is_list = n[0].valueType() == ValueType::List; + const std::size_t container_size = is_list ? n[0].constList().size() : n[0].string().size(); + + const long start = n[1].valueType() == ValueType::Number ? static_cast(n[1].number()) : 0; + const std::size_t i = static_cast(start < 0 ? static_cast(container_size) + start : start); + + if (i >= container_size) + VM::throwVMError( + ErrorKind::Index, + fmt::format("{} out of range {} (length {})", start, n[0].toString(*vm, /* show_as_code= */ true), container_size)); + + const long end = n[2].valueType() == ValueType::Number ? static_cast(n[2].number()) : static_cast(container_size); + const std::size_t j = std::min(container_size, static_cast(end < 0 ? static_cast(container_size) + end : end)); + + const long step = n.size() == 4 ? static_cast(n[3].number()) : 1L; + if (step == 0) + throw Error("slice: a step of 0 is illegal"); + + std::size_t a = step > 0 ? i : j - 1; + const std::size_t b = step > 0 ? j : i; + const std::size_t increments = static_cast(std::abs(step)); + + if (is_list) + { + Value output(ValueType::List); + while ((step > 0 && a < b) || (step < 0 && a >= b)) + { + output.push_back(n[0].constList()[a]); + + if (step > 0) + a += increments; + else if (a >= increments) + a -= increments; + else + break; // step < 0 and 'increments' is bigger than 'a' + } + return output; + } + + std::string output; + while ((step > 0 && a < b) || (step < 0 && a >= b)) + { + output += n[0].string()[a]; + + if (step > 0) + a += increments; + else if (a >= increments) + a -= increments; + else + break; // step < 0 and 'increments' is bigger than 'a' + } + return Value(output); + } + extern const std::vector> builtins = { // builtin variables or constants { "false", falseSym }, @@ -110,6 +199,8 @@ namespace Ark::internal::Builtins // Bytecode { "disassemble", Value(Bytecode::disassemble) }, + { "slice", Value(slice) }, + // Operators that can also be used as builtins { "+", Value(Operators::add) }, { "-", Value(Operators::sub) }, diff --git a/src/arkreactor/Compiler/AST/Parser.cpp b/src/arkreactor/Compiler/AST/Parser.cpp index c6e778db..903bba10 100644 --- a/src/arkreactor/Compiler/AST/Parser.cpp +++ b/src/arkreactor/Compiler/AST/Parser.cpp @@ -192,10 +192,11 @@ namespace Ark::internal { // we haven't parsed anything while in "macro state" std::string symbol_name; + const auto value_pos = getCursor(); if (!name(&symbol_name)) errorWithNextToken(token + " needs a symbol"); - leaf->push_back(Node(NodeType::Symbol, symbol_name)); + leaf->push_back(positioned(Node(NodeType::Symbol, symbol_name), value_pos)); } comment = newlineOrComment(); diff --git a/tests/unittests/resources/CompilerSuite/ir/operators_as_builtins.expected b/tests/unittests/resources/CompilerSuite/ir/operators_as_builtins.expected index c5fc3d82..a82a0c73 100644 --- a/tests/unittests/resources/CompilerSuite/ir/operators_as_builtins.expected +++ b/tests/unittests/resources/CompilerSuite/ir/operators_as_builtins.expected @@ -1,5 +1,6 @@ page_0 PUSH_RETURN_ADDRESS L0 + BUILTIN 90 BUILTIN 89 BUILTIN 88 BUILTIN 87 @@ -21,7 +22,6 @@ page_0 BUILTIN 71 BUILTIN 70 BUILTIN 69 - BUILTIN 68 BUILTIN 9 CALL 22 .L0: diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name.expected index eab554eb..aa48794c 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name.expected @@ -1,5 +1,5 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name.ark:1 1 | (let print 5) - | ^~~~~~~~~~~~ + | ^~~~~ 2 | Can not use a reserved identifier ('print') as a constant name. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name_bis.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name_bis.expected index a466a127..feae2c61 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name_bis.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name_bis.expected @@ -1,5 +1,5 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/forbidden_name_bis.ark:1 1 | (mut print 5) - | ^~~~~~~~~~~~ + | ^~~~~ 2 | Can not use a reserved identifier ('print') as a variable name. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/name_collision_with_builtin.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/name_collision_with_builtin.expected index 1e28ea85..d50bbc5c 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/name_collision_with_builtin.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/name_collision_with_builtin.expected @@ -1,5 +1,5 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/package/list.ark:1 1 | (let random (fun (l) l)) - | ^~~~~~~~~~~~~~~~~~~~~~~ + | ^~~~~~ 2 | Can not use a reserved identifier ('random') as a constant name. diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/redefine.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/redefine.expected index cd025c4b..48e8ec4d 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/redefine.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/redefine.expected @@ -1,6 +1,6 @@ -In file tests/unittests/resources/DiagnosticsSuite/compileTime/redefine.ark:1 +In file tests/unittests/resources/DiagnosticsSuite/compileTime/redefine.ark:2 1 | (mut a 5) - | ^~~~~~~~ 2 | (let a 6) + | ^ 3 | MutabilityError: Can not use 'let' to redefine variable `a' diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/set_const.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/set_const.expected index 20077142..ffaa1ba3 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/set_const.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/set_const.expected @@ -1,6 +1,6 @@ -In file tests/unittests/resources/DiagnosticsSuite/compileTime/set_const.ark:1 +In file tests/unittests/resources/DiagnosticsSuite/compileTime/set_const.ark:2 1 | (let a 5) - | ^~~~~~~~ 2 | (set a 6) + | ^ 3 | MutabilityError: Can not set the constant `a' to 6 diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.expected index 3218c831..f6fc92c6 100644 --- a/tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.expected +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.expected @@ -6,16 +6,16 @@ Signature Arguments → `list' (expected List), got "live" (String) -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))) +In file /lib/std/List.ark:52 + 49 | # (list:sort [4 2 3]) # [1 2 4] + 50 | # =end + 51 | # @author https://github.com/SuperFola + 52 | (let sort (fun (_L) (builtin__list:sort _L))) | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 52 | - 53 | # @brief Generate a List of n copies of an element + 53 | + 54 | # @brief Generate a List of n copies of an element -[ 2] In function `list:sort' (/lib/std/List.ark:51) +[ 2] In function `list:sort' (/lib/std/List.ark:52) [ 1] In global scope (tests/unittests/resources/DiagnosticsSuite/runtime/backtrace_builtin.ark:3) Current scope variables values: diff --git a/tests/unittests/resources/LangSuite/builtins-tests.ark b/tests/unittests/resources/LangSuite/builtins-tests.ark index 7f6852cb..132d02e6 100644 --- a/tests/unittests/resources/LangSuite/builtins-tests.ark +++ b/tests/unittests/resources/LangSuite/builtins-tests.ark @@ -18,14 +18,25 @@ (append! rands (random 0 10)) (set i (+ 1 i)) }) (test:expect - (not (list:any rands (fun (e) (or (< e 0) (> e 10))))) + (list:none rands (fun (e) (or (< e 0) (> e 10)))) "should not find any number outside the given range") (let r (random)) (test:expect (and (<= r 2147483647) (>= r -2147483648))) }) (test:case "disassemble" { - (disassemble foo) - (let filename "tests/unittests/resources/DiagnosticsSuite/runtime/__arkscript__/assert.arkc") - (if (fileExists? filename) - (disassemble filename)) }) }) + (disassemble foo) + (let filename "tests/unittests/resources/DiagnosticsSuite/runtime/__arkscript__/assert.arkc") + (if (fileExists? filename) + (disassemble filename)) }) + + (test:case "slice" { + (test:eq (slice "abc" nil nil) "abc") + (test:eq (slice "abc" nil 1) "a") + (test:eq (slice "abc" nil 2) "ab") + (test:eq (slice "abc" nil 3) "abc") + (test:eq (slice "abc" nil 10) "abc") + (test:eq (slice "abcdef" 1 -1) "bcde") + (test:eq (slice "abcdef" 1 -1 30) "b") + (test:eq (slice "abcdef" 1 -1 -1) "edcb") + (test:eq (slice "abcdef" 1 -1 -30) "e") }) }) diff --git a/tests/unittests/resources/RosettaSuite/abbreviations_easy.ark b/tests/unittests/resources/RosettaSuite/abbreviations_easy.ark index da2282ce..28f53566 100644 --- a/tests/unittests/resources/RosettaSuite/abbreviations_easy.ark +++ b/tests/unittests/resources/RosettaSuite/abbreviations_easy.ark @@ -36,7 +36,7 @@ (and (<= min_len wlen) (<= wlen (len cmd)) - (= lower (string:slice cmd 0 wlen))) })) + (= lower (slice cmd 0 wlen))) })) (fun (cmd_with_len) (head cmd_with_len))) })) (let user_inputs (extract_cmds user_words)) diff --git a/tests/unittests/resources/RosettaSuite/middle_three_digits.ark b/tests/unittests/resources/RosettaSuite/middle_three_digits.ark index 74e86c7a..972fb7ad 100644 --- a/tests/unittests/resources/RosettaSuite/middle_three_digits.ark +++ b/tests/unittests/resources/RosettaSuite/middle_three_digits.ark @@ -1,10 +1,11 @@ (import std.Math) -(import std.String) (let middle_three_digits (fun (i) { (let s (toString (math:abs i))) (if (and (math:odd? (len s)) (>= (len s) 3)) - (string:slice s (- (/ (len s) 2) 1) 3) + { + (let j (- (/ (len s) 2) 1)) + (slice s j (+ j 3)) } nil) })) (import std.List :map) From 5e85abbfcbcadb457ad7771f6bf86c8fe67e1d0b Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Mon, 16 Feb 2026 18:28:46 +0100 Subject: [PATCH 04/10] fix(macro processor): ARK-332, arguments of macros being called in other macros were not applied all the way, causing bugs when code relied on partially applied macros, expecting macros to be fully applied --- examples/macros.ark | 4 +- include/Ark/Compiler/Macros/Executor.hpp | 3 +- include/Ark/Compiler/Macros/Processor.hpp | 9 ++-- lib/std | 2 +- src/arkreactor/Compiler/Macros/Executor.cpp | 4 +- .../Compiler/Macros/Executors/Conditional.cpp | 2 +- .../Compiler/Macros/Executors/Function.cpp | 20 ++++---- src/arkreactor/Compiler/Macros/Processor.cpp | 48 +++++++++++-------- .../compileTime/argcount_unknown_arg.ark | 2 +- .../compileTime/argcount_unknown_arg.expected | 5 -- .../compileTime/at_out_of_range.ark | 2 +- .../compileTime/at_out_of_range.expected | 2 +- .../compileTime/macro_empty_arity_error.ark | 2 +- .../macro_empty_arity_error.expected | 6 +-- .../compileTime/macro_head_arity_error.ark | 2 +- .../macro_head_arity_error.expected | 6 +-- .../compileTime/macro_len_arity_error.ark | 2 +- .../macro_len_arity_error.expected | 6 +-- .../macro_symcat_arity_error.expected | 4 +- .../compileTime/macro_tail_arity_error.ark | 2 +- .../macro_tail_arity_error.expected | 6 +-- .../resources/LangSuite/async-tests.ark | 20 ++++---- .../resources/LangSuite/macro-tests.ark | 30 ++++++------ .../RosettaSuite/extend_your_language.ark | 4 +- thirdparties/ut | 2 +- 25 files changed, 100 insertions(+), 95 deletions(-) diff --git a/examples/macros.ark b/examples/macros.ark index 81d2022f..6cc8ef8f 100644 --- a/examples/macros.ark +++ b/examples/macros.ark @@ -3,7 +3,7 @@ ($symcat sym x) }) (macro partial (func ...defargs) { - (macro bloc (suffix-dup a (- ($argcount func) (len defargs)))) + (macro bloc (suffix-dup a (- ($argcount func) ($len defargs)))) (fun (bloc) (func ...defargs bloc)) ($undef bloc) }) @@ -65,7 +65,7 @@ (print "Demonstrating a threading macro") (macro -> (arg fn1 ...fn) { - ($if (> (len fn) 0) + ($if (> ($len fn) 0) (-> (fn1 arg) ...fn) (fn1 arg)) }) diff --git a/include/Ark/Compiler/Macros/Executor.hpp b/include/Ark/Compiler/Macros/Executor.hpp index 3b1930b5..d8cde240 100644 --- a/include/Ark/Compiler/Macros/Executor.hpp +++ b/include/Ark/Compiler/Macros/Executor.hpp @@ -95,8 +95,9 @@ namespace Ark::internal * Proxy function for MacroProcessor::handleMacroNode * * @param node A node of type Macro + * @param depth */ - void handleMacroNode(Node& node) const; + void handleMacroNode(Node& node, unsigned depth) const; /** * @brief Check if a node can be evaluated to true diff --git a/include/Ark/Compiler/Macros/Processor.hpp b/include/Ark/Compiler/Macros/Processor.hpp index 0bd75376..ff765af1 100644 --- a/include/Ark/Compiler/Macros/Processor.hpp +++ b/include/Ark/Compiler/Macros/Processor.hpp @@ -116,8 +116,9 @@ namespace Ark::internal * @details Validate macros and register them by their name * * @param node A node of type Macro + * @param depth */ - void handleMacroNode(Node& node); + void handleMacroNode(Node& node, unsigned depth); /** * @brief Registers a function definition node @@ -154,7 +155,7 @@ namespace Ark::internal * @param is_expansion if the error message should switch from "Interpreting ..." to "When expanding ..." * @param kind the macro kind, empty by default (eg "operator", "condition") */ - void checkMacroArgCountEq(const Node& node, std::size_t expected, const std::string& name, bool is_expansion = false, const std::string& kind = ""); + void checkMacroArgCountEq(const Node& node, std::size_t expected, const std::string& name, bool is_expansion = false, const std::string& kind = "") const; /** * @brief Check if the given node has at least the provided argument count, otherwise throws an error @@ -164,7 +165,7 @@ namespace Ark::internal * @param name the name of the macro being applied * @param kind the macro kind, empty by default (eg "operator", "condition") */ - void checkMacroArgCountGe(const Node& node, std::size_t expected, const std::string& name, const std::string& kind = ""); + void checkMacroArgCountGe(const Node& node, std::size_t expected, const std::string& name, const std::string& kind = "") const; /** * @brief Evaluate only the macros @@ -183,7 +184,7 @@ namespace Ark::internal * @return true * @return false */ - bool isTruthy(const Node& node); + bool isTruthy(const Node& node) const; /** * @brief Throw a macro processing error diff --git a/lib/std b/lib/std index 13c7149d..c139926c 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit 13c7149dda94c60fcdb91ad4b325139b7a077966 +Subproject commit c139926cd0c4beda45d720f6542ea5ca447e0f1c diff --git a/src/arkreactor/Compiler/Macros/Executor.cpp b/src/arkreactor/Compiler/Macros/Executor.cpp index 427fcb57..588f327f 100644 --- a/src/arkreactor/Compiler/Macros/Executor.cpp +++ b/src/arkreactor/Compiler/Macros/Executor.cpp @@ -19,9 +19,9 @@ namespace Ark::internal m_processor->applyMacro(node, depth); } - void MacroExecutor::handleMacroNode(Node& node) const + void MacroExecutor::handleMacroNode(Node& node, const unsigned depth) const { - m_processor->handleMacroNode(node); + m_processor->handleMacroNode(node, depth); } bool MacroExecutor::isTruthy(const Node& node) const diff --git a/src/arkreactor/Compiler/Macros/Executors/Conditional.cpp b/src/arkreactor/Compiler/Macros/Executors/Conditional.cpp index 320a38c5..ac68f0d9 100644 --- a/src/arkreactor/Compiler/Macros/Executors/Conditional.cpp +++ b/src/arkreactor/Compiler/Macros/Executors/Conditional.cpp @@ -23,7 +23,7 @@ namespace Ark::internal if (node.nodeType() == NodeType::Macro) { - handleMacroNode(node); + handleMacroNode(node, depth + 1); applyMacroProxy(node, depth + 1); } diff --git a/src/arkreactor/Compiler/Macros/Executors/Function.cpp b/src/arkreactor/Compiler/Macros/Executors/Function.cpp index 8b511c27..1a938b3e 100644 --- a/src/arkreactor/Compiler/Macros/Executors/Function.cpp +++ b/src/arkreactor/Compiler/Macros/Executors/Function.cpp @@ -17,16 +17,16 @@ namespace Ark::internal { const Node& first = node.list()[0]; - // (macro name (args) body) + // macro corresponds to the following form: (macro name (args) body) if (const Node* macro = findNearestMacro(first.string()); macro != nullptr && macro->constList().size() == 3) { Node temp_body = macro->constList()[2]; - Node args = macro->constList()[1]; - const std::size_t args_needed = args.list().size(); + const Node args = macro->constList()[1]; + const std::size_t args_needed = args.constList().size(); const std::size_t args_given = node.constList().size() - 1; // remove the first (the name of the macro) const std::string macro_name = macro->constList()[0].string(); // thanks to the parser, we are guaranteed that the spread will be in last position, if any - const bool has_spread = args_needed > 0 && args.list().back().nodeType() == NodeType::Spread; + const bool has_spread = args_needed > 0 && args.constList().back().nodeType() == NodeType::Spread; // save the args given to the macro by giving them a name (from the macro args block), // and a value (in node.constList()) @@ -38,15 +38,15 @@ namespace Ark::internal if (j >= args_needed) break; - if (args.list()[j].nodeType() == NodeType::Symbol) + if (args.constList()[j].nodeType() == NodeType::Symbol) { - const std::string& arg_name = args.list()[j].string(); + const std::string& arg_name = args.constList()[j].string(); args_applied[arg_name] = node.constList()[i]; ++j; } - else if (args.list()[j].nodeType() == NodeType::Spread) + else if (args.constList()[j].nodeType() == NodeType::Spread) { - const std::string& arg_name = args.list()[j].string(); + const std::string& arg_name = args.constList()[j].string(); if (!args_applied.contains(arg_name)) { args_applied[arg_name] = Node(NodeType::List); @@ -61,8 +61,8 @@ namespace Ark::internal if (args_applied.size() + 1 == args_needed && has_spread) { // just a spread we didn't assign - args_applied[args.list().back().string()] = Node(NodeType::List); - args_applied[args.list().back().string()].push_back(getListNode()); + args_applied[args.constList().back().string()] = Node(NodeType::List); + args_applied[args.constList().back().string()].push_back(getListNode()); } if (args_given != args_needed && !has_spread) diff --git a/src/arkreactor/Compiler/Macros/Processor.cpp b/src/arkreactor/Compiler/Macros/Processor.cpp index d0a9e929..5cb6f8cd 100644 --- a/src/arkreactor/Compiler/Macros/Processor.cpp +++ b/src/arkreactor/Compiler/Macros/Processor.cpp @@ -47,7 +47,7 @@ namespace Ark::internal return m_ast; } - void MacroProcessor::handleMacroNode(Node& node) + void MacroProcessor::handleMacroNode(Node& node, const unsigned depth) { // a macro needs at least 2 nodes, name + value is the minimal form // this is guaranteed by the parser @@ -59,11 +59,16 @@ namespace Ark::internal if (node.constList().size() == 2) { assert(first_node.nodeType() == NodeType::Symbol && "Can not define a macro without a symbol"); - applyMacro(node.list()[1], 0); - node.list()[1] = evaluate(node.list()[1], 0, true); + // processNode is needed here to correctly expand macros when we have something like + // (macro arg_bloc (__suffix-dup arg length)) / + // (macro all_args (__replace_placeholders [arg_bloc] ...args)) <--------------* + // otherwise, 'arg_bloc' isn't expanded here, and we get in trouble + processNode(node.list()[1], depth + 1); + applyMacro(node.list()[1], depth + 1); + node.list()[1] = evaluate(node.list()[1], depth + 1, true); m_macros.back().add(first_node.string(), node); } - // ($ name (args) body) + // (macro name (args) body) else if (node.constList().size() == 3 && first_node.nodeType() == NodeType::Symbol) { assert(node.constList()[1].nodeType() == NodeType::List && "Invalid macro argument's list"); @@ -71,7 +76,7 @@ namespace Ark::internal } // in case we had a conditional, we need to evaluate and expand it else if (m_conditional_executor->canHandle(node)) - m_conditional_executor->applyMacro(node, 0); + m_conditional_executor->applyMacro(node, depth + 1); } // todo find a better way to do this @@ -111,7 +116,6 @@ namespace Ark::internal if (node.nodeType() == NodeType::List) { bool has_created = false; - // recursive call std::size_t i = 0; while (i < node.list().size()) { @@ -131,7 +135,9 @@ namespace Ark::internal m_macros.emplace_back(depth); } - handleMacroNode(child); + handleMacroNode(child, depth); + if (child.nodeType() != NodeType::Macro) + evaluate(child, depth); added_begin = isBeginNode(child) && !had_begin; } else // running on non-macros @@ -208,7 +214,7 @@ namespace Ark::internal return false; } - void MacroProcessor::checkMacroArgCountEq(const Node& node, std::size_t expected, const std::string& name, const bool is_expansion, const std::string& kind) + void MacroProcessor::checkMacroArgCountEq(const Node& node, std::size_t expected, const std::string& name, const bool is_expansion, const std::string& kind) const { const std::size_t argcount = node.constList().size(); if (argcount != expected + 1) @@ -235,7 +241,7 @@ namespace Ark::internal } } - void MacroProcessor::checkMacroArgCountGe(const Node& node, std::size_t expected, const std::string& name, const std::string& kind) + void MacroProcessor::checkMacroArgCountGe(const Node& node, std::size_t expected, const std::string& name, const std::string& kind) const { const std::size_t argcount = node.constList().size(); if (argcount < expected + 1) @@ -401,9 +407,9 @@ namespace Ark::internal } return getFalseNode(); } - else if (name == "len") + else if (name == "$len") { - checkMacroArgCountEq(node, 1, "len", true); + checkMacroArgCountEq(node, 1, "$len", true); if (Node& lst = node.list()[1]; lst.nodeType() == NodeType::List) // only apply len at compile time if we can { @@ -416,9 +422,9 @@ namespace Ark::internal } } } - else if (name == "empty?") + else if (name == "$empty?") { - checkMacroArgCountEq(node, 1, "empty?", true); + checkMacroArgCountEq(node, 1, "$empty?", true); if (Node& lst = node.list()[1]; lst.nodeType() == NodeType::List && isConstEval(lst)) { @@ -431,9 +437,9 @@ namespace Ark::internal else if (lst == getNilNode()) node.updateValueAndType(getTrueNode()); } - else if (name == "@") + else if (name == "$at") { - checkMacroArgCountEq(node, 2, "@"); + checkMacroArgCountEq(node, 2, "$at"); Node sublist = evaluate(node.list()[1], depth + 1, is_not_body); const Node idx = evaluate(node.list()[2], depth + 1, is_not_body); @@ -464,9 +470,9 @@ namespace Ark::internal return output; } } - else if (name == "head") + else if (name == "$head") { - checkMacroArgCountEq(node, 1, "head", true); + checkMacroArgCountEq(node, 1, "$head", true); if (node.list()[1].nodeType() == NodeType::List) { @@ -487,9 +493,9 @@ namespace Ark::internal node.updateValueAndType(getNilNode()); } } - else if (name == "tail") + else if (name == "$tail") { - checkMacroArgCountEq(node, 1, "tail", true); + checkMacroArgCountEq(node, 1, "$tail", true); if (node.list()[1].nodeType() == NodeType::List) { @@ -636,7 +642,7 @@ namespace Ark::internal return node; } - bool MacroProcessor::isTruthy(const Node& node) + bool MacroProcessor::isTruthy(const Node& node) const { if (node.nodeType() == NodeType::Symbol) { @@ -700,7 +706,7 @@ namespace Ark::internal if (node.isListLike() && node.list()[i].nodeType() == NodeType::List && !node.list()[i].list().empty()) { Node lst = node.constList()[i]; - Node first = lst.constList()[0]; + const Node first = lst.constList()[0]; if (first.nodeType() == NodeType::Keyword && first.keyword() == Keyword::Begin) { diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.ark index 2a55f7e3..ce9f3ade 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.ark +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.ark @@ -4,7 +4,7 @@ ($symcat sym x)}) (macro partial (func ...defargs) { - (macro bloc (suffix-dup a (- ($argcount func) (len defargs)))) + (macro bloc (suffix-dup a (- ($argcount func) ($len defargs)))) (fun (bloc) (func ...defargs bloc)) ($undef bloc)}) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.expected index de13f162..f84e0b65 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.expected @@ -1,9 +1,4 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/argcount_unknown_arg.ark:13 - | ┌─ macro expansion started here - 1 | (macro suffix-dup (sym x) { - 2 | ($if (> x 1) - 3 | (suffix-dup sym (- x 1))) - ... | 10 | 11 | (let test_func (fun (a b c) (* a b c))) 12 | (let test_func1 (partial test_func 1)) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.ark index 12122fb3..3eb6729f 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.ark +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.ark @@ -1,2 +1,2 @@ -(macro last (...args) (print args " => " (@ args -9))) +(macro last (...args) (print args " => " ($at args -9))) (last 1 5 6 7 8) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.expected index 8d3f1406..2e792940 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.expected @@ -1,5 +1,5 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/at_out_of_range.ark:1 - 1 | (macro last (...args) (print args " => " (@ args -9))) + 1 | (macro last (...args) (print args " => " ($at args -9))) | │ └─ error | │ | └─ macro expansion started here diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.ark index 0d865d0e..8ef9b95f 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.ark +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.ark @@ -1,2 +1,2 @@ -(macro a (empty? 1 2)) +(macro a ($empty? 1 2)) (print a) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.expected index 366515d0..eec8f99d 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.expected @@ -1,6 +1,6 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_empty_arity_error.ark:1 - 1 | (macro a (empty? 1 2)) - | ^~~~~~~~~~~~ + 1 | (macro a ($empty? 1 2)) + | ^~~~~~~~~~~~~ 2 | (print a) 3 | - When expanding `empty?' inside a macro, got 2 arguments, expected 1 + When expanding `$empty?' inside a macro, got 2 arguments, expected 1 diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.ark index 73fe70d4..7d6bdf60 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.ark +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.ark @@ -1,2 +1,2 @@ -(macro a (head 1 2)) +(macro a ($head 1 2)) (print a) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.expected index c76c87ae..55888b85 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.expected @@ -1,6 +1,6 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_arity_error.ark:1 - 1 | (macro a (head 1 2)) - | ^~~~~~~~~~ + 1 | (macro a ($head 1 2)) + | ^~~~~~~~~~~ 2 | (print a) 3 | - When expanding `head' inside a macro, got 2 arguments, expected 1 + When expanding `$head' inside a macro, got 2 arguments, expected 1 diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.ark index e34b7877..47b5cb70 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.ark +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.ark @@ -1,2 +1,2 @@ -(macro a (len 1 2)) +(macro a ($len 1 2)) (print a) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.expected index ca0ba95f..a4e33d00 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.expected @@ -1,6 +1,6 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_arity_error.ark:1 - 1 | (macro a (len 1 2)) - | ^~~~~~~~~ + 1 | (macro a ($len 1 2)) + | ^~~~~~~~~~ 2 | (print a) 3 | - When expanding `len' inside a macro, got 2 arguments, expected 1 + When expanding `$len' inside a macro, got 2 arguments, expected 1 diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_arity_error.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_arity_error.expected index 5583bf2a..2fc70a5e 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_arity_error.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_arity_error.expected @@ -1,6 +1,8 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_arity_error.ark:1 1 | (macro a { ($symcat b) }) - | ^~~~~~~~~~~ + | │ └─ error + | │ + | └─ macro expansion started here 2 | (print a) 3 | When expanding `$symcat', expected at least 2 arguments, got 1 arguments diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.ark index 2a2636d0..f6952f0a 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.ark +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.ark @@ -1,2 +1,2 @@ -(macro a (tail 1 2)) +(macro a ($tail 1 2)) (print a) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.expected index 15c74ad9..053798c5 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.expected @@ -1,6 +1,6 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_arity_error.ark:1 - 1 | (macro a (tail 1 2)) - | ^~~~~~~~~~ + 1 | (macro a ($tail 1 2)) + | ^~~~~~~~~~~ 2 | (print a) 3 | - When expanding `tail' inside a macro, got 2 arguments, expected 1 + When expanding `$tail' inside a macro, got 2 arguments, expected 1 diff --git a/tests/unittests/resources/LangSuite/async-tests.ark b/tests/unittests/resources/LangSuite/async-tests.ark index c82c285b..1578ece2 100644 --- a/tests/unittests/resources/LangSuite/async-tests.ark +++ b/tests/unittests/resources/LangSuite/async-tests.ark @@ -51,15 +51,15 @@ (test:case "execution contexts get reused" { # this forces the VM to create more than 5 execution context, # and delete the 6th - (async sleep 128) - (async sleep 128) - (async sleep 128) - (async sleep 128) - (async sleep 128) - (sleep 250) # wait for the async call to complete + (async sleep 12) + (async sleep 12) + (async sleep 12) + (async sleep 12) + (async sleep 12) + (sleep 25) # wait for the async call to complete # we have 5 contexts here, and 4 that are free to be (re)used - (async sleep 128) (sleep 200) - (async sleep 128) (sleep 200) - (async sleep 128) (sleep 200) - (async sleep 128) (sleep 200) }) }) + (async sleep 12) (sleep 20) + (async sleep 12) (sleep 20) + (async sleep 12) (sleep 20) + (async sleep 12) (sleep 20) }) }) diff --git a/tests/unittests/resources/LangSuite/macro-tests.ark b/tests/unittests/resources/LangSuite/macro-tests.ark index 3b41e82f..e889a362 100644 --- a/tests/unittests/resources/LangSuite/macro-tests.ark +++ b/tests/unittests/resources/LangSuite/macro-tests.ark @@ -7,7 +7,7 @@ (let magic_func (fun ((suffix-dup a 3)) (- a1 a2 a3))) (macro partial (func ...defargs) { - (macro bloc (suffix-dup a (- ($argcount func) (len defargs)))) + (macro bloc (suffix-dup a (- ($argcount func) ($len defargs)))) (fun (bloc) (func ...defargs bloc)) ($undef bloc)}) @@ -43,11 +43,11 @@ (test:eq h (/ 12 6)) }) (test:case "node manipulation" { - (macro node_tail () (tail (begin 1 2 3))) - (macro length () (len (fun () 5))) - (macro not_empty_node () (empty? (fun () ()))) - (macro empty_node () (empty? ())) - (macro empty_node_bis (empty? ())) + (macro node_tail () ($tail (begin 1 2 3))) + (macro length () ($len (fun () 5))) + (macro not_empty_node () ($empty? (fun () ()))) + (macro empty_node () ($empty? ())) + (macro empty_node_bis ($empty? ())) (test:eq (length) 3) (test:eq (not_empty_node) false) @@ -68,8 +68,8 @@ (test:expect ($if (> nice_value 14) false true)) (test:expect ($if (<= nice_value 12) true false)) (test:expect ($if (>= nice_value 12) true false)) - (test:expect ($if (@ [true false] 0) true false)) - (test:expect ($if (@ [true false] -2) true false)) + (test:expect ($if ($at [true false] 0) true false)) + (test:expect ($if ($at [true false] -2) true false)) (test:expect ($if 1 true false)) ($if true { (macro in_if_1 true) @@ -95,7 +95,7 @@ (test:eq val 6 "val should still resolve to 6")} (test:case "macro expansion" { - (macro bar (a ...args) (+ a (len args))) + (macro bar (a ...args) (+ a ($len args))) (test:eq (bar 1) 1) (test:eq (bar 2 3) 3) (test:eq (bar 4 5 6) 6) @@ -106,23 +106,23 @@ (test:eq (egg 0 1) 1) (test:eq (egg 0 0 0 1) 3) - (macro h (...args) (head args)) + (macro h (...args) ($head args)) (test:eq (h) nil) (test:eq (h 1) 1) (test:eq (h 1 2) 1) - (macro g (...args) (tail args)) + (macro g (...args) ($tail args)) (test:eq (g) []) (test:eq (g 1) []) (test:eq (g 1 2) [2]) (test:eq (g 1 2 3) [2 3]) - (macro one (...args) (@ args 1)) + (macro one (...args) ($at args 1)) (test:eq (one 1 2) 2) (test:eq (one 1 3 4) 3) (test:eq (one 1 5 6 7 8) 5) - (macro last (...args) (@ args -1)) + (macro last (...args) ($at args -1)) (test:eq (last 1 2) 2) (test:eq (last 1 3 4) 4) (test:eq (last 1 5 6 7 8) 8) }) @@ -144,8 +144,8 @@ (test:eq (type foo) "Function") (test:eq (foo 2 3) 5) - (macro get_symbol (bloc) (@ bloc 1)) - (macro define (bloc) (let (get_symbol bloc) (@ bloc 2))) + (macro get_symbol (bloc) ($at bloc 1)) + (macro define (bloc) (let (get_symbol bloc) ($at bloc 2))) (define (let a 12)) (test:eq a 12) }) diff --git a/tests/unittests/resources/RosettaSuite/extend_your_language.ark b/tests/unittests/resources/RosettaSuite/extend_your_language.ark index 5998248d..5721b636 100644 --- a/tests/unittests/resources/RosettaSuite/extend_your_language.ark +++ b/tests/unittests/resources/RosettaSuite/extend_your_language.ark @@ -1,6 +1,6 @@ (macro if2 (conds bothConditionsAreTrue firstConditionIsTrue secondConditionIsTrue noConditionIsTrue) { - (mut result1 (head conds)) - (mut result2 (@ conds 1)) + (mut result1 ($head conds)) + (mut result2 ($at conds 1)) (if (and result1 result2) bothConditionsAreTrue (if result1 diff --git a/thirdparties/ut b/thirdparties/ut index b390c666..59a9beba 160000 --- a/thirdparties/ut +++ b/thirdparties/ut @@ -1 +1 @@ -Subproject commit b390c6660274b21c2846e16d812837b7c4d593b0 +Subproject commit 59a9beba0763dbb45b3cc68e4cf484c659319a97 From 380de69b77a80e1be8e0ff37cf512f80370693f8 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Mon, 16 Feb 2026 19:05:21 +0100 Subject: [PATCH 05/10] chore: add more tests for slice builtin --- tests/unittests/resources/LangSuite/builtins-tests.ark | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unittests/resources/LangSuite/builtins-tests.ark b/tests/unittests/resources/LangSuite/builtins-tests.ark index 132d02e6..d11e2552 100644 --- a/tests/unittests/resources/LangSuite/builtins-tests.ark +++ b/tests/unittests/resources/LangSuite/builtins-tests.ark @@ -39,4 +39,8 @@ (test:eq (slice "abcdef" 1 -1) "bcde") (test:eq (slice "abcdef" 1 -1 30) "b") (test:eq (slice "abcdef" 1 -1 -1) "edcb") - (test:eq (slice "abcdef" 1 -1 -30) "e") }) }) + (test:eq (slice "abcdef" 1 -1 -30) "e") + (test:eq (slice "abcdef" nil nil 2) "ace") + (test:eq (slice "abcdef" nil nil -2) "fdb") + (test:eq (slice "abcdef" 1 nil 2) "bdf") + (test:eq (slice "abcdef" 1 nil -2) "fdb") }) }) From a62b803d66e0c76dff84027b6a9100cf1d839913 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Tue, 17 Feb 2026 19:21:29 +0100 Subject: [PATCH 06/10] feat(macro processor): ARK-332, all predefined macros can run on any code, no need for a node to match some obscur criteria --- docs/arkdoc/Macros.txt | 124 ++++++++++++++++++ include/Ark/Compiler/Macros/Processor.hpp | 11 +- lib/std | 2 +- src/arkreactor/Compiler/Macros/Processor.cpp | 56 +------- .../resources/LangSuite/macro-tests.ark | 2 +- .../resources/LangSuite/operators-tests.ark | 8 +- 6 files changed, 136 insertions(+), 67 deletions(-) create mode 100644 docs/arkdoc/Macros.txt diff --git a/docs/arkdoc/Macros.txt b/docs/arkdoc/Macros.txt new file mode 100644 index 00000000..85e49d43 --- /dev/null +++ b/docs/arkdoc/Macros.txt @@ -0,0 +1,124 @@ +--# +* @name $undef +* @brief Delete a given macro in the nearest scope +* @param name macro name +* =begin +* (macro a 5) +* ($undef a) +* (print a) # will fail, as 'a' doesn't exist anymore +* =end +#-- + +--# +* @name $argcount +* @brief Retrieve at compile time the number of arguments taken by a given function. +* @details The function must have been defined before using `$argcount`, or must be an anonymous function: `($argcount (fun (a b c) ()))`, `($argcount my-function)`. +* @param node +* =begin +* (let foo (fun (a b) (+ a b))) +* (print ($argcount foo)) # 2 +* =end +#-- + +--# +* @name $symcat +* @brief Create a new symbol by concatenating a symbol with numbers, strings and/or other symbols +* @param symbol +* @param args... numbers, strings or symbols +* =begin +* (macro foo () (let ($symcat a 5) 6)) +* (foo) +* (print a5) +* =end +#-- + +--# +* @name $repr +* @brief Return the AST representation of a given node, as a string. +* @details Indentation, newlines and comments are not preserved. +* @param node +* =begin +* ($repr foobar) # will return "foobar" +* ($repr (fun () (+ 1 2 3))) # will return "(fun () (+ 1 2 3))" +* =end +#-- + +--# +* @name $as-is +* @brief Use a given node as it is, without evaluating it any further in the macro context. Useful to stop the evaluation of arguments passed to a function macro. +* @param node +#-- + +--# +* @name $type +* @brief Return the type of a given node, as a string. +* @param node +* =begin +* (print ($type foobar)) # Symbol +* (print ($type (fun () (+ 1 2 3)))) # List +* =end +#-- + +--# +* @name $len +* @brief Return the length of a node +* @param node +* =begin +* (macro -> (arg fn1 ...fn) { +* # we use $len to check if we have more functions to apply +* ($if (> ($len fn) 0) +* (-> (fn1 arg) ...fn) +* (fn1 arg)) }) +* +* (macro foo () ($len (fun () ()))) +* (print (foo)) # 3 +* =end +#-- + +--# +* @name $empty? +* @brief Check if a node is empty. An empty list, `[]` or `(list)`, is considered empty. +* @param node +* =begin +* (macro not_empty_node () ($empty? (fun () ()))) +* (print (not_empty_node)) # false +* =end +#-- + +--# +* @name $head +* @brief Return the head node in a list of nodes. The head of a `[1 2 3]` / `(list 1 2 3)` disregards the `list` and returns 1. +* @param node +* =begin +* (macro h (...args) ($head args)) +* (print (h)) # nil +* (print (h 1)) # 1 +* (print (h 1 2)) # 1 +* =end +#-- + +--# +* @name $tail +* @brief Return the tails nodes in a list of nodes, as a `(list ...)` +* @param node +* =begin +* (macro g (...args) ($tail args)) +* (print (g)) # [] +* (print (g 1)) # [] +* (print (g 1 2)) # [2] +* (print (g 1 2 3)) # [2 3] +* =end +#-- + +--# +* @name $at +* @brief Return the node at a given index in a list of nodes +* @param node +* @param index must be a number +* =begin +* (macro one (...args) ($at args 1)) +* (print (one 1 2)) # 2 +* (print (one 1 3 4)) # 3 +* (print (one 1 5 6 7 8)) # 5 +* =end +#-- diff --git a/include/Ark/Compiler/Macros/Processor.hpp b/include/Ark/Compiler/Macros/Processor.hpp index ff765af1..3c16083c 100644 --- a/include/Ark/Compiler/Macros/Processor.hpp +++ b/include/Ark/Compiler/Macros/Processor.hpp @@ -102,15 +102,6 @@ namespace Ark::internal */ static void removeBegin(Node& node, std::size_t i); - /** - * @brief Check if a node can be evaluated at compile time - * - * @param node - * @return true - * @return false - */ - [[nodiscard]] bool isConstEval(const Node& node) const; - /** * @brief Registers macros based on their type, expand conditional macros * @details Validate macros and register them by their name @@ -184,7 +175,7 @@ namespace Ark::internal * @return true * @return false */ - bool isTruthy(const Node& node) const; + [[nodiscard]] bool isTruthy(const Node& node) const; /** * @brief Throw a macro processing error diff --git a/lib/std b/lib/std index c139926c..10ab24c1 160000 --- a/lib/std +++ b/lib/std @@ -1 +1 @@ -Subproject commit c139926cd0c4beda45d720f6542ea5ca447e0f1c +Subproject commit 10ab24c1dd1839b9c2bbd062b96d9bb9eebb6bc9 diff --git a/src/arkreactor/Compiler/Macros/Processor.cpp b/src/arkreactor/Compiler/Macros/Processor.cpp index 5cb6f8cd..99be9702 100644 --- a/src/arkreactor/Compiler/Macros/Processor.cpp +++ b/src/arkreactor/Compiler/Macros/Processor.cpp @@ -413,20 +413,17 @@ namespace Ark::internal if (Node& lst = node.list()[1]; lst.nodeType() == NodeType::List) // only apply len at compile time if we can { - if (isConstEval(lst)) - { - if (!lst.list().empty() && lst.list()[0] == getListNode()) - node.updateValueAndType(Node(static_cast(lst.list().size()) - 1)); - else - node.updateValueAndType(Node(static_cast(lst.list().size()))); - } + if (!lst.list().empty() && lst.list()[0] == getListNode()) + node.updateValueAndType(Node(static_cast(lst.list().size()) - 1)); + else + node.updateValueAndType(Node(static_cast(lst.list().size()))); } } else if (name == "$empty?") { checkMacroArgCountEq(node, 1, "$empty?", true); - if (Node& lst = node.list()[1]; lst.nodeType() == NodeType::List && isConstEval(lst)) + if (Node& lst = node.list()[1]; lst.nodeType() == NodeType::List) { // only apply len at compile time if we can if (!lst.list().empty() && lst.list()[0] == getListNode()) @@ -722,49 +719,6 @@ namespace Ark::internal } } - bool MacroProcessor::isConstEval(const Node& node) const - { - switch (node.nodeType()) - { - case NodeType::Symbol: - { - const auto it = std::ranges::find(Language::operators, node.string()); - const auto it2 = std::ranges::find_if(Builtins::builtins, - [&node](const std::pair& element) -> bool { - return node.string() == element.first; - }); - - return it != Language::operators.end() || - it2 != Builtins::builtins.end() || - findNearestMacro(node.string()) != nullptr || - node.string() == "list" || - node.string() == "nil"; - } - - case NodeType::List: - return std::ranges::all_of(node.constList(), [this](const Node& child) { - return isConstEval(child); - }); - - case NodeType::MutArg: - case NodeType::RefArg: - case NodeType::Capture: - case NodeType::Field: - return false; - - case NodeType::Keyword: - case NodeType::String: - case NodeType::Number: - case NodeType::Macro: - case NodeType::Spread: - case NodeType::Namespace: - case NodeType::Unused: - return true; - } - - return false; - } - void MacroProcessor::throwMacroProcessingError(const std::string& message, const Node& node) const { const std::optional maybe_context = [this]() -> std::optional { diff --git a/tests/unittests/resources/LangSuite/macro-tests.ark b/tests/unittests/resources/LangSuite/macro-tests.ark index e889a362..d1e19071 100644 --- a/tests/unittests/resources/LangSuite/macro-tests.ark +++ b/tests/unittests/resources/LangSuite/macro-tests.ark @@ -47,7 +47,7 @@ (macro length () ($len (fun () 5))) (macro not_empty_node () ($empty? (fun () ()))) (macro empty_node () ($empty? ())) - (macro empty_node_bis ($empty? ())) + (macro empty_node_bis ($empty? [])) (test:eq (length) 3) (test:eq (not_empty_node) false) diff --git a/tests/unittests/resources/LangSuite/operators-tests.ark b/tests/unittests/resources/LangSuite/operators-tests.ark index 954d82e9..ee4d29eb 100644 --- a/tests/unittests/resources/LangSuite/operators-tests.ark +++ b/tests/unittests/resources/LangSuite/operators-tests.ark @@ -119,9 +119,9 @@ (test:eq (apply len ["abc"]) 3) (test:eq (apply len [""]) (len "")) (test:eq (apply len [""]) 0) - (test:eq (apply len [(dict)]) ($as-is (len (dict)))) + (test:eq (apply len [(dict)]) (len (dict))) (test:eq (apply len [(dict)]) 0) - (test:eq (apply len [(dict "a" 1)]) ($as-is (len (dict "a" 1)))) + (test:eq (apply len [(dict "a" 1)]) (len (dict "a" 1))) (test:eq (apply len [(dict "a" 1)]) 1) }) (test:case "empty?" { @@ -135,9 +135,9 @@ (test:eq (apply empty? [""]) true) (test:eq (apply empty? [nil]) (empty? nil)) (test:eq (apply empty? [nil]) true) - (test:eq (apply empty? [(dict)]) ($as-is (empty? (dict)))) + (test:eq (apply empty? [(dict)]) (empty? (dict))) (test:eq (apply empty? [(dict)]) true) - (test:eq (apply empty? [(dict "a" 1)]) ($as-is (empty? (dict "a" 1)))) + (test:eq (apply empty? [(dict "a" 1)]) (empty? (dict "a" 1))) (test:eq (apply empty? [(dict "a" 1)]) false) }) (test:case "nil?" { From 12d4e1a0dfdca3e74b48a3ca728cd60446964531 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Tue, 17 Feb 2026 20:02:07 +0100 Subject: [PATCH 07/10] feat(macro processor): ARK-332, type-checking builtin macros arguments --- CHANGELOG.md | 4 + include/Ark/Compiler/Macros/Processor.hpp | 2 + src/arkreactor/Compiler/Macros/Processor.cpp | 136 +++++++++--------- tests/unittests/Suites/DiagnosticsSuite.cpp | 2 +- .../compileTime/macro_at_list_string.ark | 2 + .../compileTime/macro_at_list_string.expected | 8 ++ .../compileTime/macro_at_number_number.ark | 2 + .../macro_at_number_number.expected | 8 ++ .../compileTime/macro_head_number.ark | 2 + .../compileTime/macro_head_number.expected | 8 ++ .../compileTime/macro_len_number.ark | 2 + .../compileTime/macro_len_number.expected | 8 ++ .../macro_symcat_type_error.expected | 2 +- .../compileTime/macro_tail_number.ark | 2 + .../compileTime/macro_tail_number.expected | 8 ++ 15 files changed, 127 insertions(+), 69 deletions(-) create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.expected diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d1f72c..3a07da23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,14 @@ # Change Log ## [Unreleased changes] - 2026-XX-YY +### Breaking change +- in macros, `len`, `empty?`, `head`, `tail`, `@` have been renamed to `$len`, `$empty?`, `$head`, `$tail` and `$at`. Those versions only work inside macros too, inside of having a weird dichotomy where they sometimes got applied and sometimes not + ### Added - `apply` function: `(apply func [args...])`, to call a function with a set of arguments stored in a list. Works with functions, closures and builtins - `+`, `-`, `*`, `/` and many other operators can now be passed around, like builtins. This now works: `(list:reduce [1 2 3] +)`, where before we would get a compile time error about a "freestanding operator '+'" - `slice` builtin, for strings and lists: `(slice data start end [step=1])` +- arguments of builtin macros are properly type-checked and will now raise runtime errors if the type is incorrect ## [4.2.0] - 2026-02-04 ### Breaking changes diff --git a/include/Ark/Compiler/Macros/Processor.hpp b/include/Ark/Compiler/Macros/Processor.hpp index 3c16083c..44155b2d 100644 --- a/include/Ark/Compiler/Macros/Processor.hpp +++ b/include/Ark/Compiler/Macros/Processor.hpp @@ -184,6 +184,8 @@ namespace Ark::internal * @param node the node in which there is an error */ [[noreturn]] void throwMacroProcessingError(const std::string& message, const Node& node) const; + + void checkMacroTypeError(const std::string& macro, const std::string& arg, NodeType expected, const Node& actual) const; }; } diff --git a/src/arkreactor/Compiler/Macros/Processor.cpp b/src/arkreactor/Compiler/Macros/Processor.cpp index 99be9702..a6152b6b 100644 --- a/src/arkreactor/Compiler/Macros/Processor.cpp +++ b/src/arkreactor/Compiler/Macros/Processor.cpp @@ -410,14 +410,13 @@ namespace Ark::internal else if (name == "$len") { checkMacroArgCountEq(node, 1, "$len", true); + Node& lst = node.list()[1]; + checkMacroTypeError("$len", "node", NodeType::List, lst); - if (Node& lst = node.list()[1]; lst.nodeType() == NodeType::List) // only apply len at compile time if we can - { - if (!lst.list().empty() && lst.list()[0] == getListNode()) - node.updateValueAndType(Node(static_cast(lst.list().size()) - 1)); - else - node.updateValueAndType(Node(static_cast(lst.list().size()))); - } + if (!lst.list().empty() && lst.list()[0] == getListNode()) + node.updateValueAndType(Node(static_cast(lst.list().size()) - 1)); + else + node.updateValueAndType(Node(static_cast(lst.list().size()))); } else if (name == "$empty?") { @@ -441,79 +440,64 @@ namespace Ark::internal Node sublist = evaluate(node.list()[1], depth + 1, is_not_body); const Node idx = evaluate(node.list()[2], depth + 1, is_not_body); - if (sublist.nodeType() == NodeType::List && idx.nodeType() == NodeType::Number) - { - const std::size_t size = sublist.list().size(); - std::size_t real_size = size; - long num_idx = static_cast(idx.number()); - - // if the first node is the function call to "list", don't count it - if (size > 0 && sublist.list()[0] == getListNode()) - { - real_size--; - if (num_idx >= 0) - ++num_idx; - } + checkMacroTypeError("$at", "list", NodeType::List, sublist); + checkMacroTypeError("$at", "index", NodeType::Number, idx); - Node output; - if (num_idx >= 0 && std::cmp_less(num_idx, size)) - output = sublist.list()[static_cast(num_idx)]; - else if (const auto c = static_cast(size) + num_idx; num_idx < 0 && std::cmp_less(c, size) && c >= 0) - output = sublist.list()[static_cast(c)]; - else - throwMacroProcessingError(fmt::format("Index ({}) out of range (list size: {})", num_idx, real_size), node); + const std::size_t size = sublist.list().size(); + std::size_t real_size = size; + long num_idx = static_cast(idx.number()); - output.setPositionFrom(node); - return output; + // if the first node is the function call to "list", don't count it + if (size > 0 && sublist.list()[0] == getListNode()) + { + real_size--; + if (num_idx >= 0) + ++num_idx; } + + Node output; + if (num_idx >= 0 && std::cmp_less(num_idx, size)) + output = sublist.list()[static_cast(num_idx)]; + else if (const auto c = static_cast(size) + num_idx; num_idx < 0 && std::cmp_less(c, size) && c >= 0) + output = sublist.list()[static_cast(c)]; + else + throwMacroProcessingError(fmt::format("Index ({}) out of range (list size: {})", num_idx, real_size), node); + + output.setPositionFrom(node); + return output; } else if (name == "$head") { checkMacroArgCountEq(node, 1, "$head", true); + Node sublist = node.list()[1]; + checkMacroTypeError("$head", "node", NodeType::List, sublist); - if (node.list()[1].nodeType() == NodeType::List) + if (!sublist.constList().empty() && sublist.constList()[0] == getListNode()) { - Node& sublist = node.list()[1]; - if (!sublist.constList().empty() && sublist.constList()[0] == getListNode()) + if (sublist.constList().size() > 1) { - if (sublist.constList().size() > 1) - { - const Node sublistCopy = sublist.constList()[1]; - node.updateValueAndType(sublistCopy); - } - else - node.updateValueAndType(getNilNode()); + const Node sublistCopy = sublist.constList()[1]; + node.updateValueAndType(sublistCopy); } - else if (!sublist.list().empty()) - node.updateValueAndType(sublist.constList()[0]); else node.updateValueAndType(getNilNode()); } + else if (!sublist.list().empty()) + node.updateValueAndType(sublist.constList()[0]); + else + node.updateValueAndType(getNilNode()); } else if (name == "$tail") { checkMacroArgCountEq(node, 1, "$tail", true); + Node sublist = node.list()[1]; + checkMacroTypeError("$tail", "node", NodeType::List, sublist); - if (node.list()[1].nodeType() == NodeType::List) + if (!sublist.list().empty() && sublist.list()[0] == getListNode()) { - Node sublist = node.list()[1]; - if (!sublist.list().empty() && sublist.list()[0] == getListNode()) - { - if (sublist.list().size() > 1) - { - sublist.list().erase(sublist.constList().begin() + 1); - node.updateValueAndType(sublist); - } - else - { - node.updateValueAndType(Node(NodeType::List)); - node.push_back(getListNode()); - } - } - else if (!sublist.list().empty()) + if (sublist.list().size() > 1) { - sublist.list().erase(sublist.constList().begin()); - sublist.list().insert(sublist.list().begin(), getListNode()); + sublist.list().erase(sublist.constList().begin() + 1); node.updateValueAndType(sublist); } else @@ -522,19 +506,23 @@ namespace Ark::internal node.push_back(getListNode()); } } + else if (!sublist.list().empty()) + { + sublist.list().erase(sublist.constList().begin()); + sublist.list().insert(sublist.list().begin(), getListNode()); + node.updateValueAndType(sublist); + } + else + { + node.updateValueAndType(Node(NodeType::List)); + node.push_back(getListNode()); + } } else if (name == Language::Symcat) { if (node.list().size() <= 2) throwMacroProcessingError(fmt::format("When expanding `{}', expected at least 2 arguments, got {} arguments", Language::Symcat, argcount), node); - if (node.list()[1].nodeType() != NodeType::Symbol) - throwMacroProcessingError( - fmt::format( - "When expanding `{}', expected the first argument to be a Symbol, got a {}: {}", - Language::Symcat, - typeToString(node.list()[1]), - node.list()[1].repr()), - node.list()[1]); + checkMacroTypeError(Language::Symcat.data(), "symbol", NodeType::Symbol, node.list()[1]); std::string sym = node.list()[1].string(); @@ -735,4 +723,18 @@ namespace Ark::internal throw CodeError(message, CodeErrorContext(node.filename(), node.position()), maybe_context); } + + void MacroProcessor::checkMacroTypeError(const std::string& macro, const std::string& arg, const NodeType expected, const Node& actual) const + { + if (actual.nodeType() != expected) + throwMacroProcessingError( + fmt::format( + "When expanding `{}', expected '{}' to be a {}, got a {}: {}", + macro, + arg, + std::string(nodeTypes[static_cast(expected)]), + typeToString(actual), + actual.repr()), + actual); + } } diff --git a/tests/unittests/Suites/DiagnosticsSuite.cpp b/tests/unittests/Suites/DiagnosticsSuite.cpp index 6c6a96cb..ae0d6392 100644 --- a/tests/unittests/Suites/DiagnosticsSuite.cpp +++ b/tests/unittests/Suites/DiagnosticsSuite.cpp @@ -21,7 +21,7 @@ ut::suite<"Diagnostics"> diagnostics_suite = [] { try { const bool ok = mut(state).doFile(data.path, features); - expect(!ok) << fatal; // we shouldn't be here, the compilation has to fail + expect(!ok); // we shouldn't be here, the compilation has to fail } catch (const Ark::CodeError& e) { diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.ark new file mode 100644 index 00000000..89801abc --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.ark @@ -0,0 +1,2 @@ +(macro a () ($at (fun () ()) "hello")) +(print (a)) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.expected new file mode 100644 index 00000000..a472e599 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.expected @@ -0,0 +1,8 @@ +In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.ark:1 + 1 | (macro a () ($at (fun () ()) "hello")) + | │ └─ error + | │ + | └─ macro expansion started here + 2 | (print (a)) + 3 | + When expanding `$at', expected 'index' to be a Number, got a String: "hello" diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.ark new file mode 100644 index 00000000..a1117c9c --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.ark @@ -0,0 +1,2 @@ +(macro a () ($at 4 5)) +(print (a)) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.expected new file mode 100644 index 00000000..c8490b7f --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.expected @@ -0,0 +1,8 @@ +In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.ark:1 + 1 | (macro a () ($at 4 5)) + | │ └─ error + | │ + | └─ macro expansion started here + 2 | (print (a)) + 3 | + When expanding `$at', expected 'list' to be a List, got a Number: 4 diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.ark new file mode 100644 index 00000000..de0b57a9 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.ark @@ -0,0 +1,2 @@ +(macro a () ($head 5)) +(print (a)) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.expected new file mode 100644 index 00000000..3a4af519 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.expected @@ -0,0 +1,8 @@ +In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.ark:1 + 1 | (macro a () ($head 5)) + | │ └─ error + | │ + | └─ macro expansion started here + 2 | (print (a)) + 3 | + When expanding `$head', expected 'node' to be a List, got a Number: 5 diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.ark new file mode 100644 index 00000000..ec2b5af1 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.ark @@ -0,0 +1,2 @@ +(macro a () ($len 5)) +(print (a)) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.expected new file mode 100644 index 00000000..41688687 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.expected @@ -0,0 +1,8 @@ +In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_len_number.ark:1 + 1 | (macro a () ($len 5)) + | │ └─ error + | │ + | └─ macro expansion started here + 2 | (print (a)) + 3 | + When expanding `$len', expected 'node' to be a List, got a Number: 5 diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_type_error.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_type_error.expected index 86ee1ecf..4256e6ae 100644 --- a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_type_error.expected +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_type_error.expected @@ -5,4 +5,4 @@ In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_symcat_type | └─ macro expansion started here 2 | (print a) 3 | - When expanding `$symcat', expected the first argument to be a Symbol, got a Number: 5 + When expanding `$symcat', expected 'symbol' to be a Symbol, got a Number: 5 diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.ark b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.ark new file mode 100644 index 00000000..c9806985 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.ark @@ -0,0 +1,2 @@ +(macro a () ($tail 5)) +(print (a)) diff --git a/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.expected b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.expected new file mode 100644 index 00000000..73093e3d --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.expected @@ -0,0 +1,8 @@ +In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_tail_number.ark:1 + 1 | (macro a () ($tail 5)) + | │ └─ error + | │ + | └─ macro expansion started here + 2 | (print (a)) + 3 | + When expanding `$tail', expected 'node' to be a List, got a Number: 5 From 832fcd44e9244a2a2f0194bfb3aa2a472e3e3322 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Wed, 18 Feb 2026 22:10:13 +0100 Subject: [PATCH 08/10] fix: remove noreturn attribute from ASTLowerer::makeError --- include/Ark/Compiler/Lowerer/ASTLowerer.hpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp index 8311769d..11d96085 100644 --- a/include/Ark/Compiler/Lowerer/ASTLowerer.hpp +++ b/include/Ark/Compiler/Lowerer/ASTLowerer.hpp @@ -234,7 +234,14 @@ namespace Ark::internal */ [[noreturn]] static void buildAndThrowError(const std::string& message, const Node& node); - [[noreturn]] static void makeError(ErrorKind kind, const Node& node, const std::string& additional_ctx); + /** + * @brief Throw a nice error message, using a message builder + * + * @param kind error kind + * @param node erroneous node + * @param additional_ctx optional context for the error, e.g. the macro name + */ + static void makeError(ErrorKind kind, const Node& node, const std::string& additional_ctx); /** * @brief Compile an expression (a node) recursively From f7564e36a0a05fe26a81b9fd0b772f7624590ea1 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Thu, 19 Feb 2026 20:52:18 +0100 Subject: [PATCH 09/10] chore(builtin): adding more tests for slice builtin --- src/arkreactor/Builtins/Builtins.cpp | 89 ------------------ src/arkreactor/Builtins/Slice.cpp | 94 +++++++++++++++++++ src/arkreactor/Compiler/AST/Node.cpp | 2 +- .../runtime/slice_of_step_0.ark | 1 + .../runtime/slice_of_step_0.expected | 6 ++ .../runtime/slice_out_of_range.ark | 1 + .../runtime/slice_out_of_range.expected | 6 ++ .../typeChecking/slice_num_num_num.ark | 1 + .../typeChecking/slice_num_num_num.expected | 23 +++++ .../typeChecking/slice_str_str_num.ark | 1 + .../typeChecking/slice_str_str_num.expected | 23 +++++ .../resources/LangSuite/builtins-tests.ark | 16 +++- 12 files changed, 172 insertions(+), 91 deletions(-) create mode 100644 src/arkreactor/Builtins/Slice.cpp create mode 100644 tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.expected create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.ark create mode 100644 tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.expected diff --git a/src/arkreactor/Builtins/Builtins.cpp b/src/arkreactor/Builtins/Builtins.cpp index 4f0be8ab..863e6f25 100644 --- a/src/arkreactor/Builtins/Builtins.cpp +++ b/src/arkreactor/Builtins/Builtins.cpp @@ -4,8 +4,6 @@ #include #include -#include -#include namespace Ark::internal::Builtins { @@ -23,93 +21,6 @@ namespace Ark::internal::Builtins extern const Value nan_ = Value(std::numeric_limits::signaling_NaN()); } - /** - * @name slice - * @brief Slice a list or string given a start, an end, and an optional step size - * @param container list or string - * @param start number, included - * @param end number, excluded - * @param step number, default 1 - * =begin - * (let d (dict "key" "value" 5 12)) - * (print d) # {key: value, 5: 12} - * =end - * @author https://github.com/SuperFola - */ - // cppcheck-suppress constParameterReference - Value slice(std::vector& n, VM* vm [[maybe_unused]]) - { - if (n.size() < 3 || n.size() > 4 || - (n[1].valueType() != ValueType::Number && n[1].valueType() != ValueType::Nil) || - (n[2].valueType() != ValueType::Number && n[2].valueType() != ValueType::Nil) || - (n.size() == 4 && n[3].valueType() != ValueType::Number)) - throw types::TypeCheckingError( - "slice", - { { types::Contract { - { types::Typedef("container", { ValueType::List, ValueType::String }), - types::Typedef("start", ValueType::Number), - types::Typedef("end", ValueType::Number) } }, - types::Contract { - { types::Typedef("container", { ValueType::List, ValueType::String }), - types::Typedef("start", ValueType::Number), - types::Typedef("end", ValueType::Number), - types::Typedef("step", ValueType::Number) } } } }, - n); - - const bool is_list = n[0].valueType() == ValueType::List; - const std::size_t container_size = is_list ? n[0].constList().size() : n[0].string().size(); - - const long start = n[1].valueType() == ValueType::Number ? static_cast(n[1].number()) : 0; - const std::size_t i = static_cast(start < 0 ? static_cast(container_size) + start : start); - - if (i >= container_size) - VM::throwVMError( - ErrorKind::Index, - fmt::format("{} out of range {} (length {})", start, n[0].toString(*vm, /* show_as_code= */ true), container_size)); - - const long end = n[2].valueType() == ValueType::Number ? static_cast(n[2].number()) : static_cast(container_size); - const std::size_t j = std::min(container_size, static_cast(end < 0 ? static_cast(container_size) + end : end)); - - const long step = n.size() == 4 ? static_cast(n[3].number()) : 1L; - if (step == 0) - throw Error("slice: a step of 0 is illegal"); - - std::size_t a = step > 0 ? i : j - 1; - const std::size_t b = step > 0 ? j : i; - const std::size_t increments = static_cast(std::abs(step)); - - if (is_list) - { - Value output(ValueType::List); - while ((step > 0 && a < b) || (step < 0 && a >= b)) - { - output.push_back(n[0].constList()[a]); - - if (step > 0) - a += increments; - else if (a >= increments) - a -= increments; - else - break; // step < 0 and 'increments' is bigger than 'a' - } - return output; - } - - std::string output; - while ((step > 0 && a < b) || (step < 0 && a >= b)) - { - output += n[0].string()[a]; - - if (step > 0) - a += increments; - else if (a >= increments) - a -= increments; - else - break; // step < 0 and 'increments' is bigger than 'a' - } - return Value(output); - } - extern const std::vector> builtins = { // builtin variables or constants { "false", falseSym }, diff --git a/src/arkreactor/Builtins/Slice.cpp b/src/arkreactor/Builtins/Slice.cpp new file mode 100644 index 00000000..d1e6f417 --- /dev/null +++ b/src/arkreactor/Builtins/Slice.cpp @@ -0,0 +1,94 @@ +#include +#include +#include + +namespace Ark::internal::Builtins +{ + /** + * @name slice + * @brief Slice a list or string given a start, an end, and an optional step size + * @param container list or string + * @param start number, included + * @param end number, excluded + * @param step number, default 1 + * =begin + * (let d (dict "key" "value" 5 12)) + * (print d) # {key: value, 5: 12} + * =end + * @author https://github.com/SuperFola + */ + // cppcheck-suppress constParameterReference + Value slice(std::vector& n, VM* vm [[maybe_unused]]) + { + if (n.size() < 3 || n.size() > 4 || + (n[0].valueType() != ValueType::List && n[0].valueType() != ValueType::String) || + (n[1].valueType() != ValueType::Number && n[1].valueType() != ValueType::Nil) || + (n[2].valueType() != ValueType::Number && n[2].valueType() != ValueType::Nil) || + (n.size() == 4 && n[3].valueType() != ValueType::Number)) + throw types::TypeCheckingError( + "slice", + { { types::Contract { + { types::Typedef("container", { ValueType::List, ValueType::String }), + types::Typedef("start", ValueType::Number), + types::Typedef("end", ValueType::Number) } }, + types::Contract { + { types::Typedef("container", { ValueType::List, ValueType::String }), + types::Typedef("start", ValueType::Number), + types::Typedef("end", ValueType::Number), + types::Typedef("step", ValueType::Number) } } } }, + n); + + const bool is_list = n[0].valueType() == ValueType::List; + const std::size_t container_size = is_list ? n[0].constList().size() : n[0].string().size(); + + const long start = n[1].valueType() == ValueType::Number ? static_cast(n[1].number()) : 0; + const std::size_t i = static_cast(start < 0 ? static_cast(container_size) + start : start); + + if (i >= container_size) + VM::throwVMError( + ErrorKind::Index, + fmt::format("{} out of range {} (length {})", start, n[0].toString(*vm, /* show_as_code= */ true), container_size)); + + const long end = n[2].valueType() == ValueType::Number ? static_cast(n[2].number()) : static_cast(container_size); + const std::size_t j = std::min(container_size, static_cast(end < 0 ? static_cast(container_size) + end : end)); + + const long step = n.size() == 4 ? static_cast(n[3].number()) : 1L; + if (step == 0) + throw Error("slice: a step of 0 is illegal"); + + std::size_t a = step > 0 ? i : j - 1; + const std::size_t b = step > 0 ? j : i; + const std::size_t increments = static_cast(std::abs(step)); + + if (is_list) + { + Value output(ValueType::List); + while ((step > 0 && a < b) || (step < 0 && a >= b)) + { + output.push_back(n[0].constList()[a]); + + if (step > 0) + a += increments; + else if (a >= increments) + a -= increments; + else + break; // step < 0 and 'increments' is bigger than 'a' + } + return output; + } + + std::string output; + while ((step > 0 && a < b) || (step < 0 && a >= b)) + { + output += n[0].string()[a]; + + if (step > 0) + a += increments; + else if (a >= increments) + a -= increments; + else + break; // step < 0 and 'increments' is bigger than 'a' + } + return Value(output); + } +} diff --git a/src/arkreactor/Compiler/AST/Node.cpp b/src/arkreactor/Compiler/AST/Node.cpp index 5216bfd6..3e6aff32 100644 --- a/src/arkreactor/Compiler/AST/Node.cpp +++ b/src/arkreactor/Compiler/AST/Node.cpp @@ -404,7 +404,7 @@ namespace Ark::internal bool operator<(const Node& A, const Node& B) { if (A.nodeType() != B.nodeType()) - return (static_cast(A.nodeType()) - static_cast(B.nodeType())) < 0; + return false; switch (A.nodeType()) { diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.ark b/tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.ark new file mode 100644 index 00000000..d0692ef0 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.ark @@ -0,0 +1 @@ +(slice "abc" 0 2 0) diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.expected new file mode 100644 index 00000000..2e048050 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.expected @@ -0,0 +1,6 @@ +slice: a step of 0 is illegal + +In file tests/unittests/resources/DiagnosticsSuite/runtime/slice_of_step_0.ark:1 + 1 | (slice "abc" 0 2 0) + | ^~~~~~~~~~~~~~~~~~ + 2 | diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.ark b/tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.ark new file mode 100644 index 00000000..d7342b94bf --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.ark @@ -0,0 +1 @@ +(slice "abc" 4 5) diff --git a/tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.expected b/tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.expected new file mode 100644 index 00000000..53541edf --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.expected @@ -0,0 +1,6 @@ +IndexError: 4 out of range "abc" (length 3) + +In file tests/unittests/resources/DiagnosticsSuite/runtime/slice_out_of_range.ark:1 + 1 | (slice "abc" 4 5) + | ^~~~~~~~~~~~~~~~ + 2 | diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.ark b/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.ark new file mode 100644 index 00000000..2df28f45 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.ark @@ -0,0 +1 @@ +(slice 1 5 5) diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.expected new file mode 100644 index 00000000..2046dc28 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.expected @@ -0,0 +1,23 @@ +Function slice expected between 3 arguments and 4 arguments +Call + ↳ (slice 1 5 5) +Signature + ↳ (slice container start end) +Arguments + → `container' (expected List, or String), got 1 (Number) + → `start' (expected Number) ✓ + → `end' (expected Number) ✓ + +Alternative 2: +Signature + ↳ (slice container start end step) +Arguments + → `container' (expected List, or String), got 1 (Number) + → `start' (expected Number) ✓ + → `end' (expected Number) ✓ + → `step' (expected Number) was not provided + +In file tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_num_num_num.ark:1 + 1 | (slice 1 5 5) + | ^~~~~~~~~~~~ + 2 | diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.ark b/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.ark new file mode 100644 index 00000000..bb9b9d6a --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.ark @@ -0,0 +1 @@ +(slice "abc" "def" 5) diff --git a/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.expected b/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.expected new file mode 100644 index 00000000..b2bf0399 --- /dev/null +++ b/tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.expected @@ -0,0 +1,23 @@ +Function slice expected between 3 arguments and 4 arguments +Call + ↳ (slice "abc" "def" 5) +Signature + ↳ (slice container start end) +Arguments + → `container' (expected List, or String) ✓ + → `start' (expected Number), got "def" (String) + → `end' (expected Number) ✓ + +Alternative 2: +Signature + ↳ (slice container start end step) +Arguments + → `container' (expected List, or String) ✓ + → `start' (expected Number), got "def" (String) + → `end' (expected Number) ✓ + → `step' (expected Number) was not provided + +In file tests/unittests/resources/DiagnosticsSuite/typeChecking/slice_str_str_num.ark:1 + 1 | (slice "abc" "def" 5) + | ^~~~~~~~~~~~~~~~~~~~ + 2 | diff --git a/tests/unittests/resources/LangSuite/builtins-tests.ark b/tests/unittests/resources/LangSuite/builtins-tests.ark index d11e2552..dea6263c 100644 --- a/tests/unittests/resources/LangSuite/builtins-tests.ark +++ b/tests/unittests/resources/LangSuite/builtins-tests.ark @@ -43,4 +43,18 @@ (test:eq (slice "abcdef" nil nil 2) "ace") (test:eq (slice "abcdef" nil nil -2) "fdb") (test:eq (slice "abcdef" 1 nil 2) "bdf") - (test:eq (slice "abcdef" 1 nil -2) "fdb") }) }) + (test:eq (slice "abcdef" 1 nil -2) "fdb") + + (test:eq (slice [1 2 3] nil nil) [1 2 3]) + (test:eq (slice [1 2 3] nil 1) [1]) + (test:eq (slice [1 2 3] nil 2) [1 2]) + (test:eq (slice [1 2 3] nil 3) [1 2 3]) + (test:eq (slice [1 2 3] nil 30) [1 2 3]) + (test:eq (slice [1 2 3 4 5 6] 1 -1) [2 3 4 5]) + (test:eq (slice [1 2 3 4 5 6] 1 -1 30) [2]) + (test:eq (slice [1 2 3 4 5 6] 1 -1 -1) [5 4 3 2]) + (test:eq (slice [1 2 3 4 5 6] 1 -1 -30) [5]) + (test:eq (slice [1 2 3 4 5 6] nil nil 2) [1 3 5]) + (test:eq (slice [1 2 3 4 5 6] nil nil -2) [6 4 2]) + (test:eq (slice [1 2 3 4 5 6] 1 nil 2) [2 4 6]) + (test:eq (slice [1 2 3 4 5 6] 1 nil -2) [6 4 2]) }) }) From 94e546d61d4acea84ba914342de5c36417707a31 Mon Sep 17 00:00:00 2001 From: Lexy Plt Date: Fri, 20 Feb 2026 18:30:48 +0100 Subject: [PATCH 10/10] fix(ir optimizer): do not optimize functions calling builtins in their first expression to a CALL_BUILTIN_WITHOUT_ADDRESS, if there isn't a RET after the builtin call Otherwise, this ends up breaking perfectly valid code by not pushing the return address --- .../IROptimizer.cpp | 11 +++--- .../call_builtin_not_optimized.ark | 12 +++++++ .../call_builtin_not_optimized.expected | 35 +++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/unittests/resources/CompilerSuite/optimized_ir/call_builtin_not_optimized.ark create mode 100644 tests/unittests/resources/CompilerSuite/optimized_ir/call_builtin_not_optimized.expected diff --git a/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp b/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp index 7f4ac8ac..eb492248 100644 --- a/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp +++ b/src/arkreactor/Compiler/IntermediateRepresentation/IROptimizer.cpp @@ -32,21 +32,21 @@ namespace Ark::internal Rule { { LOAD_FAST_BY_INDEX, SET_VAL }, SET_VAL_FROM_INDEX }, Rule { { STORE, PUSH_RETURN_ADDRESS, LOAD_FAST_BY_INDEX, BUILTIN, CALL }, [](const Entities entities, const std::size_t start_idx) { - return Builtins::builtins[entities[3].primaryArg()].second.isFunction() && start_idx == 0; + return Builtins::builtins[entities[3].primaryArg()].second.isFunction() && start_idx == 0 && entities[6].inst() == RET; }, [](const Entities e) { return IR::Entity(CALL_BUILTIN_WITHOUT_RETURN_ADDRESS, e[3].primaryArg(), 1); } }, Rule { { STORE, STORE, PUSH_RETURN_ADDRESS, LOAD_FAST_BY_INDEX, LOAD_FAST_BY_INDEX, BUILTIN, CALL }, [](const Entities entities, const std::size_t start_idx) { - return Builtins::builtins[entities[5].primaryArg()].second.isFunction() && start_idx == 0; + return Builtins::builtins[entities[5].primaryArg()].second.isFunction() && start_idx == 0 && entities[8].inst() == RET; }, [](const Entities e) { return IR::Entity(CALL_BUILTIN_WITHOUT_RETURN_ADDRESS, e[5].primaryArg(), 2); } }, Rule { { STORE, STORE, STORE, PUSH_RETURN_ADDRESS, LOAD_FAST_BY_INDEX, LOAD_FAST_BY_INDEX, LOAD_FAST_BY_INDEX, BUILTIN, CALL }, [](const Entities entities, const std::size_t start_idx) { - return Builtins::builtins[entities[7].primaryArg()].second.isFunction() && start_idx == 0; + return Builtins::builtins[entities[7].primaryArg()].second.isFunction() && start_idx == 0 && entities[10].inst() == RET; }, [](const Entities e) { return IR::Entity(CALL_BUILTIN_WITHOUT_RETURN_ADDRESS, e[7].primaryArg(), 3); @@ -313,10 +313,13 @@ namespace Ark::internal if (expected_insts.size() > entities.size()) return false; - for (std::size_t i = 0; i < expected_insts.size(); ++i) + std::size_t i = 0; + while (i < expected_insts.size()) { if (expected_insts[i] != entities[i].inst()) return false; + if (entities[i].kind() != IR::Kind::Label) + ++i; } return true; diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/call_builtin_not_optimized.ark b/tests/unittests/resources/CompilerSuite/optimized_ir/call_builtin_not_optimized.ark new file mode 100644 index 00000000..8d4d27bf --- /dev/null +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/call_builtin_not_optimized.ark @@ -0,0 +1,12 @@ +# we don't want the call to print to be optimized by a CALL_BUILTIN_WITHOUT_RETURN_ADDRESS here +# since it would break the rest of the function +(let foo (fun (a) { + (print a) + (let k (toString a)) + k })) +(foo 5) + +# but this should still be allowed here +(let bar (fun (b) + (print b))) +(bar 6) diff --git a/tests/unittests/resources/CompilerSuite/optimized_ir/call_builtin_not_optimized.expected b/tests/unittests/resources/CompilerSuite/optimized_ir/call_builtin_not_optimized.expected new file mode 100644 index 00000000..5b095f44 --- /dev/null +++ b/tests/unittests/resources/CompilerSuite/optimized_ir/call_builtin_not_optimized.expected @@ -0,0 +1,35 @@ +page_0 + LOAD_CONST_STORE 0, 0 + PUSH_RETURN_ADDRESS L1 + LOAD_CONST 1 + LOAD_FAST_BY_INDEX 0 + CALL 1 +.L1: + POP 0 + LOAD_CONST_STORE 2, 3 + PUSH_RETURN_ADDRESS L3 + LOAD_CONST 3 + LOAD_FAST_BY_INDEX 0 + CALL 1 +.L3: + HALT 0 + +page_1 + STORE 1 + PUSH_RETURN_ADDRESS L0 + LOAD_FAST_BY_INDEX 0 + CALL_BUILTIN 9, 1 +.L0: + POP 0 + LOAD_FAST_BY_INDEX 0 + TO_STR 0 + STORE 2 + LOAD_FAST_BY_INDEX 0 + RET 0 + HALT 0 + +page_2 + CALL_BUILTIN_WITHOUT_RETURN_ADDRESS 9, 1 +.L2: + RET 0 + HALT 0