diff options
author | 3gg <3gg@shellblade.net> | 2023-03-02 20:03:52 -0800 |
---|---|---|
committer | 3gg <3gg@shellblade.net> | 2023-03-02 20:03:52 -0800 |
commit | 664006b1c42aae84a3c749d9b71c1047e0b8ffcf (patch) | |
tree | e08f8af944b132742b3bb1d240d8954328e667e5 /vm |
Diffstat (limited to 'vm')
-rw-r--r-- | vm/CMakeLists.txt | 19 | ||||
-rw-r--r-- | vm/src/vm.c | 402 | ||||
-rw-r--r-- | vm/src/vm.h | 166 | ||||
-rw-r--r-- | vm/test/test.h | 185 | ||||
-rw-r--r-- | vm/test/vm_test.c | 182 |
5 files changed, 954 insertions, 0 deletions
diff --git a/vm/CMakeLists.txt b/vm/CMakeLists.txt new file mode 100644 index 0000000..3199ce6 --- /dev/null +++ b/vm/CMakeLists.txt | |||
@@ -0,0 +1,19 @@ | |||
1 | cmake_minimum_required(VERSION 3.0) | ||
2 | |||
3 | project(vm) | ||
4 | |||
5 | # Library | ||
6 | add_library(vm | ||
7 | src/vm.c) | ||
8 | |||
9 | target_include_directories(vm PUBLIC | ||
10 | ${CMAKE_CURRENT_SOURCE_DIR}/src) | ||
11 | |||
12 | # Tests | ||
13 | add_executable(vmtest | ||
14 | test/vm_test.c) | ||
15 | |||
16 | target_link_libraries(vmtest | ||
17 | vm) | ||
18 | |||
19 | target_compile_options(vmtest PRIVATE -DUNIT_TEST -Wall -Wextra) | ||
diff --git a/vm/src/vm.c b/vm/src/vm.c new file mode 100644 index 0000000..559ad5e --- /dev/null +++ b/vm/src/vm.c | |||
@@ -0,0 +1,402 @@ | |||
1 | #include "vm.h" | ||
2 | |||
3 | #include <assert.h> | ||
4 | #include <stdbool.h> | ||
5 | #include <stdio.h> | ||
6 | #include <stdlib.h> | ||
7 | |||
8 | // TODO: Make all these arguments of vm_new(). | ||
9 | |||
10 | // Program's main memory stack size. | ||
11 | #define PROGRAM_STACK_SIZE (64 * 1024) | ||
12 | |||
13 | // Locals stack size. | ||
14 | #define LOCALS_STACK_SIZE 1024 | ||
15 | |||
16 | // Frame stack size. | ||
17 | #define FRAME_STACK_SIZE (16 * 1024) | ||
18 | |||
19 | // Block stack size. | ||
20 | #define BLOCK_STACK_SIZE 1024 | ||
21 | |||
22 | #define IMPLICIT_LABEL -1 | ||
23 | |||
24 | // Locals index. | ||
25 | typedef size_t Index; | ||
26 | |||
27 | // Bools are internally I32s. 0 = false, non-zero = true. | ||
28 | static Type Bool = I32; | ||
29 | typedef int32_t bool_t; | ||
30 | |||
31 | /// Function frame. | ||
32 | /// | ||
33 | /// Every Frame implicitly starts a Block. The function's locals are inside this | ||
34 | /// implicit block. | ||
35 | typedef struct Frame { | ||
36 | Label label; | ||
37 | } Frame; | ||
38 | |||
39 | /// A block of code, used for control flow. | ||
40 | /// | ||
41 | /// Blocks have a label that the machine can jump to. Jumps are constrained to | ||
42 | /// the block labels that are in scope. | ||
43 | /// | ||
44 | /// Block termination automatically frees the block's locals. | ||
45 | typedef struct Block { | ||
46 | Label label; | ||
47 | size_t addr; // Address (saved program counter) of the block. | ||
48 | size_t locals_start; // Offset into the locals stack. | ||
49 | size_t locals_size; // Total size in bytes of local variables. | ||
50 | } Block; | ||
51 | |||
52 | typedef struct Vm { | ||
53 | struct { | ||
54 | bool exit : 1; | ||
55 | } flags; | ||
56 | int32_t exit_code; | ||
57 | size_t pc; // Program instruction counter. | ||
58 | size_t sp; // Program stack pointer. Points to next available slot. | ||
59 | size_t lsp; // Locals stack pointer. Points to next available slot. | ||
60 | size_t fsp; // Frame stack pointer. | ||
61 | size_t bsp; // Block stack pointer. Points to current Block. | ||
62 | uint8_t* stack; // Program stack. Program's main memory. | ||
63 | uint8_t* locals; // Locals stack. Stores locals for each Block. | ||
64 | Frame* frames; // Frame stack for function calls. | ||
65 | Block* blocks; // Block stack for control flow. | ||
66 | } Vm; | ||
67 | |||
68 | Vm* vm_new() { | ||
69 | Vm* vm = calloc(1, sizeof(Vm)); | ||
70 | if (!vm) { | ||
71 | goto cleanup; | ||
72 | } | ||
73 | if (!(vm->stack = calloc(1, PROGRAM_STACK_SIZE))) { | ||
74 | goto cleanup; | ||
75 | } | ||
76 | if (!(vm->locals = calloc(1, LOCALS_STACK_SIZE))) { | ||
77 | goto cleanup; | ||
78 | } | ||
79 | if (!(vm->frames = calloc(FRAME_STACK_SIZE, sizeof(Frame)))) { | ||
80 | goto cleanup; | ||
81 | } | ||
82 | if (!(vm->blocks = calloc(BLOCK_STACK_SIZE, sizeof(Block)))) { | ||
83 | goto cleanup; | ||
84 | } | ||
85 | return vm; | ||
86 | |||
87 | cleanup: | ||
88 | vm_del(&vm); | ||
89 | return 0; | ||
90 | } | ||
91 | |||
92 | void vm_del(Vm** pVm) { | ||
93 | if (pVm && *pVm) { | ||
94 | Vm* vm = *pVm; | ||
95 | if (vm->stack) { | ||
96 | free(vm->stack); | ||
97 | } | ||
98 | if (vm->locals) { | ||
99 | free(vm->locals); | ||
100 | } | ||
101 | if (vm->frames) { | ||
102 | free(vm->frames); | ||
103 | } | ||
104 | if (vm->blocks) { | ||
105 | free(vm->blocks); | ||
106 | } | ||
107 | free(vm); | ||
108 | *pVm = 0; | ||
109 | } | ||
110 | } | ||
111 | |||
112 | // static size_t get_size(Type type) { | ||
113 | // switch (type) { | ||
114 | // case I32: | ||
115 | // return sizeof(int32_t); | ||
116 | // } | ||
117 | // assert(false); | ||
118 | // return 0; | ||
119 | // } | ||
120 | |||
121 | // TODO: Not used? | ||
122 | #define VM_ASSERT(expr) \ | ||
123 | {} | ||
124 | |||
125 | #define TOP(vm, type) \ | ||
126 | (assert(vm->sp >= sizeof(type)), \ | ||
127 | *((const type*)&vm->stack[vm->sp - sizeof(type)])) | ||
128 | |||
129 | #define PUSH(vm, value, type) \ | ||
130 | assert(vm->sp + sizeof(type) <= PROGRAM_STACK_SIZE); \ | ||
131 | *((type*)(&vm->stack[vm->sp])) = value; \ | ||
132 | vm->sp += sizeof(type); | ||
133 | |||
134 | #define POP(vm, type) \ | ||
135 | (assert(vm->sp >= sizeof(type)), vm->sp -= sizeof(type), \ | ||
136 | *((type*)(&vm->stack[vm->sp]))) | ||
137 | |||
138 | #define BLOCK_PUSH(vm, block) \ | ||
139 | assert(vm->bsp < BLOCK_STACK_SIZE); \ | ||
140 | vm->blocks[++vm->bsp] = block; | ||
141 | |||
142 | #define BLOCK_POP(vm) \ | ||
143 | assert(vm->bsp > 0); \ | ||
144 | vm->locals -= vm->blocks[vm->bsp].locals_size; \ | ||
145 | vm->bsp--; | ||
146 | |||
147 | #define PUSH_LOCAL(vm, type) \ | ||
148 | assert(vm->lsp + sizeof(type) <= LOCALS_STACK_SIZE); \ | ||
149 | /* Auto-initialize locals to 0. */ \ | ||
150 | *((type*)(&vm->locals[vm->lsp])) = 0; \ | ||
151 | vm->lsp += sizeof(type); \ | ||
152 | vm->blocks[vm->bsp].locals_size += sizeof(type); | ||
153 | |||
154 | #define POP_LOCAL(vm, type) \ | ||
155 | (assert(vm->lsp >= sizeof(type)), vm->lsp -= sizeof(type), \ | ||
156 | vm->blocks[vm->bsp].locals -= sizeof(type), \ | ||
157 | *((type*)(&vm->locals[vm->lsp]))) | ||
158 | |||
159 | // TODO: Should be an offset from the current frame, not block. | ||
160 | #define GET_LOCAL_PTR(vm, idx, type) \ | ||
161 | ((const type*)(&vm->locals[vm->blocks[vm->bsp].locals_start + idx])) | ||
162 | // TODO: Same here. | ||
163 | #define GET_LOCAL_PTR_MUT(vm, idx, type) \ | ||
164 | ((type*)(&vm->locals[vm->blocks[vm->bsp].locals_start + idx])) | ||
165 | |||
166 | #define LOCAL_RD(vm, idx, type) (*GET_LOCAL_PTR(vm, idx, type)) | ||
167 | |||
168 | #define LOCAL_WR(vm, idx, val, type) (*GET_LOCAL_PTR_MUT(vm, idx, type) = val) | ||
169 | |||
170 | static void push(Vm* vm, Inst inst) { | ||
171 | switch (inst.type) { | ||
172 | case I32: | ||
173 | PUSH(vm, inst.payload.i32, int32_t); | ||
174 | break; | ||
175 | case F32: | ||
176 | PUSH(vm, inst.payload.f32, float); | ||
177 | break; | ||
178 | } | ||
179 | } | ||
180 | |||
181 | static Value pop(Vm* vm, Type type) { | ||
182 | Value val; | ||
183 | switch (type) { | ||
184 | case I32: | ||
185 | val.i32 = POP(vm, int32_t); | ||
186 | break; | ||
187 | case F32: | ||
188 | val.f32 = POP(vm, float); | ||
189 | break; | ||
190 | } | ||
191 | return val; | ||
192 | } | ||
193 | |||
194 | static void vm_exit(Vm* vm, Inst inst) { | ||
195 | vm->exit_code = vm->sp == 0 ? 0 : POP(vm, int32_t); | ||
196 | vm->flags.exit = true; | ||
197 | } | ||
198 | |||
199 | #define ADD(vm, a, b, field, type) \ | ||
200 | { \ | ||
201 | type result = ((a.field) + (b.field)); \ | ||
202 | PUSH(vm, result, type); \ | ||
203 | } | ||
204 | |||
205 | static void add(Vm* vm, Type type) { | ||
206 | Value opr1 = pop(vm, type); | ||
207 | Value opr2 = pop(vm, type); | ||
208 | switch (type) { | ||
209 | case I32: | ||
210 | ADD(vm, opr1, opr2, i32, int32_t); | ||
211 | break; | ||
212 | |||
213 | case F32: | ||
214 | ADD(vm, opr1, opr2, f32, float); | ||
215 | break; | ||
216 | } | ||
217 | } | ||
218 | |||
219 | static void dec(Vm* vm, Type type) { | ||
220 | Value top = pop(vm, type); | ||
221 | switch (type) { | ||
222 | case I32: | ||
223 | PUSH(vm, top.i32 - 1, int32_t); | ||
224 | break; | ||
225 | case F32: | ||
226 | PUSH(vm, top.f32 - 1.0f, float); | ||
227 | break; | ||
228 | } | ||
229 | } | ||
230 | |||
231 | static void empty(Vm* vm) { PUSH(vm, vm->sp == 0, bool_t); } | ||
232 | |||
233 | #define CMP(vm, val, type) (POP(vm, type) == val) | ||
234 | |||
235 | static void cmp(Vm* vm, Inst inst) { | ||
236 | switch (inst.type) { | ||
237 | case I32: | ||
238 | PUSH(vm, CMP(vm, inst.payload.i32, int32_t), bool_t); | ||
239 | break; | ||
240 | case F32: | ||
241 | PUSH(vm, CMP(vm, inst.payload.f32, float), bool_t); | ||
242 | break; | ||
243 | } | ||
244 | } | ||
245 | |||
246 | static void end(Vm* vm) { BLOCK_POP(vm); } | ||
247 | |||
248 | static void loop(Vm* vm, Inst inst) { | ||
249 | const Block block = (Block){.label = inst.payload.i32, .addr = vm->pc}; | ||
250 | BLOCK_PUSH(vm, block); | ||
251 | } | ||
252 | |||
253 | static void br(Vm* vm, Inst inst) { | ||
254 | const Branch branch = inst.payload.branch; | ||
255 | const int32_t label = branch.label; | ||
256 | const bool value = branch.expected; | ||
257 | const bool is_conditional = branch.conditional; | ||
258 | bool should_branch = is_conditional ? POP(vm, bool_t) == value : true; | ||
259 | // printf("is conditional: %d\n", is_conditional); | ||
260 | // printf("value: %d\n", value); | ||
261 | // printf("should branch: %d\n", should_branch); | ||
262 | if (should_branch) { | ||
263 | while (vm->bsp > 0) { | ||
264 | const Block block = vm->blocks[vm->bsp]; | ||
265 | if (block.label == label) { | ||
266 | vm->pc = block.addr; | ||
267 | vm->pc--; // Account for increment at every step of the VM loop. | ||
268 | return; | ||
269 | } | ||
270 | vm->bsp--; | ||
271 | } | ||
272 | // We should be able to find the label in the block stack. | ||
273 | assert(false); | ||
274 | } | ||
275 | } | ||
276 | |||
277 | static void vm_break(Vm* vm, Inst inst) { | ||
278 | // TODO. | ||
279 | // Step over instructions until an End instruction is found. | ||
280 | } | ||
281 | |||
282 | static void local(Vm* vm, Type type) { | ||
283 | switch (type) { | ||
284 | case I32: | ||
285 | PUSH_LOCAL(vm, int32_t); | ||
286 | break; | ||
287 | case F32: | ||
288 | PUSH_LOCAL(vm, float); | ||
289 | break; | ||
290 | } | ||
291 | } | ||
292 | |||
293 | static void local_rd(Vm* vm, Inst inst) { | ||
294 | const Index idx = (Index)inst.payload.u64; | ||
295 | switch (inst.type) { | ||
296 | case I32: | ||
297 | PUSH(vm, LOCAL_RD(vm, idx, int32_t), int32_t); | ||
298 | break; | ||
299 | case F32: | ||
300 | PUSH(vm, LOCAL_RD(vm, idx, float), float); | ||
301 | break; | ||
302 | } | ||
303 | } | ||
304 | |||
305 | static void local_wr(Vm* vm, Inst inst) { | ||
306 | const Index idx = (Index)inst.payload.u64; | ||
307 | const Value top = pop(vm, inst.type); | ||
308 | switch (inst.type) { | ||
309 | case I32: | ||
310 | LOCAL_WR(vm, idx, top.i32, int32_t); | ||
311 | break; | ||
312 | case F32: | ||
313 | LOCAL_WR(vm, idx, top.f32, float); | ||
314 | break; | ||
315 | } | ||
316 | } | ||
317 | |||
318 | static void exec(Vm* vm, Inst inst) { | ||
319 | switch (inst.op) { | ||
320 | case Exit: | ||
321 | vm_exit(vm, inst); | ||
322 | break; | ||
323 | case Push: | ||
324 | push(vm, inst); | ||
325 | break; | ||
326 | case Pop: | ||
327 | pop(vm, inst.type); | ||
328 | break; | ||
329 | case Add: | ||
330 | add(vm, inst.type); | ||
331 | break; | ||
332 | case Sub: | ||
333 | break; | ||
334 | case Mul: | ||
335 | break; | ||
336 | case Div: | ||
337 | break; | ||
338 | case Dec: | ||
339 | dec(vm, inst.type); | ||
340 | break; | ||
341 | case Empty: | ||
342 | empty(vm); | ||
343 | break; | ||
344 | case Cmp: | ||
345 | cmp(vm, inst); | ||
346 | break; | ||
347 | case End: | ||
348 | end(vm); | ||
349 | break; | ||
350 | case Break: | ||
351 | vm_break(vm, inst); | ||
352 | break; | ||
353 | case Loop: | ||
354 | loop(vm, inst); | ||
355 | break; | ||
356 | case Br: | ||
357 | br(vm, inst); | ||
358 | break; | ||
359 | case Func: // TODO | ||
360 | break; | ||
361 | case Arg: // TODO | ||
362 | break; | ||
363 | case Call: // TODO | ||
364 | break; | ||
365 | case Local: | ||
366 | local(vm, inst.type); | ||
367 | break; | ||
368 | case LocalRd: | ||
369 | local_rd(vm, inst); | ||
370 | break; | ||
371 | case LocalWr: | ||
372 | local_wr(vm, inst); | ||
373 | break; | ||
374 | } | ||
375 | } | ||
376 | |||
377 | static void init(Vm* vm) { | ||
378 | // Create an implicit frame for the start of the program. | ||
379 | vm->frames[0] = (Frame){.label = IMPLICIT_LABEL}; | ||
380 | vm->blocks[0] = (Block){.label = IMPLICIT_LABEL}; | ||
381 | // TODO: Reset all Vm state. | ||
382 | } | ||
383 | |||
384 | int vm_run(Vm* vm, const Inst instructions[], size_t count) { | ||
385 | assert(vm); | ||
386 | init(vm); | ||
387 | for (vm->pc = 0; !vm->flags.exit && vm->pc < count; vm->pc++) { | ||
388 | const Inst inst = instructions[vm->pc]; | ||
389 | exec(vm, inst); | ||
390 | } | ||
391 | // printf("exit code: %d\n", vm->exit_code); | ||
392 | return vm->exit_code; | ||
393 | } | ||
394 | |||
395 | void vm_print_stack(const Vm* vm) { | ||
396 | printf("stack start\n"); | ||
397 | for (size_t i = 0; i < vm->sp; ++i) { | ||
398 | const char sep = (i == vm->sp - 1) ? '\n' : ' '; | ||
399 | printf("%x%c", vm->stack[i], sep); | ||
400 | } | ||
401 | printf("stack end\n"); | ||
402 | } | ||
diff --git a/vm/src/vm.h b/vm/src/vm.h new file mode 100644 index 0000000..03dfc88 --- /dev/null +++ b/vm/src/vm.h | |||
@@ -0,0 +1,166 @@ | |||
1 | #pragma once | ||
2 | |||
3 | #include <stdbool.h> | ||
4 | #include <stddef.h> | ||
5 | #include <stdint.h> | ||
6 | |||
7 | typedef enum Op { | ||
8 | Exit, // Pop value from the stack and return as exit code. Return 0 if the | ||
9 | // stack is empty. | ||
10 | Push, | ||
11 | Pop, | ||
12 | Add, | ||
13 | Sub, | ||
14 | Mul, | ||
15 | Div, | ||
16 | Dec, // Decrement the top of the stack by 1. | ||
17 | Empty, // Check whether the stack is empty. Pushes a bool. | ||
18 | Cmp, // Pop the top of the stack and compare it with the payload. Pushes a | ||
19 | // bool. | ||
20 | /* Blocks */ | ||
21 | End, // Marks the end of a block. | ||
22 | Break, // Exit the current block. | ||
23 | Loop, // Push a loop block. Payload (i32): label. | ||
24 | /* Branches */ | ||
25 | Br, // Branch. Payload (i64): [(i32) conditional? | (i32) label]. | ||
26 | // A condtional branch pops a bool from the stack and branches if true. | ||
27 | // The condition can also be negated. See br_if(). | ||
28 | /* Functions */ | ||
29 | Func, | ||
30 | Arg, | ||
31 | Call, | ||
32 | /* Locals */ | ||
33 | Local, // Create a local variable. | ||
34 | LocalRd, // Load a local variable into the top of the stack. | ||
35 | LocalWr, // Pop the top of the stack and store it in a local variable. | ||
36 | } Op; | ||
37 | |||
38 | typedef enum Type { | ||
39 | I32, | ||
40 | F32, | ||
41 | } Type; | ||
42 | |||
43 | // Label type for blocks and locals. | ||
44 | typedef uint32_t Label; | ||
45 | |||
46 | typedef struct Branch { | ||
47 | Label label; | ||
48 | bool conditional : 1; // True for conditional branches. | ||
49 | bool expected : 1; // Comparison value for conditional branches. | ||
50 | } Branch; | ||
51 | |||
52 | typedef struct Function { | ||
53 | Label label; | ||
54 | } Function; | ||
55 | |||
56 | typedef struct Value { | ||
57 | union { | ||
58 | uint64_t u64; | ||
59 | int32_t i32; | ||
60 | float f32; | ||
61 | Branch branch; | ||
62 | Label label; | ||
63 | }; | ||
64 | } Value; | ||
65 | |||
66 | typedef struct Inst { | ||
67 | Op op : 5; | ||
68 | Type type : 2; | ||
69 | Value payload; | ||
70 | } Inst; | ||
71 | |||
72 | typedef struct Vm Vm; | ||
73 | |||
74 | // ----------------------------------------------------------------------------- | ||
75 | // VM API | ||
76 | |||
77 | /// Create a new virtual machine. | ||
78 | Vm* vm_new(); | ||
79 | |||
80 | /// Destroy the virtual machine. | ||
81 | void vm_del(Vm**); | ||
82 | |||
83 | /// Execute code on the virtual machine. | ||
84 | /// | ||
85 | /// Returns the program exit code if an exit operation is executed, 0 otherwise. | ||
86 | int vm_run(Vm*, const Inst[], size_t count); | ||
87 | |||
88 | /// Prints the virtual machine's stack to stdout. | ||
89 | void vm_print_stack(const Vm*); | ||
90 | |||
91 | // ----------------------------------------------------------------------------- | ||
92 | // Programming API | ||
93 | |||
94 | /// Exit the program. | ||
95 | static inline Inst vmExit() { return (Inst){.op = Exit}; } | ||
96 | |||
97 | /// Push a value. | ||
98 | static inline Inst vmPushI32(int32_t value) { | ||
99 | return (Inst){.op = Push, .type = I32, .payload = (Value){.i32 = value}}; | ||
100 | } | ||
101 | |||
102 | /// Pop a value. | ||
103 | static inline Inst vmPop(Type type) { return (Inst){.op = Pop, .type = type}; } | ||
104 | |||
105 | /// Add two values. | ||
106 | static inline Inst vmAdd(Type type) { return (Inst){.op = Add, .type = type}; } | ||
107 | |||
108 | /// Decrement a value. | ||
109 | static inline Inst vmDec(Type type) { return (Inst){.op = Dec, .type = type}; } | ||
110 | |||
111 | /// Compare a value. | ||
112 | static inline Inst vmCmpI32(int32_t value) { | ||
113 | return (Inst){.op = Cmp, .type = I32, .payload = (Value){.i32 = value}}; | ||
114 | } | ||
115 | |||
116 | /// End the current block. | ||
117 | static inline Inst vmEnd() { return (Inst){.op = End}; } | ||
118 | |||
119 | /// Create a loop. | ||
120 | static inline Inst vmLoop(Label label) { | ||
121 | return (Inst){.op = Loop, .payload = (Value){.label = label}}; | ||
122 | } | ||
123 | |||
124 | /// Create the payload of a conditional branch. | ||
125 | static inline Inst vmBr_if(bool value, Label label) { | ||
126 | return (Inst){ | ||
127 | .op = Br, | ||
128 | .payload = (Value){ | ||
129 | .branch = { | ||
130 | .label = label, | ||
131 | .conditional = 1, | ||
132 | .expected = value, | ||
133 | }}}; | ||
134 | } | ||
135 | |||
136 | /// Create a function. | ||
137 | static inline Inst vmFunc(Label label) { | ||
138 | return (Inst){.op = Func, .payload = (Value){.label = label}}; | ||
139 | } | ||
140 | |||
141 | /// Create a function argument. | ||
142 | static inline Inst vmArg(Type type, Label label) { | ||
143 | return (Inst){.op = Arg, .type = type, .payload = (Value){.label = label}}; | ||
144 | } | ||
145 | |||
146 | /// Call a function. | ||
147 | static inline Inst vmCall(Label label) { | ||
148 | return (Inst){.op = Call, .payload = (Value){.label = label}}; | ||
149 | } | ||
150 | |||
151 | /// Create a local variable. | ||
152 | static inline Inst vmLocal(Type type, Label label) { | ||
153 | return (Inst){.op = Local, .type = type, .payload = (Value){.label = label}}; | ||
154 | } | ||
155 | |||
156 | /// Read a local variable. | ||
157 | static inline Inst vmLocalRd(Type type, Label label) { | ||
158 | return (Inst){ | ||
159 | .op = LocalRd, .type = type, .payload = (Value){.label = label}}; | ||
160 | } | ||
161 | |||
162 | /// Write a local variable. | ||
163 | static inline Inst vmLocalWr(Type type, Label label) { | ||
164 | return (Inst){ | ||
165 | .op = LocalWr, .type = type, .payload = (Value){.label = label}}; | ||
166 | } | ||
diff --git a/vm/test/test.h b/vm/test/test.h new file mode 100644 index 0000000..fd8dc22 --- /dev/null +++ b/vm/test/test.h | |||
@@ -0,0 +1,185 @@ | |||
1 | // SPDX-License-Identifier: MIT | ||
2 | #pragma once | ||
3 | |||
4 | #ifdef UNIT_TEST | ||
5 | |||
6 | #include <stdbool.h> | ||
7 | #include <stdio.h> | ||
8 | #include <stdlib.h> | ||
9 | #include <string.h> | ||
10 | |||
11 | #if defined(__DragonFly__) || defined(__FreeBSD__) || defined(__FreeBSD_kernel__) || \ | ||
12 | defined(__NetBSD__) || defined(__OpenBSD__) | ||
13 | #define USE_SYSCTL_FOR_ARGS 1 | ||
14 | // clang-format off | ||
15 | #include <sys/types.h> | ||
16 | #include <sys/sysctl.h> | ||
17 | // clang-format on | ||
18 | #include <unistd.h> // getpid | ||
19 | #endif | ||
20 | |||
21 | struct test_file_metadata; | ||
22 | |||
23 | struct test_failure { | ||
24 | bool present; | ||
25 | const char *message; | ||
26 | const char *file; | ||
27 | int line; | ||
28 | }; | ||
29 | |||
30 | struct test_case_metadata { | ||
31 | void (*fn)(struct test_case_metadata *, struct test_file_metadata *); | ||
32 | struct test_failure failure; | ||
33 | const char *name; | ||
34 | struct test_case_metadata *next; | ||
35 | }; | ||
36 | |||
37 | struct test_file_metadata { | ||
38 | bool registered; | ||
39 | const char *name; | ||
40 | struct test_file_metadata *next; | ||
41 | struct test_case_metadata *tests; | ||
42 | }; | ||
43 | |||
44 | struct test_file_metadata __attribute__((weak)) * test_file_head; | ||
45 | |||
46 | #define SET_FAILURE(_message) \ | ||
47 | metadata->failure = (struct test_failure) { \ | ||
48 | .message = _message, .file = __FILE__, .line = __LINE__, .present = true, \ | ||
49 | } | ||
50 | |||
51 | #define TEST_EQUAL(a, b) \ | ||
52 | do { \ | ||
53 | if ((a) != (b)) { \ | ||
54 | SET_FAILURE(#a " != " #b); \ | ||
55 | return; \ | ||
56 | } \ | ||
57 | } while (0) | ||
58 | |||
59 | #define TEST_TRUE(a) \ | ||
60 | do { \ | ||
61 | if (!(a)) { \ | ||
62 | SET_FAILURE(#a " is not true"); \ | ||
63 | return; \ | ||
64 | } \ | ||
65 | } while (0) | ||
66 | |||
67 | #define TEST_STREQUAL(a, b) \ | ||
68 | do { \ | ||
69 | if (strcmp(a, b) != 0) { \ | ||
70 | SET_FAILURE(#a " != " #b); \ | ||
71 | return; \ | ||
72 | } \ | ||
73 | } while (0) | ||
74 | |||
75 | #define TEST_CASE(_name) \ | ||
76 | static void __test_h_##_name(struct test_case_metadata *, \ | ||
77 | struct test_file_metadata *); \ | ||
78 | static struct test_file_metadata __test_h_file; \ | ||
79 | static struct test_case_metadata __test_h_meta_##_name = { \ | ||
80 | .name = #_name, \ | ||
81 | .fn = __test_h_##_name, \ | ||
82 | }; \ | ||
83 | static void __attribute__((constructor(101))) __test_h_##_name##_register(void) { \ | ||
84 | __test_h_meta_##_name.next = __test_h_file.tests; \ | ||
85 | __test_h_file.tests = &__test_h_meta_##_name; \ | ||
86 | if (!__test_h_file.registered) { \ | ||
87 | __test_h_file.name = __FILE__; \ | ||
88 | __test_h_file.next = test_file_head; \ | ||
89 | test_file_head = &__test_h_file; \ | ||
90 | __test_h_file.registered = true; \ | ||
91 | } \ | ||
92 | } \ | ||
93 | static void __test_h_##_name( \ | ||
94 | struct test_case_metadata *metadata __attribute__((unused)), \ | ||
95 | struct test_file_metadata *file_metadata __attribute__((unused))) | ||
96 | |||
97 | extern void __attribute__((weak)) (*test_h_unittest_setup)(void); | ||
98 | /// Run defined tests, return true if all tests succeeds | ||
99 | /// @param[out] tests_run if not NULL, set to whether tests were run | ||
100 | static inline void __attribute__((constructor(102))) run_tests(void) { | ||
101 | bool should_run = false; | ||
102 | #ifdef USE_SYSCTL_FOR_ARGS | ||
103 | int mib[] = { | ||
104 | CTL_KERN, | ||
105 | #if defined(__NetBSD__) || defined(__OpenBSD__) | ||
106 | KERN_PROC_ARGS, | ||
107 | getpid(), | ||
108 | KERN_PROC_ARGV, | ||
109 | #else | ||
110 | KERN_PROC, | ||
111 | KERN_PROC_ARGS, | ||
112 | getpid(), | ||
113 | #endif | ||
114 | }; | ||
115 | char *arg = NULL; | ||
116 | size_t arglen; | ||
117 | sysctl(mib, sizeof(mib) / sizeof(mib[0]), NULL, &arglen, NULL, 0); | ||
118 | arg = malloc(arglen); | ||
119 | sysctl(mib, sizeof(mib) / sizeof(mib[0]), arg, &arglen, NULL, 0); | ||
120 | #else | ||
121 | FILE *cmdlinef = fopen("/proc/self/cmdline", "r"); | ||
122 | char *arg = NULL; | ||
123 | int arglen; | ||
124 | fscanf(cmdlinef, "%ms%n", &arg, &arglen); | ||
125 | fclose(cmdlinef); | ||
126 | #endif | ||
127 | for (char *pos = arg; pos < arg + arglen; pos += strlen(pos) + 1) { | ||
128 | if (strcmp(pos, "--unittest") == 0) { | ||
129 | should_run = true; | ||
130 | break; | ||
131 | } | ||
132 | } | ||
133 | free(arg); | ||
134 | |||
135 | if (!should_run) { | ||
136 | return; | ||
137 | } | ||
138 | |||
139 | if (&test_h_unittest_setup) { | ||
140 | test_h_unittest_setup(); | ||
141 | } | ||
142 | |||
143 | struct test_file_metadata *i = test_file_head; | ||
144 | int failed = 0, success = 0; | ||
145 | while (i) { | ||
146 | fprintf(stderr, "Running tests from %s:\n", i->name); | ||
147 | struct test_case_metadata *j = i->tests; | ||
148 | while (j) { | ||
149 | fprintf(stderr, "\t%s ... ", j->name); | ||
150 | j->failure.present = false; | ||
151 | j->fn(j, i); | ||
152 | if (j->failure.present) { | ||
153 | fprintf(stderr, "failed (%s at %s:%d)\n", j->failure.message, | ||
154 | j->failure.file, j->failure.line); | ||
155 | failed++; | ||
156 | } else { | ||
157 | fprintf(stderr, "passed\n"); | ||
158 | success++; | ||
159 | } | ||
160 | j = j->next; | ||
161 | } | ||
162 | fprintf(stderr, "\n"); | ||
163 | i = i->next; | ||
164 | } | ||
165 | int total = failed + success; | ||
166 | fprintf(stderr, "Test results: passed %d/%d, failed %d/%d\n", success, total, | ||
167 | failed, total); | ||
168 | exit(failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE); | ||
169 | } | ||
170 | |||
171 | #else | ||
172 | |||
173 | #include <stdbool.h> | ||
174 | |||
175 | #define TEST_CASE(name) static void __attribute__((unused)) __test_h_##name(void) | ||
176 | |||
177 | #define TEST_EQUAL(a, b) \ | ||
178 | (void)(a); \ | ||
179 | (void)(b) | ||
180 | #define TEST_TRUE(a) (void)(a) | ||
181 | #define TEST_STREQUAL(a, b) \ | ||
182 | (void)(a); \ | ||
183 | (void)(b) | ||
184 | |||
185 | #endif | ||
diff --git a/vm/test/vm_test.c b/vm/test/vm_test.c new file mode 100644 index 0000000..2d1a91f --- /dev/null +++ b/vm/test/vm_test.c | |||
@@ -0,0 +1,182 @@ | |||
1 | #include "vm.h" | ||
2 | |||
3 | #include "test.h" | ||
4 | |||
5 | #include <stdio.h> | ||
6 | |||
7 | /// Create and destroy a vm. | ||
8 | TEST_CASE(vm_create_destroy) { | ||
9 | Vm* vm = vm_new(); | ||
10 | TEST_TRUE(vm != 0); | ||
11 | vm_del(&vm); | ||
12 | } | ||
13 | |||
14 | // Exit with an implicit 0 exit code. | ||
15 | TEST_CASE(vm_exit_implicit) { | ||
16 | // clang-format off | ||
17 | const Inst instructions[] = { | ||
18 | vmExit(), | ||
19 | }; | ||
20 | // clang-format on | ||
21 | |||
22 | Vm* vm = vm_new(); | ||
23 | TEST_TRUE(vm != 0); | ||
24 | const int exit_code = | ||
25 | vm_run(vm, instructions, sizeof(instructions) / sizeof(Inst)); | ||
26 | TEST_TRUE(exit_code == 0); | ||
27 | vm_del(&vm); | ||
28 | } | ||
29 | |||
30 | // Exit with an explicit exit code. | ||
31 | TEST_CASE(vm_exit_explicit) { | ||
32 | const int32_t expected = 17; | ||
33 | |||
34 | // clang-format off | ||
35 | const Inst instructions[] = { | ||
36 | vmPushI32(expected), | ||
37 | vmExit(), | ||
38 | }; | ||
39 | // clang-format on | ||
40 | |||
41 | Vm* vm = vm_new(); | ||
42 | TEST_TRUE(vm != 0); | ||
43 | const int exit_code = | ||
44 | vm_run(vm, instructions, sizeof(instructions) / sizeof(Inst)); | ||
45 | TEST_TRUE(exit_code == expected); | ||
46 | vm_del(&vm); | ||
47 | } | ||
48 | |||
49 | /// Add two i32 numbers. | ||
50 | TEST_CASE(vm_add_i32) { | ||
51 | const int n1 = 2; | ||
52 | const int n2 = 3; | ||
53 | |||
54 | // clang-format off | ||
55 | const Inst instructions[] = { | ||
56 | vmPushI32(n1), | ||
57 | vmPushI32(n2), | ||
58 | vmAdd(I32), | ||
59 | vmExit(), | ||
60 | }; | ||
61 | // clang-format on | ||
62 | |||
63 | Vm* vm = vm_new(); | ||
64 | TEST_TRUE(vm != 0); | ||
65 | const int exit_code = | ||
66 | vm_run(vm, instructions, sizeof(instructions) / sizeof(Inst)); | ||
67 | TEST_EQUAL(exit_code, n1 + n2); | ||
68 | vm_del(&vm); | ||
69 | } | ||
70 | |||
71 | /// Sum an array of numbers with 4 add instructions. | ||
72 | TEST_CASE(vm_sum_array_i32_explicit) { | ||
73 | const int vals[5] = {1, 2, 3, 4, 5}; | ||
74 | |||
75 | // clang-format off | ||
76 | const Inst instructions[] = { | ||
77 | vmPushI32(vals[0]), | ||
78 | vmPushI32(vals[1]), | ||
79 | vmPushI32(vals[2]), | ||
80 | vmPushI32(vals[3]), | ||
81 | vmPushI32(vals[4]), | ||
82 | vmAdd(I32), | ||
83 | vmAdd(I32), | ||
84 | vmAdd(I32), | ||
85 | vmAdd(I32), | ||
86 | vmExit(), | ||
87 | }; | ||
88 | // clang-format on | ||
89 | |||
90 | int sum = 0; | ||
91 | for (size_t i = 0; i < sizeof(vals) / sizeof(vals[0]); ++i) { | ||
92 | sum += vals[i]; | ||
93 | } | ||
94 | |||
95 | Vm* vm = vm_new(); | ||
96 | TEST_TRUE(vm != 0); | ||
97 | const int exit_code = | ||
98 | vm_run(vm, instructions, sizeof(instructions) / sizeof(Inst)); | ||
99 | TEST_EQUAL(exit_code, sum); | ||
100 | vm_del(&vm); | ||
101 | } | ||
102 | |||
103 | /// Sum an array of numbers with a loop. | ||
104 | TEST_CASE(vm_sum_array_i32_loop) { | ||
105 | const int vals[5] = {1, 2, 3, 4, 5}; | ||
106 | |||
107 | const Label loop_label = 0; | ||
108 | const Label counter_index = 0; | ||
109 | |||
110 | // clang-format off | ||
111 | const Inst instructions[] = { | ||
112 | vmPushI32(vals[0]), | ||
113 | vmPushI32(vals[1]), | ||
114 | vmPushI32(vals[2]), | ||
115 | vmPushI32(vals[3]), | ||
116 | vmPushI32(vals[4]), | ||
117 | vmLocal(I32, counter_index), | ||
118 | vmPushI32(sizeof(vals) / sizeof(vals[0]) - 1), | ||
119 | vmLocalWr(I32, counter_index), | ||
120 | vmLoop(loop_label), | ||
121 | vmAdd(I32), | ||
122 | vmLocalRd(I32, counter_index), | ||
123 | vmDec(I32), | ||
124 | // TODO: Could be useful to have a function that writes the local but | ||
125 | // leaves its value on the stack. | ||
126 | vmLocalWr(I32, counter_index), | ||
127 | vmLocalRd(I32, counter_index), | ||
128 | // TODO: Perhaps we should expect the comparison value to also be pushed | ||
129 | // to the stack. | ||
130 | vmCmpI32(0), | ||
131 | vmBr_if(false, loop_label), | ||
132 | vmEnd(), | ||
133 | vmExit(), | ||
134 | }; | ||
135 | // clang-format on | ||
136 | |||
137 | int sum = 0; | ||
138 | for (size_t i = 0; i < sizeof(vals) / sizeof(vals[0]); ++i) { | ||
139 | sum += vals[i]; | ||
140 | } | ||
141 | |||
142 | Vm* vm = vm_new(); | ||
143 | TEST_TRUE(vm != 0); | ||
144 | const int exit_code = | ||
145 | vm_run(vm, instructions, sizeof(instructions) / sizeof(Inst)); | ||
146 | TEST_EQUAL(exit_code, sum); | ||
147 | vm_del(&vm); | ||
148 | } | ||
149 | |||
150 | // Call a function to add two numbers. | ||
151 | TEST_CASE(vm_function_call) { | ||
152 | const Label func = 0; | ||
153 | const Label a = 0; | ||
154 | const Label b = 1; | ||
155 | const int32_t a_val = 3; | ||
156 | const int32_t b_val = 5; | ||
157 | const int32_t expected = a + b; | ||
158 | |||
159 | // clang-format off | ||
160 | const Inst instructions[] = { | ||
161 | /* Function definition */ | ||
162 | vmFunc(func), | ||
163 | vmArg(I32, b), | ||
164 | vmArg(I32, a), | ||
165 | vmAdd(I32), | ||
166 | vmEnd(), | ||
167 | /* Main program */ | ||
168 | vmPushI32(a_val), | ||
169 | vmPushI32(b_val), | ||
170 | vmCall(func), | ||
171 | vmExit(), | ||
172 | }; | ||
173 | // clang-format on | ||
174 | |||
175 | Vm* vm = vm_new(); | ||
176 | TEST_TRUE(vm != 0); | ||
177 | // const int exit_code = | ||
178 | // vm_run(vm, instructions, sizeof(instructions) / sizeof(Inst)); | ||
179 | vm_del(&vm); | ||
180 | } | ||
181 | |||
182 | int main() { return 0; } | ||