1#include "asm.h"
2#include "vm.h"
3
4#include <inttypes.h>
5#include <stdio.h>
6#include <stdlib.h>
7#include <stdint.h>
8#include <string.h>
9
10enum {
11 ERROR_TEXT_CAPACITY = 256,
12 OUTPUT_CAPACITY = 8192
13};
14
15typedef struct OutputCapture {
16 char text[OUTPUT_CAPACITY];
17 size_t length;
18} OutputCapture;
19
20#define FAIL(...) \
21 do { \
22 fprintf(stderr, "FAIL %s:%d: ", __func__, __LINE__); \
23 fprintf(stderr, __VA_ARGS__); \
24 fputc('\n', stderr); \
25 return 0; \
26 } while (0)
27
28#define CHECK(condition, ...) \
29 do { \
30 if (!(condition)) { \
31 FAIL(__VA_ARGS__); \
32 } \
33 } while (0)
34
35static int capture_value(void *user, int64_t value) {
36 OutputCapture *capture = user;
37 char line[64];
38 int count;
39
40 if (capture == NULL) {
41 return -1;
42 }
43
44 count = snprintf(line, sizeof(line), "%" PRId64 "\n", value);
45 if (count < 0 || (size_t)count >= sizeof(line) ||
46 (size_t)count >= OUTPUT_CAPACITY - capture->length) {
47 return -1;
48 }
49
50 memcpy(capture->text + capture->length, line, (size_t)count);
51 capture->length += (size_t)count;
52 capture->text[capture->length] = '\0';
53 return 0;
54}
55
56static int reject_output(void *user, int64_t value) {
57 (void)user;
58 (void)value;
59 return -1;
60}
61
62static size_t emit_opcode(uint8_t *code, size_t offset, VMOpcode opcode) {
63 code[offset] = (uint8_t)opcode;
64 return offset + VM_OPCODE_SIZE;
65}
66
67static size_t emit_operand_opcode(uint8_t *code, size_t offset, VMOpcode opcode,
68 int32_t operand) {
69 uint32_t bits = (uint32_t)operand;
70
71 code[offset] = (uint8_t)opcode;
72 code[offset + 1] = (uint8_t)(bits & UINT32_C(0xff));
73 code[offset + 2] = (uint8_t)((bits >> 8) & UINT32_C(0xff));
74 code[offset + 3] = (uint8_t)((bits >> 16) & UINT32_C(0xff));
75 code[offset + 4] = (uint8_t)((bits >> 24) & UINT32_C(0xff));
76 return offset + VM_OPCODE_SIZE + VM_OPERAND_SIZE;
77}
78
79static int execute_callback(const uint8_t *code, size_t code_length,
80 size_t max_stack, size_t max_call_frames,
81 VMOutputFn output, void *output_user,
82 VMError *result) {
83 VM vm = {0};
84
85 if (vm_init(&vm, max_stack, max_call_frames) != 0) {
86 fprintf(stderr, "could not initialize VM: %s\n", vm_error_string(vm.error));
87 vm_free(&vm);
88 return 0;
89 }
90
91 *result = vm_run_callback(&vm, code, code_length, output, output_user);
92 vm_free(&vm);
93 return 1;
94}
95
96static int expect_vm_error(const char *case_name, const uint8_t *code,
97 size_t code_length, size_t max_stack,
98 size_t max_call_frames, VMError expected) {
99 OutputCapture output = {{0}, 0};
100 VMError actual;
101
102 CHECK(execute_callback(code, code_length, max_stack, max_call_frames,
103 capture_value, &output, &actual),
104 "%s: VM setup failed", case_name);
105 CHECK(actual == expected, "%s: expected %s, got %s", case_name,
106 vm_error_string(expected), vm_error_string(actual));
107 CHECK(output.length == 0, "%s: error path unexpectedly printed \"%s\"",
108 case_name, output.text);
109 return 1;
110}
111
112static int assemble_source(const char *source, uint8_t **code,
113 size_t *code_length) {
114 char error[ERROR_TEXT_CAPACITY] = {0};
115
116 *code = NULL;
117 *code_length = 0;
118 if (asm_assemble(source, code, code_length, error, sizeof(error)) != 0) {
119 fprintf(stderr, "assembly unexpectedly failed: %s\n", error);
120 return 0;
121 }
122 return 1;
123}
124
125static int expect_assembled_vm_error(const char *case_name, const char *source,
126 size_t max_stack, size_t max_call_frames,
127 VMError expected) {
128 uint8_t *code = NULL;
129 size_t code_length = 0;
130 int passed;
131
132 CHECK(assemble_source(source, &code, &code_length),
133 "%s: could not assemble test program", case_name);
134 passed = expect_vm_error(case_name, code, code_length, max_stack,
135 max_call_frames, expected);
136 asm_free(code);
137 return passed;
138}
139
140static int run_source_and_expect_output(const char *case_name, const char *source,
141 const char *expected_output) {
142 uint8_t *code = NULL;
143 size_t code_length = 0;
144 OutputCapture output = {{0}, 0};
145 VMError result;
146 int passed = 0;
147
148 if (!assemble_source(source, &code, &code_length)) {
149 fprintf(stderr, "%s: could not assemble source\n", case_name);
150 goto done;
151 }
152 if (!execute_callback(code, code_length, 0, 0, capture_value, &output,
153 &result)) {
154 fprintf(stderr, "%s: VM setup failed\n", case_name);
155 goto done;
156 }
157 if (result != VM_OK) {
158 fprintf(stderr, "%s: execution failed: %s\n", case_name,
159 vm_error_string(result));
160 goto done;
161 }
162 if (strcmp(output.text, expected_output) != 0) {
163 fprintf(stderr, "%s: expected output \"%s\", got \"%s\"\n", case_name,
164 expected_output, output.text);
165 goto done;
166 }
167 passed = 1;
168
169done:
170 asm_free(code);
171 return passed;
172}
173
174static int test_examples(void) {
175 static const struct {
176 const char *path;
177 const char *expected_output;
178 } examples[] = {
179 {"examples/fib.vasm", "6765\n"},
180 {"examples/fact.vasm", "3628800\n"},
181 {"examples/primes.vasm",
182 "2\n3\n5\n7\n11\n13\n17\n19\n23\n29\n31\n37\n41\n43\n47\n"},
183 };
184 size_t index;
185
186 for (index = 0; index < sizeof(examples) / sizeof(examples[0]); ++index) {
187 uint8_t *code = NULL;
188 size_t code_length = 0;
189 char error[ERROR_TEXT_CAPACITY] = {0};
190 OutputCapture output = {{0}, 0};
191 VMError result;
192
193 if (asm_assemble_file(examples[index].path, &code, &code_length, error,
194 sizeof(error)) != 0) {
195 fprintf(stderr, "%s: assembly failed: %s\n", examples[index].path,
196 error);
197 asm_free(code);
198 return 0;
199 }
200 if (!execute_callback(code, code_length, 0, 0, capture_value, &output,
201 &result)) {
202 fprintf(stderr, "%s: VM setup failed\n", examples[index].path);
203 asm_free(code);
204 return 0;
205 }
206 if (result != VM_OK) {
207 fprintf(stderr, "%s: execution failed: %s\n", examples[index].path,
208 vm_error_string(result));
209 asm_free(code);
210 return 0;
211 }
212 if (strcmp(output.text, examples[index].expected_output) != 0) {
213 fprintf(stderr, "%s: output mismatch\nexpected: %sactual: %s",
214 examples[index].path, examples[index].expected_output,
215 output.text);
216 asm_free(code);
217 return 0;
218 }
219 asm_free(code);
220 }
221 return 1;
222}
223
224static int test_all_instruction_families(void) {
225 static const char source[] =
226 "entry: PUSH 9 # arithmetic and comparison\n"
227 "PUSH 3\n"
228 "DIV\n"
229 "PUSH 3\n"
230 "EQ\n"
231 "JZ bad\n"
232 "PUSH 4\n"
233 "NEG\n"
234 "PRINT\n"
235 "PUSH 1\n"
236 "PUSH 2\n"
237 "SWAP\n"
238 "SUB\n"
239 "DUP\n"
240 "PRINT\n"
241 "POP\n"
242 "HALT\n"
243 "bad: PUSH 99\n"
244 "PRINT\n"
245 "HALT\n";
246
247 return run_source_and_expect_output("instruction smoke test", source,
248 "-4\n1\n");
249}
250
251static int test_file_output(void) {
252 uint8_t code[VM_OPCODE_SIZE + VM_OPERAND_SIZE + VM_OPCODE_SIZE +
253 VM_OPCODE_SIZE];
254 size_t length = 0;
255 VM vm = {0};
256 VMError result;
257 FILE *output = NULL;
258 char text[32] = {0};
259 size_t count;
260
261 length = emit_operand_opcode(code, length, VM_OP_PUSH, -7);
262 length = emit_opcode(code, length, VM_OP_PRINT);
263 length = emit_opcode(code, length, VM_OP_HALT);
264 CHECK(length == sizeof(code), "internal bytecode size mismatch");
265
266 output = tmpfile();
267 CHECK(output != NULL, "tmpfile failed");
268 CHECK(vm_init(&vm, 0, 0) == 0, "VM init failed: %s",
269 vm_error_string(vm.error));
270 result = vm_run(&vm, code, length, output);
271 CHECK(result == VM_OK, "file output program failed: %s",
272 vm_error_string(result));
273 CHECK(fflush(output) == 0, "could not flush output");
274 CHECK(fseek(output, 0, SEEK_SET) == 0, "could not rewind output");
275 count = fread(text, 1, sizeof(text) - 1, output);
276 CHECK(ferror(output) == 0, "could not read output");
277 text[count] = '\0';
278 CHECK(strcmp(text, "-7\n") == 0, "expected -7 output, got \"%s\"", text);
279
280 fclose(output);
281 vm_free(&vm);
282 return 1;
283}
284
285static int test_vm_errors(void) {
286 static const uint8_t bad_opcode[] = {UINT8_C(0xff)};
287 static const uint8_t truncated_instruction[] = {VM_OP_PUSH, 0, 0, 0};
288 uint8_t jump_into_operand[VM_OPCODE_SIZE + VM_OPERAND_SIZE +
289 VM_OPCODE_SIZE];
290 uint8_t invalid_local[VM_OPCODE_SIZE + VM_OPERAND_SIZE + VM_OPCODE_SIZE];
291 uint8_t output_failure[VM_OPCODE_SIZE + VM_OPERAND_SIZE + VM_OPCODE_SIZE +
292 VM_OPCODE_SIZE];
293 size_t jump_into_operand_length = 0;
294 size_t invalid_local_length = 0;
295 size_t output_failure_length = 0;
296 VMError actual;
297
298 CHECK(expect_assembled_vm_error("operand stack underflow", "POP\nHALT\n", 0,
299 0, VM_ERR_STACK_UNDERFLOW),
300 "operand underflow test failed");
301 CHECK(expect_assembled_vm_error("operand stack overflow",
302 "PUSH 1\nPUSH 2\nHALT\n", 1, 0,
303 VM_ERR_STACK_OVERFLOW),
304 "operand overflow test failed");
305 CHECK(expect_assembled_vm_error("division by zero",
306 "PUSH 7\nPUSH 0\nDIV\nHALT\n", 0, 0,
307 VM_ERR_DIVISION_BY_ZERO),
308 "division by zero test failed");
309 CHECK(expect_assembled_vm_error("modulo by zero",
310 "PUSH 7\nPUSH 0\nMOD\nHALT\n", 0, 0,
311 VM_ERR_DIVISION_BY_ZERO),
312 "modulo by zero test failed");
313 CHECK(expect_vm_error("bad opcode", bad_opcode, sizeof(bad_opcode), 0, 0,
314 VM_ERR_BAD_OPCODE),
315 "bad opcode test failed");
316 CHECK(expect_vm_error("truncated operand", truncated_instruction,
317 sizeof(truncated_instruction), 0, 0,
318 VM_ERR_TRUNCATED_INSTRUCTION),
319 "truncated instruction test failed");
320 CHECK(expect_assembled_vm_error("out-of-range jump", "JMP 1000000\n", 0,
321 0, VM_ERR_INVALID_JUMP),
322 "invalid jump test failed");
323
324 jump_into_operand_length = emit_operand_opcode(
325 jump_into_operand, jump_into_operand_length, VM_OP_JMP, 1);
326 jump_into_operand_length = emit_opcode(jump_into_operand,
327 jump_into_operand_length,
328 VM_OP_HALT);
329 CHECK(expect_vm_error("jump into operand", jump_into_operand,
330 jump_into_operand_length, 0, 0,
331 VM_ERR_INVALID_JUMP),
332 "instruction-boundary jump test failed");
333
334 invalid_local_length = emit_operand_opcode(invalid_local, invalid_local_length,
335 VM_OP_LOAD,
336 VM_LOCALS_PER_FRAME);
337 invalid_local_length = emit_opcode(invalid_local, invalid_local_length,
338 VM_OP_HALT);
339 CHECK(expect_vm_error("invalid local", invalid_local, invalid_local_length, 0,
340 0, VM_ERR_INVALID_LOCAL),
341 "invalid local test failed");
342 CHECK(expect_assembled_vm_error("call stack overflow", "again: CALL again\n",
343 0, 1, VM_ERR_CALL_STACK_OVERFLOW),
344 "call stack overflow test failed");
345 CHECK(expect_assembled_vm_error("return with no caller", "RET\n", 0, 0,
346 VM_ERR_CALL_STACK_UNDERFLOW),
347 "call stack underflow test failed");
348 CHECK(expect_assembled_vm_error(
349 "arithmetic overflow",
350 "PUSH 2147483647\nPUSH 2147483647\nMUL\nPUSH 3\nMUL\nHALT\n",
351 0, 0, VM_ERR_ARITHMETIC_OVERFLOW),
352 "arithmetic overflow test failed");
353 CHECK(expect_assembled_vm_error("missing HALT", "PUSH 1\n", 0, 0,
354 VM_ERR_NO_HALT),
355 "missing HALT test failed");
356
357 output_failure_length = emit_operand_opcode(output_failure,
358 output_failure_length, VM_OP_PUSH,
359 1);
360 output_failure_length = emit_opcode(output_failure, output_failure_length,
361 VM_OP_PRINT);
362 output_failure_length = emit_opcode(output_failure, output_failure_length,
363 VM_OP_HALT);
364 CHECK(execute_callback(output_failure, output_failure_length, 0, 0,
365 reject_output, NULL, &actual),
366 "output failure: VM setup failed");
367 CHECK(actual == VM_ERR_OUTPUT, "output callback failure: expected %s, got %s",
368 vm_error_string(VM_ERR_OUTPUT), vm_error_string(actual));
369 return 1;
370}
371
372static int test_vm_argument_and_allocation_errors(void) {
373 static const uint8_t halt[] = {VM_OP_HALT};
374 VM vm = {0};
375 VMError result;
376
377 CHECK(vm_init(NULL, 0, 0) == -1,
378 "vm_init should reject a NULL VM pointer");
379 CHECK(vm_init(&vm, SIZE_MAX, 1) == -1,
380 "vm_init should reject an unrepresentable stack allocation");
381 CHECK(vm.error == VM_ERR_ALLOCATION,
382 "expected allocation error, got %s", vm_error_string(vm.error));
383 vm_free(&vm);
384
385 CHECK(vm_init(&vm, 0, 0) == 0, "VM init failed: %s",
386 vm_error_string(vm.error));
387 result = vm_run_callback(&vm, NULL, sizeof(halt), NULL, NULL);
388 CHECK(result == VM_ERR_INVALID_ARGUMENT,
389 "NULL nonempty bytecode: expected %s, got %s",
390 vm_error_string(VM_ERR_INVALID_ARGUMENT), vm_error_string(result));
391 vm_free(&vm);
392
393 result = vm_run_callback(NULL, halt, sizeof(halt), NULL, NULL);
394 CHECK(result == VM_ERR_INVALID_ARGUMENT,
395 "NULL VM: expected %s, got %s",
396 vm_error_string(VM_ERR_INVALID_ARGUMENT), vm_error_string(result));
397 return 1;
398}
399
400static int test_assembler_rejections(void) {