Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +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
Expand Down
124 changes: 124 additions & 0 deletions docs/arkdoc/Macros.txt
Original file line number Diff line number Diff line change
@@ -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
#--
4 changes: 2 additions & 2 deletions examples/macros.ark
Original file line number Diff line number Diff line change
Expand Up @@ -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) })

Expand Down Expand Up @@ -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)) })

Expand Down
2 changes: 2 additions & 0 deletions include/Ark/Builtins/Builtins.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ namespace Ark::internal::Builtins
ARK_BUILTIN(disassemble);
}

ARK_BUILTIN(slice);

namespace Operators
{
ARK_BUILTIN(add);
Expand Down
17 changes: 17 additions & 0 deletions include/Ark/Compiler/Lowerer/ASTLowerer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -226,6 +234,15 @@ namespace Ark::internal
*/
[[noreturn]] static void buildAndThrowError(const std::string& message, const Node& node);

/**
* @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
*
Expand Down
3 changes: 2 additions & 1 deletion include/Ark/Compiler/Macros/Executor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 7 additions & 13 deletions include/Ark/Compiler/Macros/Processor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,22 +102,14 @@ 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
*
* @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
Expand Down Expand Up @@ -154,7 +146,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
Expand All @@ -164,7 +156,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
Expand All @@ -183,7 +175,7 @@ namespace Ark::internal
* @return true
* @return false
*/
bool isTruthy(const Node& node);
[[nodiscard]] bool isTruthy(const Node& node) const;

/**
* @brief Throw a macro processing error
Expand All @@ -192,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;
};
}

Expand Down
2 changes: 1 addition & 1 deletion lib/std
2 changes: 2 additions & 0 deletions src/arkreactor/Builtins/Builtins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,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) },
Expand Down
94 changes: 94 additions & 0 deletions src/arkreactor/Builtins/Slice.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include <Ark/Builtins/Builtins.hpp>
#include <Ark/TypeChecker.hpp>
#include <Ark/VM/VM.hpp>

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<Value>& 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<long>(n[1].number()) : 0;
const std::size_t i = static_cast<std::size_t>(start < 0 ? static_cast<long>(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<long>(n[2].number()) : static_cast<long>(container_size);
const std::size_t j = std::min(container_size, static_cast<std::size_t>(end < 0 ? static_cast<long>(container_size) + end : end));

const long step = n.size() == 4 ? static_cast<long>(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::size_t>(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);
}
}
2 changes: 1 addition & 1 deletion src/arkreactor/Compiler/AST/Node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ namespace Ark::internal
bool operator<(const Node& A, const Node& B)
{
if (A.nodeType() != B.nodeType())
return (static_cast<int>(A.nodeType()) - static_cast<int>(B.nodeType())) < 0;
return false;

switch (A.nodeType())
{
Expand Down
3 changes: 2 additions & 1 deletion src/arkreactor/Compiler/AST/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading