Skip to content

Commit 12d4e1a

Browse files
committed
feat(macro processor): ARK-332, type-checking builtin macros arguments
1 parent a62b803 commit 12d4e1a

File tree

15 files changed

+127
-69
lines changed

15 files changed

+127
-69
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# Change Log
22

33
## [Unreleased changes] - 2026-XX-YY
4+
### Breaking change
5+
- 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
6+
47
### Added
58
- `apply` function: `(apply func [args...])`, to call a function with a set of arguments stored in a list. Works with functions, closures and builtins
69
- `+`, `-`, `*`, `/` 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 '+'"
710
- `slice` builtin, for strings and lists: `(slice data start end [step=1])`
11+
- arguments of builtin macros are properly type-checked and will now raise runtime errors if the type is incorrect
812

913
## [4.2.0] - 2026-02-04
1014
### Breaking changes

include/Ark/Compiler/Macros/Processor.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ namespace Ark::internal
184184
* @param node the node in which there is an error
185185
*/
186186
[[noreturn]] void throwMacroProcessingError(const std::string& message, const Node& node) const;
187+
188+
void checkMacroTypeError(const std::string& macro, const std::string& arg, NodeType expected, const Node& actual) const;
187189
};
188190
}
189191

src/arkreactor/Compiler/Macros/Processor.cpp

Lines changed: 69 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -410,14 +410,13 @@ namespace Ark::internal
410410
else if (name == "$len")
411411
{
412412
checkMacroArgCountEq(node, 1, "$len", true);
413+
Node& lst = node.list()[1];
414+
checkMacroTypeError("$len", "node", NodeType::List, lst);
413415

414-
if (Node& lst = node.list()[1]; lst.nodeType() == NodeType::List) // only apply len at compile time if we can
415-
{
416-
if (!lst.list().empty() && lst.list()[0] == getListNode())
417-
node.updateValueAndType(Node(static_cast<long>(lst.list().size()) - 1));
418-
else
419-
node.updateValueAndType(Node(static_cast<long>(lst.list().size())));
420-
}
416+
if (!lst.list().empty() && lst.list()[0] == getListNode())
417+
node.updateValueAndType(Node(static_cast<long>(lst.list().size()) - 1));
418+
else
419+
node.updateValueAndType(Node(static_cast<long>(lst.list().size())));
421420
}
422421
else if (name == "$empty?")
423422
{
@@ -441,79 +440,64 @@ namespace Ark::internal
441440
Node sublist = evaluate(node.list()[1], depth + 1, is_not_body);
442441
const Node idx = evaluate(node.list()[2], depth + 1, is_not_body);
443442

444-
if (sublist.nodeType() == NodeType::List && idx.nodeType() == NodeType::Number)
445-
{
446-
const std::size_t size = sublist.list().size();
447-
std::size_t real_size = size;
448-
long num_idx = static_cast<long>(idx.number());
449-
450-
// if the first node is the function call to "list", don't count it
451-
if (size > 0 && sublist.list()[0] == getListNode())
452-
{
453-
real_size--;
454-
if (num_idx >= 0)
455-
++num_idx;
456-
}
443+
checkMacroTypeError("$at", "list", NodeType::List, sublist);
444+
checkMacroTypeError("$at", "index", NodeType::Number, idx);
457445

458-
Node output;
459-
if (num_idx >= 0 && std::cmp_less(num_idx, size))
460-
output = sublist.list()[static_cast<std::size_t>(num_idx)];
461-
else if (const auto c = static_cast<long>(size) + num_idx; num_idx < 0 && std::cmp_less(c, size) && c >= 0)
462-
output = sublist.list()[static_cast<std::size_t>(c)];
463-
else
464-
throwMacroProcessingError(fmt::format("Index ({}) out of range (list size: {})", num_idx, real_size), node);
446+
const std::size_t size = sublist.list().size();
447+
std::size_t real_size = size;
448+
long num_idx = static_cast<long>(idx.number());
465449

466-
output.setPositionFrom(node);
467-
return output;
450+
// if the first node is the function call to "list", don't count it
451+
if (size > 0 && sublist.list()[0] == getListNode())
452+
{
453+
real_size--;
454+
if (num_idx >= 0)
455+
++num_idx;
468456
}
457+
458+
Node output;
459+
if (num_idx >= 0 && std::cmp_less(num_idx, size))
460+
output = sublist.list()[static_cast<std::size_t>(num_idx)];
461+
else if (const auto c = static_cast<long>(size) + num_idx; num_idx < 0 && std::cmp_less(c, size) && c >= 0)
462+
output = sublist.list()[static_cast<std::size_t>(c)];
463+
else
464+
throwMacroProcessingError(fmt::format("Index ({}) out of range (list size: {})", num_idx, real_size), node);
465+
466+
output.setPositionFrom(node);
467+
return output;
469468
}
470469
else if (name == "$head")
471470
{
472471
checkMacroArgCountEq(node, 1, "$head", true);
472+
Node sublist = node.list()[1];
473+
checkMacroTypeError("$head", "node", NodeType::List, sublist);
473474

474-
if (node.list()[1].nodeType() == NodeType::List)
475+
if (!sublist.constList().empty() && sublist.constList()[0] == getListNode())
475476
{
476-
Node& sublist = node.list()[1];
477-
if (!sublist.constList().empty() && sublist.constList()[0] == getListNode())
477+
if (sublist.constList().size() > 1)
478478
{
479-
if (sublist.constList().size() > 1)
480-
{
481-
const Node sublistCopy = sublist.constList()[1];
482-
node.updateValueAndType(sublistCopy);
483-
}
484-
else
485-
node.updateValueAndType(getNilNode());
479+
const Node sublistCopy = sublist.constList()[1];
480+
node.updateValueAndType(sublistCopy);
486481
}
487-
else if (!sublist.list().empty())
488-
node.updateValueAndType(sublist.constList()[0]);
489482
else
490483
node.updateValueAndType(getNilNode());
491484
}
485+
else if (!sublist.list().empty())
486+
node.updateValueAndType(sublist.constList()[0]);
487+
else
488+
node.updateValueAndType(getNilNode());
492489
}
493490
else if (name == "$tail")
494491
{
495492
checkMacroArgCountEq(node, 1, "$tail", true);
493+
Node sublist = node.list()[1];
494+
checkMacroTypeError("$tail", "node", NodeType::List, sublist);
496495

497-
if (node.list()[1].nodeType() == NodeType::List)
496+
if (!sublist.list().empty() && sublist.list()[0] == getListNode())
498497
{
499-
Node sublist = node.list()[1];
500-
if (!sublist.list().empty() && sublist.list()[0] == getListNode())
501-
{
502-
if (sublist.list().size() > 1)
503-
{
504-
sublist.list().erase(sublist.constList().begin() + 1);
505-
node.updateValueAndType(sublist);
506-
}
507-
else
508-
{
509-
node.updateValueAndType(Node(NodeType::List));
510-
node.push_back(getListNode());
511-
}
512-
}
513-
else if (!sublist.list().empty())
498+
if (sublist.list().size() > 1)
514499
{
515-
sublist.list().erase(sublist.constList().begin());
516-
sublist.list().insert(sublist.list().begin(), getListNode());
500+
sublist.list().erase(sublist.constList().begin() + 1);
517501
node.updateValueAndType(sublist);
518502
}
519503
else
@@ -522,19 +506,23 @@ namespace Ark::internal
522506
node.push_back(getListNode());
523507
}
524508
}
509+
else if (!sublist.list().empty())
510+
{
511+
sublist.list().erase(sublist.constList().begin());
512+
sublist.list().insert(sublist.list().begin(), getListNode());
513+
node.updateValueAndType(sublist);
514+
}
515+
else
516+
{
517+
node.updateValueAndType(Node(NodeType::List));
518+
node.push_back(getListNode());
519+
}
525520
}
526521
else if (name == Language::Symcat)
527522
{
528523
if (node.list().size() <= 2)
529524
throwMacroProcessingError(fmt::format("When expanding `{}', expected at least 2 arguments, got {} arguments", Language::Symcat, argcount), node);
530-
if (node.list()[1].nodeType() != NodeType::Symbol)
531-
throwMacroProcessingError(
532-
fmt::format(
533-
"When expanding `{}', expected the first argument to be a Symbol, got a {}: {}",
534-
Language::Symcat,
535-
typeToString(node.list()[1]),
536-
node.list()[1].repr()),
537-
node.list()[1]);
525+
checkMacroTypeError(Language::Symcat.data(), "symbol", NodeType::Symbol, node.list()[1]);
538526

539527
std::string sym = node.list()[1].string();
540528

@@ -735,4 +723,18 @@ namespace Ark::internal
735723

736724
throw CodeError(message, CodeErrorContext(node.filename(), node.position()), maybe_context);
737725
}
726+
727+
void MacroProcessor::checkMacroTypeError(const std::string& macro, const std::string& arg, const NodeType expected, const Node& actual) const
728+
{
729+
if (actual.nodeType() != expected)
730+
throwMacroProcessingError(
731+
fmt::format(
732+
"When expanding `{}', expected '{}' to be a {}, got a {}: {}",
733+
macro,
734+
arg,
735+
std::string(nodeTypes[static_cast<std::size_t>(expected)]),
736+
typeToString(actual),
737+
actual.repr()),
738+
actual);
739+
}
738740
}

