ArkScript
A small, lisp-inspired, functional scripting language
Function.cpp
Go to the documentation of this file.
1#include <cassert>
3
4#include <fmt/core.h>
5#include <ranges>
6
7#include <Ark/Constants.hpp>
8
9namespace Ark::internal
10{
12 {
13 return node.nodeType() == NodeType::List && !node.constList().empty() && node.constList()[0].nodeType() == NodeType::Symbol;
14 }
15
16 bool FunctionExecutor::applyMacro(Node& node, const unsigned depth)
17 {
18 const Node& first = node.list()[0];
19
20 // macro corresponds to the following form: (macro name (args) body)
21 if (const Node* macro = findNearestMacro(first.string()); macro != nullptr && macro->constList().size() == 3)
22 {
23 Node temp_body = macro->constList()[2];
24 const Node args = macro->constList()[1];
25 const std::size_t args_needed = args.constList().size();
26 const std::size_t args_given = node.constList().size() - 1; // remove the first (the name of the macro)
27 const std::string macro_name = macro->constList()[0].string();
28 // thanks to the parser, we are guaranteed that the spread will be in last position, if any
29 const bool has_spread = args_needed > 0 && args.constList().back().nodeType() == NodeType::Spread;
30
31 // save the args given to the macro by giving them a name (from the macro args block),
32 // and a value (in node.constList())
33 std::unordered_map<std::string, Node> args_applied;
34 std::size_t j = 0;
35 for (std::size_t i = 1, end = node.constList().size(); i < end; ++i)
36 {
37 // by breaking early if we have too many arguments, the args_applied/args_needed check will fail
38 if (j >= args_needed)
39 break;
40
41 if (args.constList()[j].nodeType() == NodeType::Symbol)
42 {
43 const std::string& arg_name = args.constList()[j].string();
44 args_applied[arg_name] = node.constList()[i];
45 ++j;
46 }
47 else if (args.constList()[j].nodeType() == NodeType::Spread)
48 {
49 const std::string& arg_name = args.constList()[j].string();
50 if (!args_applied.contains(arg_name))
51 {
52 args_applied[arg_name] = Node(NodeType::List);
53 args_applied[arg_name].push_back(getListNode());
54 }
55 // do not move j because we checked before that the spread is always the last one
56 args_applied[arg_name].push_back(node.constList()[i]);
57 }
58 }
59
60 // check argument count
61 if (args_applied.size() + 1 == args_needed && has_spread)
62 {
63 // just a spread we didn't assign
64 args_applied[args.constList().back().string()] = Node(NodeType::List);
65 args_applied[args.constList().back().string()].push_back(getListNode());
66 }
67
68 if (args_given != args_needed && !has_spread)
69 throwMacroProcessingError(fmt::format("Macro `{}' got {} argument(s) but needed {}", macro_name, args_given, args_needed), node);
70 if (args_applied.size() != args_needed && has_spread)
71 // args_needed - 1 because we do not count the spread as a required argument
72 throwMacroProcessingError(fmt::format("Macro `{}' got {} argument(s) but needed at least {}", macro_name, args_applied.size(), args_needed - 1), node);
73
74 if (!args_applied.empty())
75 unify(args_applied, temp_body, nullptr);
76
77 node.updateValueAndType(evaluate(temp_body, depth + 1, false));
78 applyMacroProxy(node, depth + 1);
79 return true;
80 }
81
82 if (std::ranges::find(Language::macros, first.string()) != Language::macros.end())
83 {
84 node.updateValueAndType(evaluate(node, depth + 1, false));
85 return true;
86 }
87
88 return false;
89 }
90
92 {
93 const Node& first = node.list()[0];
94 if (const Node* macro = findNearestMacro(first.string()); macro != nullptr && macro->constList().size() == 3)
95 return *macro;
96 return {};
97 }
98
99 void FunctionExecutor::unify(const std::unordered_map<std::string, Node>& map, Node& target, Node* parent, const std::size_t index, const std::size_t unify_depth)
100 {
101 if (unify_depth > MaxMacroUnificationDepth)
103 fmt::format(
104 "Max macro unification depth reached ({}). You may have a macro trying to evaluate itself, try splitting your code in multiple nodes.",
106 *parent);
107
108 if (target.nodeType() == NodeType::Symbol)
109 {
110 if (const auto p = map.find(target.string()); p != map.end())
111 target = p->second;
112 }
113 else if (target.isListLike())
114 {
115 if (target.nodeType() == NodeType::Macro && target.list()[0].nodeType() == NodeType::Symbol)
116 {
117 if (const std::string macro_name = target.list()[0].string(); map.contains(macro_name))
119 fmt::format(
120 "Can not define a macro by reusing the argument name `{}'",
121 macro_name),
122 target);
123
124 // proceed for expansion only on the value of each macro
125 unify(map, target.list().back(), &target, target.list().size() - 1, unify_depth + 1);
126 }
127 else
128 {
129 // proceed for expansion on normal nodes, we can safely run on all subnodes
130 for (std::size_t i = 0; i < target.list().size(); ++i)
131 unify(map, target.list()[i], &target, i, unify_depth + 1);
132 }
133 }
134 else if (target.nodeType() == NodeType::Spread)
135 {
136 assert(parent != nullptr && "Parent node should be defined when unifying a spread");
137
138 Node sub_node = target;
140 unify(map, sub_node, parent, 0, unify_depth + 1);
141
142 if (sub_node.nodeType() != NodeType::List)
143 parent->list()[index] = sub_node;
144 else
145 {
146 const bool is_list = sub_node.list().front() == getListNode();
147
148 for (std::size_t i = is_list ? 1 : 0, end = sub_node.list().size(); i < end; ++i)
149 parent->list().insert(
150 parent->list().begin() + static_cast<std::vector<Node>::difference_type>(index + i + (is_list ? 0 : 1)),
151 sub_node.list()[i]);
152 // remove the spread
153 parent->list().erase(parent->list().begin() + static_cast<std::vector<Node>::difference_type>(index));
154 }
155 }
156 }
157}
Constants used by ArkScript.
Executor for List Macros.
bool canHandle(Node &node) override
Definition Function.cpp:11
bool applyMacro(Node &node, unsigned depth) override
Definition Function.cpp:16
Node macroNode(Node &node) override
Returns the macro node that will be expanded.
Definition Function.cpp:91
void unify(const std::unordered_map< std::string, Node > &map, Node &target, Node *parent, std::size_t index=0, std::size_t unify_depth=0)
Definition Function.cpp:99
void applyMacroProxy(Node &node, unsigned depth)
Apply a macro on a given node.
Definition Executor.cpp:17
void throwMacroProcessingError(const std::string &message, const Node &node)
Throw a macro processing error.
Definition Executor.cpp:37
Node evaluate(Node &node, unsigned depth, bool is_not_body) const
Evaluate only the macros.
Definition Executor.cpp:32
const Node * findNearestMacro(const std::string &name) const
Find the nearest macro matching a giving name.
Definition Executor.cpp:12
A node of an Abstract Syntax Tree for ArkScript.
Definition Node.hpp:32
NodeType nodeType() const noexcept
Return the node type.
Definition Node.cpp:78
void setNodeType(NodeType type) noexcept
Set the Node Type object.
Definition Node.cpp:107
bool isListLike() const noexcept
Check if the node is a list like node.
Definition Node.cpp:83
const std::string & string() const noexcept
Return the string held by the value (if the node type allows it)
Definition Node.cpp:38
const std::vector< Node > & constList() const noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:73
void updateValueAndType(const Node &source) noexcept
Copy a node to the current one, while keeping the filename and position in the file.
Definition Node.cpp:101
std::vector< Node > & list() noexcept
Return the list of sub-nodes held by the node.
Definition Node.cpp:68
constexpr std::array macros
Definition Common.hpp:146
const Node & getListNode()
Definition Node.cpp:388
constexpr std::size_t MaxMacroUnificationDepth
Controls the number of recursive calls to MacroProcessor::unify.
Definition Constants.hpp:77