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 invalid_local[VM_OPCODE_SIZE + VM_OPERAND_SIZE + VM_OPCODE_SIZE];
289 uint8_t output_failure[VM_OPCODE_SIZE + VM_OPERAND_SIZE + VM_OPCODE_SIZE +
290 VM_OPCODE_SIZE];
291 size_t invalid_local_length = 0;
292 size_t output_failure_length = 0;
293 VMError actual;
294
295 CHECK(expect_assembled_vm_error("operand stack underflow", "POP\nHALT\n", 0,
296 0, VM_ERR_STACK_UNDERFLOW),
297 "operand underflow test failed");
298 CHECK(expect_assembled_vm_error("operand stack overflow",
299 "PUSH 1\nPUSH 2\nHALT\n", 1, 0,
300 VM_ERR_STACK_OVERFLOW),
301 "operand overflow test failed");
302 CHECK(expect_assembled_vm_error("division by zero",
303 "PUSH 7\nPUSH 0\nDIV\nHALT\n", 0, 0,
304 VM_ERR_DIVISION_BY_ZERO),
305 "division by zero test failed");
306 CHECK(expect_assembled_vm_error("modulo by zero",
307 "PUSH 7\nPUSH 0\nMOD\nHALT\n", 0, 0,
308 VM_ERR_DIVISION_BY_ZERO),
309 "modulo by zero test failed");
310 CHECK(expect_vm_error("bad opcode", bad_opcode, sizeof(bad_opcode), 0, 0,
311 VM_ERR_BAD_OPCODE),
312 "bad opcode test failed");
313 CHECK(expect_vm_error("truncated operand", truncated_instruction,
314 sizeof(truncated_instruction), 0, 0,
315 VM_ERR_TRUNCATED_INSTRUCTION),
316 "truncated instruction test failed");
317 CHECK(expect_assembled_vm_error("out-of-range jump", "JMP 1000000\n", 0,
318 0, VM_ERR_INVALID_JUMP),
319 "invalid jump test failed");
320
321 invalid_local_length = emit_operand_opcode(invalid_local, invalid_local_length,
322 VM_OP_LOAD,
323 VM_LOCALS_PER_FRAME);
324 invalid_local_length = emit_opcode(invalid_local, invalid_local_length,
325 VM_OP_HALT);
326 CHECK(expect_vm_error("invalid local", invalid_local, invalid_local_length, 0,
327 0, VM_ERR_INVALID_LOCAL),
328 "invalid local test failed");
329 CHECK(expect_assembled_vm_error("call stack overflow", "again: CALL again\n",
330 0, 1, VM_ERR_CALL_STACK_OVERFLOW),
331 "call stack overflow test failed");
332 CHECK(expect_assembled_vm_error("return with no caller", "RET\n", 0, 0,
333 VM_ERR_CALL_STACK_UNDERFLOW),
334 "call stack underflow test failed");
335 CHECK(expect_assembled_vm_error(
336 "arithmetic overflow",
337 "PUSH 2147483647\nPUSH 2147483647\nMUL\nPUSH 3\nMUL\nHALT\n",
338 0, 0, VM_ERR_ARITHMETIC_OVERFLOW),
339 "arithmetic overflow test failed");
340 CHECK(expect_assembled_vm_error("missing HALT", "PUSH 1\n", 0, 0,
341 VM_ERR_NO_HALT),
342 "missing HALT test failed");
343
344 output_failure_length = emit_operand_opcode(output_failure,
345 output_failure_length, VM_OP_PUSH,
346 1);
347 output_failure_length = emit_opcode(output_failure, output_failure_length,
348 VM_OP_PRINT);
349 output_failure_length = emit_opcode(output_failure, output_failure_length,
350 VM_OP_HALT);
351 CHECK(execute_callback(output_failure, output_failure_length, 0, 0,
352 reject_output, NULL, &actual),
353 "output failure: VM setup failed");
354 CHECK(actual == VM_ERR_OUTPUT, "output callback failure: expected %s, got %s",
355 vm_error_string(VM_ERR_OUTPUT), vm_error_string(actual));
356 return 1;
357}
358
359static int test_assembler_rejections(void) {
360 static const char *const invalid_sources[] = {
361 "WOBBLE\n",
362 "start: HALT\nstart: HALT\n",
363 "JMP missing_label\n",
364 "PUSH no_number\n",
365 "LOAD 16\n",
366 "label: PUSH label\n",
367 };
368 size_t index;
369
370 for (index = 0; index < sizeof(invalid_sources) / sizeof(invalid_sources[0]);
371 ++index) {
372 uint8_t sentinel = 0;
373 uint8_t *code = &sentinel;
374 size_t code_length = 123;
375 char error[ERROR_TEXT_CAPACITY] = {0};
376 int result = asm_assemble(invalid_sources[index], &code, &code_length,
377 error, sizeof(error));
378
379 if (result != -1) {
380 fprintf(stderr, "invalid assembly %zu unexpectedly succeeded\n", index);
381 asm_free(code);
382 return 0;
383 }
384 if (code != NULL || code_length != 0 || error[0] == '\0') {
385 fprintf(stderr,
386 "invalid assembly %zu violated failure output contract\n",
387 index);
388 return 0;
389 }
390 }
391 return 1;
392}
393
394static int test_assembler_labels_comments_and_case(void) {
395 static const char source[] =
396 "jump_to_value: jMp value // skip this value\n"
397 "PUSH 99\n"
398 "value: PuSh 42 ; mnemonic case is irrelevant\n"
399 "PrInT\n"
400 "HaLt\n";
401
402 return run_source_and_expect_output("assembler labels/comments/case", source,
403 "42\n");
404}
405
406int main(void) {
407 static const struct {
408 const char *name;
409 int (*function)(void);
410 } tests[] = {
411 {"examples", test_examples},
412 {"instruction families", test_all_instruction_families},
413 {"FILE output", test_file_output},
414 {"VM errors", test_vm_errors},
415 {"assembler rejections", test_assembler_rejections},
416 {"assembler labels, comments, and case", test_assembler_labels_comments_and_case},
417 };
418 size_t index;
419 size_t passed = 0;
420
421 for (index = 0; index < sizeof(tests) / sizeof(tests[0]); ++index) {
422 if (tests[index].function()) {
423 ++passed;
424 fprintf(stderr, "ok - %s\n", tests[index].name);
425 } else {
426 fprintf(stderr, "not ok - %s\n", tests[index].name);
427 }
428 }
429
430 if (passed != sizeof(tests) / sizeof(tests[0])) {
431 fprintf(stderr, "%zu/%zu tests passed\n", passed,
432 sizeof(tests) / sizeof(tests[0]));
433 return EXIT_FAILURE;
434 }
435
436 printf("%zu tests passed\n", passed);
437 return EXIT_SUCCESS;
438}
439
Discussion
No comments yet. Start the discussion. Recorded by @patrick-toulme.