tests/unittests/Suites/DiagnosticsSuite.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ ut::suite<"Diagnostics"> diagnostics_suite = [] {
2121
try
2222
{
2323
const bool ok = mut(state).doFile(data.path, features);
24-
expect(!ok) << fatal; // we shouldn't be here, the compilation has to fail
24+
expect(!ok); // we shouldn't be here, the compilation has to fail
2525
}
2626
catch (const Ark::CodeError& e)
2727
{
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
(macro a () ($at (fun () ()) "hello"))
2+
(print (a))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_list_string.ark:1
2+
1 | (macro a () ($at (fun () ()) "hello"))
3+
| │ └─ error
4+
| │
5+
| └─ macro expansion started here
6+
2 | (print (a))
7+
3 |
8+
When expanding `$at', expected 'index' to be a Number, got a String: "hello"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
(macro a () ($at 4 5))
2+
(print (a))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_at_number_number.ark:1
2+
1 | (macro a () ($at 4 5))
3+
| │ └─ error
4+
| │
5+
| └─ macro expansion started here
6+
2 | (print (a))
7+
3 |
8+
When expanding `$at', expected 'list' to be a List, got a Number: 4
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
(macro a () ($head 5))
2+
(print (a))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
In file tests/unittests/resources/DiagnosticsSuite/compileTime/macro_head_number.ark:1
2+
1 | (macro a () ($head 5))
3+
| │ └─ error
4+
| │
5+
| └─ macro expansion started here
6+
2 | (print (a))
7+
3 |
8+
When expanding `$head', expected 'node' to be a List, got a Number: 5

0 commit comments

Comments
 (0)