ArkScript
A small, lisp-inspired, functional scripting language
VM.hpp
Go to the documentation of this file.
1/**
2 * @file VM.hpp
3 * @author Lex Plateau (lexplt.dev@gmail.com)
4 * @brief The ArkScript virtual machine
5 * @date 2020-10-27
6 *
7 * @copyright Copyright (c) 2020-2025
8 *
9 */
10
11#ifndef ARK_VM_VM_HPP
12#define ARK_VM_VM_HPP
13
14#include <array>
15#include <vector>
16#include <string>
17#include <ranges>
18#include <cassert>
19#include <utility>
20#include <cinttypes>
21#include <unordered_map>
22#include <algorithm>
23#include <fmt/core.h>
24#include <fmt/ranges.h>
25
27#include <Ark/VM/Value.hpp>
28#include <Ark/VM/State.hpp>
29#include <Ark/VM/ScopeView.hpp>
30#include <Ark/VM/ErrorKind.hpp>
36#include <Ark/VM/Future.hpp>
37
38namespace Ark
39{
40 using namespace std::string_literals;
41
42 /**
43 * @brief The ArkScript virtual machine, executing ArkScript bytecode
44 *
45 */
46 class ARK_API VM final
47 {
48 public:
49 /**
50 * @brief Construct a new vm t object
51 *
52 * @param state a reference to an ArkScript state, which can be reused for multiple VMs
53 */
54 explicit VM(State& state) noexcept;
55
56 /**
57 * @brief Run the bytecode held in the state
58 *
59 * @param fail_with_exception throw if true, display a stacktrace if false
60 * @return int the exit code (default to 0 if no error)
61 */
62 int run(bool fail_with_exception = false);
63
64 /**
65 * @brief Retrieve a value from the virtual machine, given its symbol name
66 *
67 * @param name the name of the variable to retrieve
68 * @return Value&
69 */
70 Value& operator[](const std::string& name) noexcept;
71
72 /**
73 * @brief Call a function from ArkScript, by giving it arguments
74 *
75 * @tparam Args
76 * @param name the function name in the ArkScript code
77 * @param args C++ argument list, converted to internal representation
78 * @return Value
79 */
80 template <typename... Args>
81 Value call(const std::string& name, Args&&... args);
82
83 // ================================================
84 // function calling from plugins
85 // ================================================
86
87 /**
88 * @brief Resolves a function call (called by plugins and builtins)
89 *
90 * @param context the execution context to use
91 * @param n the function and its arguments
92 * @return Value
93 */
94 inline Value resolve(internal::ExecutionContext* context, const std::vector<Value>& n);
95
96 /**
97 * @brief Ask the VM to exit with a given exit code
98 *
99 * @param code an exit code
100 */
101 void exit(int code) noexcept;
102
103 /**
104 * @brief Return a pointer to the first execution context, for the main thread of the app
105 * @return internal::ExecutionContext*
106 */
108 {
109 return m_execution_contexts.front().get();
110 }
111
112 /**
113 * @brief Create an execution context and returns it
114 * @details This method is thread-safe VM wise.
115 *
116 * @return internal::ExecutionContext*
117 */
118 internal::ExecutionContext* createAndGetContext();
119
120 /**
121 * @brief Free a given execution context
122 * @details This method is thread-safe VM wise.
123 *
124 * @param ec
125 */
126 void deleteContext(internal::ExecutionContext* ec);
127
128 /**
129 * @brief Create a Future object from a function and its arguments and return a managed pointer to it
130 * @details This method is thread-safe VM wise.
132 * @param args
133 * @return internal::Future*
134 */
135 internal::Future* createFuture(std::vector<Value>& args);
136
137 /**
138 * @brief Free a given future
139 * @details This method is thread-safe VM wise.
140 *
141 * @param f
142 */
143 void deleteFuture(internal::Future* f);
144
145 /**
146 * @brief Used by the REPL to force reload all the plugins and their bound methods
147 *
148 * @return true on success
149 * @return false if one or more plugins couldn't be reloaded
150 */
151 [[nodiscard]] bool forceReloadPlugins() const;
152
153 /**
154 * @brief Throw a VM error message
155 *
156 * @param kind type of VM error
157 * @param message
158 */
159 [[noreturn]] static void throwVMError(internal::ErrorKind kind, const std::string& message);
160
161 inline const bytecode_t& bytecode() const
162 {
163 return m_state.m_bytecode;
164 }
165
166 friend class Value;
167 friend class internal::Closure;
168 friend class Repl;
170 private:
172 std::vector<std::unique_ptr<internal::ExecutionContext>> m_execution_contexts;
173 int m_exit_code; ///< VM exit code, defaults to 0. Can be changed through `sys:exit`
175 std::mutex m_mutex, m_mutex_futures;
176 std::vector<std::shared_ptr<internal::SharedLibrary>> m_shared_lib_objects;
177 std::vector<std::unique_ptr<internal::Future>> m_futures; ///< Storing the promises while we are resolving them
178
179 // a little trick for operator[] and for pop
180 Value m_no_value = internal::Builtins::nil;
182
183 /**
184 * @brief Run ArkScript bytecode inside a try catch to retrieve all the exceptions and display a stack trace if needed
185 *
186 * @param context
187 * @param untilFrameCount the frame count we need to reach before stopping the VM
188 * @param fail_with_exception throw if true, display a stacktrace if false
189 * @return int the exit code
190 */
191 int safeRun(internal::ExecutionContext& context, std::size_t untilFrameCount = 0, bool fail_with_exception = false);
192
193 /**
194 * @brief Initialize the VM according to the parameters
195 *
196 */
197 void init() noexcept;
198
199 // ================================================
200 // instruction helpers
201 // ================================================
202
203 /**
204 * @brief Load a symbol by its id in the current context. Performs a lookup in the scope stack, in reverse order.
205 *
206 * @param id symbol id
207 * @param context
208 * @return Value* nullptr if the symbol could not be loaded
209 */
210 [[nodiscard]] inline Value* loadSymbol(uint16_t id, internal::ExecutionContext& context);
211
212 /**
213 * @brief Load a symbol by its (reversed) index in the current scope
214 *
215 * @param index index of the symbol to load, starting from the end
216 * @param context
217 * @return Value*
218 */
219 [[nodiscard]] inline Value* loadSymbolFromIndex(uint16_t index, internal::ExecutionContext& context);
221 /**
222 * @brief Load a constant from the constant table by its id
223 *
224 * @param id
225 * @return Value*
226 */
227 [[nodiscard]] inline Value* loadConstAsPtr(uint16_t id) const;
228
229 /**
230 * @brief Create a new symbol with an associated value in the current scope
231 *
232 * @param id
233 * @param val
234 * @param context
235 */
236 inline void store(uint16_t id, const Value* val, internal::ExecutionContext& context);
238 /**
239 * @brief Change the value of a symbol given its identifier
240 *
241 * @param id
242 * @param val
243 * @param context
244 */
245 inline void setVal(uint16_t id, const Value* val, internal::ExecutionContext& context);
246
247 inline void jump(uint16_t address, internal::ExecutionContext& context);
248
249 Value getField(Value* closure, uint16_t id, const internal::ExecutionContext& context);
250
251 Value createList(std::size_t count, internal::ExecutionContext& context);
252
253 void listAppendInPlace(Value* list, std::size_t count, internal::ExecutionContext& context);
254
255 // ================================================
256 // stack related
257 // ================================================
258
259 /**
260 * @brief Pop a value from the stack
261 *
262 * @param context
263 * @return Value*
264 */
265 inline Value* pop(internal::ExecutionContext& context);
266
267 /**
268 * @brief Return a pointer to the top of the stack without consuming it, and resolve it if possible
269 *
270 * @param context
271 * @return Value*
272 */
273 inline Value* peekAndResolveAsPtr(internal::ExecutionContext& context);
274
275 /**
276 * @brief Push a value on the stack
277 *
278 * @param value
279 * @param context
280 */
281 inline void push(const Value& value, internal::ExecutionContext& context) noexcept;
282
283 /**
284 * @brief Push a value on the stack
285 *
286 * @param value
287 * @param context
288 */
289 inline void push(Value&& value, internal::ExecutionContext& context) noexcept;
290
291 /**
292 * @brief Push a value on the stack as a reference
293 *
294 * @param valptr
295 * @param context
296 */
297 inline void push(Value* valptr, internal::ExecutionContext& context) noexcept;
298
299 /**
300 * @brief Pop a value from the stack and resolve it if possible, then return it
301 *
302 * @param context
303 * @return Value*
304 */
305 inline Value* popAndResolveAsPtr(internal::ExecutionContext& context);
306
307 // ================================================
308 // locals related
309 // ================================================
310
311 /**
312 * @brief Find the nearest variable of a given id
313 *
314 * @param id the id to find
315 * @param context
316 * @return Value*
317 */
318 inline Value* findNearestVariable(uint16_t id, internal::ExecutionContext& context) noexcept;
319
320 /**
321 * @brief Destroy the current frame and get back to the previous one, resuming execution
322 *
323 * Doing the job nobody wants to do: cleaning after everyone has finished to play.
324 * This is a sort of primitive garbage collector
325 *
326 * @param context
327 */
328 inline void returnFromFuncCall(internal::ExecutionContext& context);
329
330 /**
331 * @brief Load a plugin from a constant id
332 *
333 * @param id Id of the constant
334 * @param context
335 */
336 void loadPlugin(uint16_t id, internal::ExecutionContext& context);
337
338 // ================================================
339 // error handling
340 // ================================================
341
342 /**
343 * @brief Find the nearest variable id with a given value
344 *
345 * Only used to display the call stack traceback
346 *
347 * @param value the value to search for
348 * @param context
349 * @return uint16_t
350 */
351 uint16_t findNearestVariableIdWithValue(const Value& value, internal::ExecutionContext& context) const noexcept;
352
353 [[noreturn]] void throwArityError(std::size_t passed_arg_count, std::size_t expected_arg_count, internal::ExecutionContext& context);
354
355 void showBacktraceWithException(const std::exception& e, internal::ExecutionContext& context);
356
357 /**
358 * @brief Find the nearest source location information given instruction and page pointers
359 *
360 * @param ip
361 * @param pp
362 * @return std::optional<InstLoc>
363 */
364 std::optional<internal::InstLoc> findSourceLocation(std::size_t ip, std::size_t pp) const;
365
366 std::string debugShowSource() const;
367
368 /**
369 * @brief Display a backtrace when the VM encounter an exception
370 *
371 * @param context
372 * @param os
373 * @param colorize
374 */
375 void backtrace(internal::ExecutionContext& context, std::ostream& os = std::cout, bool colorize = true);
376
377 /**
378 * @brief Function called when the CALL instruction is met in the bytecode
379 *
380 * @param context
381 * @param argc number of arguments already sent
382 * @param function_ptr optional pointer to the function to call. If not provided, obtain it from the stack (unless or_address is not 0)
383 * @param or_address optional page address, used if non-zero and function_ptr is nullptr
384 */
385 inline void call(internal::ExecutionContext& context, uint16_t argc, Value* function_ptr = nullptr, internal::PageAddr_t or_address = 0);
386
387 /**
388 * @brief Builtin called when the CALL_BUILTIN instruction is met in the bytecode
389 *
390 * @param context
391 * @param builtin the builtin to call
392 * @param argc number of arguments already sent
393 * @param remove_return_address remove the return address pushed by the compiler
394 */
395 inline void callBuiltin(internal::ExecutionContext& context, const Value& builtin, uint16_t argc, bool remove_return_address = true);
396 };
397
398#include "VM.inl"
399}
400
401#endif
Host the declaration of all the ArkScript builtins.
Keeping track of the internal data needed by the VM.
Internal object to resolve asynchronously a function call in ArkScript.
The different instructions used by the compiler and virtual machine.
User defined literals for Ark internals.
#define ARK_API
Definition Module.hpp:22
ArkScript configuration macros.
Loads .dll/.so/.dynlib files.
State used by the virtual machine: it loads the bytecode, can compile it if needed,...
Default value type handled by the virtual machine.
Ark state to handle the dirty job of loading and compiling ArkScript code.
Definition State.hpp:33
The ArkScript virtual machine, executing ArkScript bytecode.
Definition VM.hpp:47
internal::ExecutionContext * getDefaultContext()
Return a pointer to the first execution context, for the main thread of the app.
Definition VM.hpp:107
std::vector< std::unique_ptr< internal::Future > > m_futures
Storing the promises while we are resolving them.
Definition VM.hpp:177
int m_exit_code
VM exit code, defaults to 0. Can be changed through sys:exit
Definition VM.hpp:173
std::vector< std::shared_ptr< internal::SharedLibrary > > m_shared_lib_objects
Definition VM.hpp:176
std::vector< std::unique_ptr< internal::ExecutionContext > > m_execution_contexts
Definition VM.hpp:172
std::mutex m_mutex
Definition VM.hpp:175
Value resolve(internal::ExecutionContext *context, const std::vector< Value > &n)
Resolves a function call (called by plugins and builtins)
Definition VM.hpp:61
bool m_running
Definition VM.hpp:174
const bytecode_t & bytecode() const
Definition VM.hpp:161
Value m_undefined_value
Definition VM.hpp:181
State & m_state
Definition VM.hpp:171
Closure management.
Definition Closure.hpp:35
uint16_t PageAddr_t
Definition Closure.hpp:26
std::vector< uint8_t > bytecode_t
Definition Common.hpp:22
STL namespace.