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