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