From 08505fdbb2bca90d26523673fcad50944b10aba0 Mon Sep 17 00:00:00 2001 From: Bennett Anderson Date: Sun, 23 Sep 2018 01:36:25 +1000 Subject: [PATCH] Remove embedding infrastructure --- CMakeLists.txt | 56 +++++--- README.md | 6 +- src/{vm => }/bytecode.h | 13 +- src/include/hydrogen.h | 91 ------------- src/{vm => }/lexer.c | 14 +- src/{vm => }/lexer.h | 4 +- src/{cli => }/main.c | 45 +++---- src/{vm => }/parser.c | 50 +++++-- src/{vm => }/parser.h | 2 +- src/{vm => }/util.c | 0 src/{vm => }/util.h | 0 src/{vm => }/value.h | 52 ++++--- src/{vm => }/vm.c | 246 ++++++++++++++++++++++++---------- src/{vm => }/vm.h | 115 +++++++++++++--- src/vm/err.c | 110 --------------- src/vm/err.h | 42 ------ tests/runtime/test_runtime.py | 237 ++++++++++++++++++++++++++++++++ tests/test_lexer.cpp | 12 +- tests/test_parser.cpp | 64 ++++----- 19 files changed, 701 insertions(+), 458 deletions(-) rename src/{vm => }/bytecode.h (95%) delete mode 100644 src/include/hydrogen.h rename src/{vm => }/lexer.c (96%) rename src/{vm => }/lexer.h (97%) rename src/{cli => }/main.c (76%) rename src/{vm => }/parser.c (97%) rename src/{vm => }/parser.h (91%) rename src/{vm => }/util.c (100%) rename src/{vm => }/util.h (100%) rename src/{vm => }/value.h (51%) rename src/{vm => }/vm.c (67%) rename src/{vm => }/vm.h (52%) delete mode 100644 src/vm/err.c delete mode 100644 src/vm/err.h create mode 100755 tests/runtime/test_runtime.py diff --git a/CMakeLists.txt b/CMakeLists.txt index df34500..43fa119 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,37 +7,40 @@ cmake_minimum_required(VERSION 3.6) project(hydrogen) set(CMAKE_C_STANDARD 99) -# Set the C compiler's debug and release flags +# Turn on all warnings for all C code set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") + +# Turn off optimisations and add debugging symbols when in debug mode set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -O0 -g") + +# Turn on all optimisations when in release mode set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3") -set(CMAKE_CXX_FLAGS "-Wno-c++11-compat-deprecated-writable-strings") -# Source files for the core library -set(VM_SOURCES - src/include/hydrogen.h - src/vm/vm.c src/vm/vm.h - src/vm/parser.c src/vm/parser.h - src/vm/lexer.c src/vm/lexer.h - src/vm/bytecode.h src/vm/value.h - src/vm/err.c src/vm/err.h - src/vm/util.c src/vm/util.h) +# Remove the annoying warning about type casting a `char *` string to a `const +# char *` in the C++ tests +set(CMAKE_CXX_FLAGS "-Wno-c++11-compat-deprecated-writable-strings") +# We split the interpreter up into a core library (hyvm) and a separate +# executable linked against this library (hydrogen) so that we can link the +# C++ tests against the core library, rather than having to re-compile all the +# source files again. +# # Create the core VM library -add_library(hyvm STATIC ${VM_SOURCES}) -target_include_directories(hyvm PUBLIC src/include) +add_library(hyvm STATIC + src/vm.c src/vm.h + src/parser.c src/parser.h + src/lexer.c src/lexer.h + src/bytecode.h src/value.h + src/util.c src/util.h) # Create the CLI executable -set(CLI_SOURCES - src/include/hydrogen.h - src/cli/main.c) -add_executable(hydrogen ${CLI_SOURCES}) +add_executable(hydrogen src/main.c) target_link_libraries(hydrogen hyvm) -# Configure Google Test framework +# The tests use the C++ Google Test framework add_subdirectory(tests/gtest) enable_testing() -include_directories(${gtest_SOURCE_DIR}/include src/include src) +include_directories(${gtest_SOURCE_DIR}/include src) # Macro for adding a test suite macro(test name) @@ -49,3 +52,18 @@ endmacro() # Test suites test(lexer) test(parser) + +# The runtime tests are run using a Python script, because that makes it easier +# to parse the expected output of the test straight from the .hy test file. +find_package(Python REQUIRED COMPONENTS Interpreter) + +# Add runtime tests +# +# To run just the runtime tests, execute the following command from inside the +# build folder: +# $ python ../tests/runtime/test_runtime.py ../tests/runtime ./hydrogen +add_test(NAME test_runtime + COMMAND ${Python_EXECUTABLE} + ${CMAKE_SOURCE_DIR}/tests/runtime/test_runtime.py + ${CMAKE_SOURCE_DIR}/tests/runtime + ${CMAKE_BINARY_DIR}/hydrogen) diff --git a/README.md b/README.md index e157d03..c727194 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ The Hydrogen Programming Language --------------------------------- -Hydrogen is a **toy programming language** I'm writing mostly for my own edification. It's an **interpreted**, **dynamically typed** language written in C and hand-coded assembly. It comes with an **easy-to-use C API** for embedding within your own programs! +Hydrogen is a **toy programming language** I'm writing mostly for my own edification. It's an **interpreted**, **dynamically typed** language written in C. It's intended to be used as a standalone language, and not embedded within other programs. -The core is written in **C** and **hand-coded assembly**, with no dependencies beyond the C standard library. The tests are written in C++ using the [Google Test](https://github.com/google/googletest) framework. +The core is written in **C** with no dependencies beyond the C standard library. Most tests are written in C++ using the [Google Test](https://github.com/google/googletest) framework. The runtime tests are written using a custom Python script. -### Code Sample +### Example Here's some Hydrogen code: diff --git a/src/vm/bytecode.h b/src/bytecode.h similarity index 95% rename from src/vm/bytecode.h rename to src/bytecode.h index 7d95c87..da70824 100644 --- a/src/vm/bytecode.h +++ b/src/bytecode.h @@ -28,14 +28,21 @@ #include -// All bytecode opcodes. We can have up to 256 opcodes, since they must be -// storable in a single byte. +// All bytecode opcodes. We can have up to 256 opcodes, since they need to fit +// into a single byte. +// +// Meaning of the various suffixes: +// * N = number +// * P = primitive (false, true, nil) +// * F = function +// * NF = native function typedef enum { // Stores OP_MOV, OP_SET_N, OP_SET_P, OP_SET_F, + OP_SET_NF, // Arithmetic operators OP_ADD_LL, @@ -75,7 +82,7 @@ typedef enum { // String representation of each opcode. static char * OPCODE_NAMES[] = { // Stores - "OP_MOV", "OP_SET_N", "OP_SET_P", "OP_SET_F", + "OP_MOV", "OP_SET_N", "OP_SET_P", "OP_SET_F", "OP_SET_NF", // Arithmetic operators "OP_ADD_LL", "OP_ADD_LN", "OP_SUB_LL", "OP_SUB_LN", "OP_SUB_NL", diff --git a/src/include/hydrogen.h b/src/include/hydrogen.h deleted file mode 100644 index 7a6f5f6..0000000 --- a/src/include/hydrogen.h +++ /dev/null @@ -1,91 +0,0 @@ - -// -// Hydrogen Programming Language -// By Ben Anderson -// July 2018 -// - -#ifndef HYDROGEN_H -#define HYDROGEN_H - -#include - -// The version of this Hydrogen distribution, expressed using semantic -// versioning. -#define HY_VERSION_MAJOR 0 -#define HY_VERSION_MINOR 1 -#define HY_VERSION_PATCH 0 - -// Human-readable version string. -#define HY_VERSION_STRING "0.1.0" - -// Hydrogen has no global state; everything that's needed is stored in this -// struct. You can create multiple VMs and they'll all function independently. -typedef struct HyVM HyVM; - -// To execute some code, it needs to live inside a package. Packages are also -// the only way to use Hydrogen's FFI. -typedef int HyPkg; - -// Tells you a bunch of information when something goes wrong. Used for all -// types of errors, including parsing and runtime errors. -typedef struct HyErr HyErr; - -// Creates a new virtual machine instance. -HyVM * hy_new_vm(); - -// Frees all the resources allocated by a virtual machine. -void hy_free_vm(HyVM *vm); - -// Creates a new package on a virtual machine. -HyPkg hy_new_pkg(HyVM *vm, char *name); - -// Executes some code. The code is run within the package's "main" function, -// and can access any variables, functions, imports, etc. that were created by -// a previous piece of code run on this package. This functionality is used to -// create the REPL. -// -// For example: -// HyVM *vm = hy_new_vm(); -// HyPkg pkg = hy_new_pkg(vm, "test"); -// hy_run(vm, pkg, "let a = 3"); -// hy_run(vm, pkg, "a = 4"); // We're still able to access the variable `a` -// -// Since no file path is specified, any imports are relative to the current -// working directory. -// -// If an error occurs, then the return value will be non-NULL and the error must -// be freed using `hy_free_err`. -HyErr * hy_run_string(HyVM *vm, HyPkg pkg, char *code); - -// Executes a file. A new package is created for the file and is named based off -// the name of the file. The package can be later imported by other pieces of -// code. -// -// Both the directory containing the file and the current working directory are -// searched when the file attempts to import any other packages. -// -// If an error occurs, then the return value is non-NULL and the error must be -// freed. -HyErr * hy_run_file(HyVM *vm, char *path); - - -// Returns a description of the error that's occurred. -char * hy_err_desc(HyErr *err); - -// Returns the path to the file that an error occurred in, or NULL if the error -// has no associated file. The returned string should NOT be freed. -char * hy_err_file(HyErr *err); - -// Returns the line number that the error occurred on, or -1 if the error has -// no associated line number. -int hy_err_line(HyErr *err); - -// Pretty prints the error to the standard output. If `use_color` is true, then -// terminal color codes will be printed alongside the error information. -void hy_err_print(HyErr *err, bool use_color); - -// Frees resources allocated by an error. -void hy_free_err(HyErr *err); - -#endif diff --git a/src/vm/lexer.c b/src/lexer.c similarity index 96% rename from src/vm/lexer.c rename to src/lexer.c index cc0c85f..223a5e4 100644 --- a/src/vm/lexer.c +++ b/src/lexer.c @@ -30,7 +30,7 @@ static inline int is_ident_continue(char ch) { } // Creates a new lexer over the given source code. -Lexer lex_new(HyVM *vm, char *path, char *code) { +Lexer lex_new(VM *vm, char *path, char *code) { Lexer lxr; lxr.vm = vm; lxr.path = path; @@ -113,9 +113,9 @@ static void lex_int(Lexer *lxr, int base) { // Check for a parse error if (errno != 0) { - HyErr *err = err_new("failed to parse number"); + Err *err = err_new("failed to parse number"); err->line = lxr->tk.line; - err_set_file(err, lxr->path); + err_file(err, lxr->path); err_trigger(lxr->vm, err); return; } @@ -139,9 +139,9 @@ static void lex_float(Lexer *lxr) { // Check for a parse error if (errno != 0) { - HyErr *err = err_new("failed to parse number"); + Err *err = err_new("failed to parse number"); err->line = lxr->tk.line; - err_set_file(err, lxr->path); + err_file(err, lxr->path); err_trigger(lxr->vm, err); return; } @@ -274,9 +274,9 @@ char * tk_to_string(Tk *tk) { // Triggers an error if the current token isn't what's expected. void lex_expect(Lexer *lxr, Tk expected) { if (lxr->tk.type != expected) { - HyErr *err = err_new("expected %s, found %s", tk_to_string(&expected), + Err *err = err_new("expected %s, found %s", tk_to_string(&expected), tk_to_string(&lxr->tk.type)); - err_set_file(err, lxr->path); + err_file(err, lxr->path); err->line = lxr->tk.line; err_trigger(lxr->vm, err); } diff --git a/src/vm/lexer.h b/src/lexer.h similarity index 97% rename from src/vm/lexer.h rename to src/lexer.h index 4ed81cb..5016081 100644 --- a/src/vm/lexer.h +++ b/src/lexer.h @@ -52,7 +52,7 @@ typedef struct { // Stores state information required by the lexer. typedef struct { - HyVM *vm; + VM *vm; char *path; char *code; @@ -68,7 +68,7 @@ typedef struct { // Creates a new lexer over the given source code. If the code is from a file, // the path to the file is also given (this can be NULL). -Lexer lex_new(HyVM *vm, char *path, char *code); +Lexer lex_new(VM *vm, char *path, char *code); // Lexes the next token, storing the result in `lxr->tk`. void lex_next(Lexer *lxr); diff --git a/src/cli/main.c b/src/main.c similarity index 76% rename from src/cli/main.c rename to src/main.c index 67e06a4..746e901 100644 --- a/src/cli/main.c +++ b/src/main.c @@ -5,12 +5,16 @@ // July 2018 // -#include - #include +#include #include -// Only if colors are supported +#include "vm.h" + +// Human-readable version string. +#define HY_VERSION_STRING "0.1.0" + +// Only include if colors are supported #if !defined(_WIN32) && !defined(_WIN64) #include #endif @@ -61,35 +65,22 @@ int run_repl() { return 0; } -// Run a source code string. -int run_string(char *code) { - HyVM *vm = hy_new_vm(); - HyPkg pkg = hy_new_pkg(vm, NULL); - HyErr *err = hy_run_string(vm, pkg, code); - - // Check for error - int return_code = 0; - if (err != NULL) { - hy_err_print(err, supports_color()); - return_code = 1; - } - hy_free_vm(vm); - return return_code; -} - // Run a file. int run_file(char *path) { - HyVM *vm = hy_new_vm(); - HyErr *err = hy_run_file(vm, path); + // Create a new VM and run the file + VM vm = vm_new(); + Err *err = vm_run_file(&vm, path); - // Check for error - int return_code = 0; + // Check for an error if (err != NULL) { - hy_err_print(err, supports_color()); - return_code = 1; + err_print(err, supports_color()); + err_free(err); + vm_free(&vm); + return EXIT_FAILURE; + } else { + vm_free(&vm); + return EXIT_SUCCESS; } - hy_free_vm(vm); - return return_code; } int main(int argc, char *argv[]) { diff --git a/src/vm/parser.c b/src/parser.c similarity index 97% rename from src/vm/parser.c rename to src/parser.c index f274629..47061b9 100644 --- a/src/vm/parser.c +++ b/src/parser.c @@ -91,7 +91,7 @@ typedef struct fn_scope { // Converts a stream of tokens from the lexer into bytecode. typedef struct { - HyVM *vm; + VM *vm; // Supplies the stream of tokens we're parsing. Lexer lxr; @@ -110,7 +110,7 @@ typedef struct { } Parser; // Create a new parser. -static Parser psr_new(HyVM *vm, int pkg, char *path, char *code) { +static Parser psr_new(VM *vm, int pkg, char *path, char *code) { Parser psr; psr.vm = vm; psr.lxr = lex_new(vm, path, code); @@ -141,10 +141,10 @@ static inline Function * psr_fn(Parser *psr) { static void psr_trigger_err(Parser *psr, char *fmt, ...) { va_list args; va_start(args, fmt); - HyErr *err = err_vnew(fmt, args); + Err *err = err_vnew(fmt, args); va_end(args); - err_set_file(err, psr->lxr.path); + err_file(err, psr->lxr.path); err->line = psr->lxr.tk.line; err_trigger(psr->vm, err); } @@ -558,7 +558,7 @@ static void expr_discharge(Parser *psr, Node *node) { UNREACHABLE(); } node->type = NODE_CONST; - node->const_idx = (uint16_t) vm_add_const_num(psr->vm, node->num); + node->const_idx = (uint16_t) vm_add_num(psr->vm, node->num); break; case NODE_LOCAL: @@ -825,7 +825,7 @@ static bool expr_fold_rel(Tk binop, Node *left, Node right) { // Set the result to be a primitive left->type = NODE_PRIM; - left->prim = (Primitive) result; + left->prim = (Primitive) (result + PRIM_FALSE); return true; } else if (left->type == NODE_PRIM) { // Only for TK_EQ and TK_NEQ // Compare the two primitives @@ -838,7 +838,7 @@ static bool expr_fold_rel(Tk binop, Node *left, Node right) { // Set the result to be a primitive left->type = NODE_PRIM; - left->prim = (Primitive) result; + left->prim = (Primitive) (result + PRIM_FALSE); return true; } @@ -1135,6 +1135,34 @@ static Node expr_operand_num(Parser *psr) { return operand; } +// Parse a native function operand. +static Node expr_operand_native_fn(Parser *psr, uint64_t name) { + // Search the native functions list + int fn_idx = -1; + for (int i = 0; i < psr->vm->natives_count; i++) { + NativeFn *native = &psr->vm->natives[i]; + if (native->pkg == psr->pkg && native->name == name) { + fn_idx = i; + break; + } + } + + if (fn_idx == -1) { + // Try finding a native function within this package of the same name + psr_trigger_err(psr, "variable not defined"); + UNREACHABLE(); + } + + // Emit an instruction to store the function + Instruction ins = ins_new2(OP_SET_NF, 0, (uint16_t) fn_idx); + int set_idx = fn_emit(psr_fn(psr), ins); + + Node result; + result.type = NODE_RELOC; + result.reloc_idx = set_idx; + return result; +} + // Parse a local operand. static Node expr_operand_local(Parser *psr) { // Check the local exists @@ -1147,10 +1175,10 @@ static Node expr_operand_local(Parser *psr) { } } - // Variable doesn't exist + // Variable doesn't exist, try searching native functions in this package + // instead if (slot == -1) { - psr_trigger_err(psr, "variable not defined"); - UNREACHABLE(); + return expr_operand_native_fn(psr, name); } Node result; @@ -1622,7 +1650,7 @@ static void parse_code(Parser *psr) { // Parses the source code into bytecode. All bytecode for top level code gets // appended to the package's main function. All other functions defined in the // code get created on the VM and associated with the given package. -HyErr * parse(HyVM *vm, int pkg, char *path, char *code) { +Err * parse(VM *vm, int pkg, char *path, char *code) { Parser psr = psr_new(vm, pkg, path, code); // Set up an error guard diff --git a/src/vm/parser.h b/src/parser.h similarity index 91% rename from src/vm/parser.h rename to src/parser.h index 2f41a81..22604b5 100644 --- a/src/vm/parser.h +++ b/src/parser.h @@ -18,6 +18,6 @@ // Parses the source code into bytecode. All bytecode for top level code gets // appended to the package's main function. All other functions defined in the // code get created on the VM and associated with the given package. -HyErr * parse(HyVM *vm, int pkg, char *path, char *code); +Err * parse(VM *vm, int pkg, char *path, char *code); #endif diff --git a/src/vm/util.c b/src/util.c similarity index 100% rename from src/vm/util.c rename to src/util.c diff --git a/src/vm/util.h b/src/util.h similarity index 100% rename from src/vm/util.h rename to src/util.h diff --git a/src/vm/value.h b/src/value.h similarity index 51% rename from src/vm/value.h rename to src/value.h index ceef1f2..51eeae8 100644 --- a/src/vm/value.h +++ b/src/value.h @@ -6,51 +6,67 @@ #ifndef VALUE_H #define VALUE_H +// A value stored on the stack. +typedef uint64_t Value; + // Bits that, when set, indicate a quiet NaN value. #define QUIET_NAN ((uint64_t) 0x7ffc000000000000) // The sign bit. Only set if the value is a pointer. #define SIGN ((uint64_t) 1 << 63) -// Flag bits used to indicate a value is of various types. These bits are set -// immediately after the lowest 2 bytes, where additional information (e.g. -// the primitive type or function index) is stored. -#define FLAG_PRIM (QUIET_NAN | 0x10000) -#define FLAG_FN (QUIET_NAN | 0x20000) - // Various flag bits to indicate different primitives. +// +// We use 0b0010 and 0b0011 for boolean values so that we can check for true +// or false by xoring out TAG_BOOL. typedef enum { - PRIM_FALSE = 0x0, - PRIM_TRUE = 0x1, - PRIM_NIL = 0x2, + PRIM_NIL = 0x0, // 0b0000 + PRIM_FALSE = 0x2, // 0b0010 + PRIM_TRUE = 0x3, // 0b0011 } Primitive; -// Evaluates to true if a value is a number. -static inline int val_is_num(uint64_t val) { - // Is a number if the quiet NaN bits are not set - return (val & QUIET_NAN) != QUIET_NAN; -} +// Flag bits used to indicate a value is of various types. These bits are set +// immediately after the lowest 2 bytes, where additional information (e.g. +// the primitive type or function index) is stored. +#define TAG_PTR (QUIET_NAN | SIGN) +#define TAG_PRIM (QUIET_NAN | 0x10000) +#define TAG_BOOL (QUIET_NAN | 0x00002) +#define TAG_FN (QUIET_NAN | 0x20000) +#define TAG_NATIVE (QUIET_NAN | 0x30000) + +#define VAL_NIL (TAG_PRIM | PRIM_NIL) // Converts a value into a floating point number. -static inline double v2n(uint64_t val) { +static inline double v2n(Value val) { // Use a union to perform to bitwise conversion union { double num; - uint64_t val; + Value val; } convert; convert.val = val; return convert.num; } // Converts a floating point number into a value. -static inline uint64_t n2v(double num) { +static inline Value n2v(double num) { // Use a union to perform to bitwise conversion union { double num; - uint64_t val; + Value val; } convert; convert.num = num; return convert.val; } +// Converts a value to a pointer. +static inline void * v2ptr(Value val) { + // Get the first 48 bits storing the pointer value + return (void *) (val & 0xffffffffffffffff); +} + +// Converts a pointer to a value. +static inline Value ptr2v(void *ptr) { + return ((Value) ptr) | TAG_PTR; +} + #endif diff --git a/src/vm/vm.c b/src/vm.c similarity index 67% rename from src/vm/vm.c rename to src/vm.c index b4ed0de..b6b48f9 100644 --- a/src/vm/vm.c +++ b/src/vm.c @@ -11,68 +11,46 @@ #include #include +#include // Creates a new virtual machine instance. -HyVM * hy_new_vm() { - HyVM *vm = malloc(sizeof(HyVM)); - vm->pkgs_capacity = 4; - vm->pkgs_count = 0; - vm->pkgs = malloc(sizeof(Package) * vm->pkgs_capacity); - - vm->fns_capacity = 16; - vm->fns_count = 0; - vm->fns = malloc(sizeof(Function) * vm->fns_capacity); - - vm->consts_capacity = 16; - vm->consts_count = 0; - vm->consts = malloc(sizeof(uint64_t) * vm->consts_capacity); - - vm->stack_size = 1024; - vm->stack = malloc(sizeof(uint64_t) * vm->stack_size); +VM vm_new() { + VM vm; + vm.pkgs_capacity = 4; + vm.pkgs_count = 0; + vm.pkgs = malloc(sizeof(Package) * vm.pkgs_capacity); + + vm.fns_capacity = 16; + vm.fns_count = 0; + vm.fns = malloc(sizeof(Function) * vm.fns_capacity); + + vm.natives_capacity = 16; + vm.natives_count = 0; + vm.natives = malloc(sizeof(NativeFn) * vm.natives_capacity); + + vm.consts_capacity = 16; + vm.consts_count = 0; + vm.consts = malloc(sizeof(Value) * vm.consts_capacity); + + vm.stack_size = 1024; + vm.stack = malloc(sizeof(Value) * vm.stack_size); return vm; } // Frees all the resources allocated by a virtual machine. -void hy_free_vm(HyVM *vm) { +void vm_free(VM *vm) { for (int i = 0; i < vm->fns_count; i++) { free(vm->fns[i].ins); } free(vm->pkgs); free(vm->fns); + free(vm->natives); free(vm->consts); free(vm->stack); - free(vm); -} - -// Adds a constant number to the VM's constants list, returning its index. -int vm_add_const_num(HyVM *vm, double num) { - // Convert the number bitwise into a uint64_t - union { - double num; - uint64_t val; - } conversion; - conversion.num = num; - - // Check if the constant already exists - for (int i = 0; i < vm->consts_count; i++) { - if (vm->consts[i] == conversion.val) { - return i; - } - } - - if (vm->consts_count >= vm->consts_capacity) { - vm->consts_capacity *= 2; - vm->consts = realloc(vm->consts, sizeof(uint64_t) * vm->consts_capacity); - } - - // Add the converted number to the constants list if it doesn't already - // exist - vm->consts[vm->consts_count++] = conversion.val; - return vm->consts_count - 1; } // Creates a new package on the VM and returns its index. -int vm_new_pkg(HyVM *vm, uint64_t name) { +int vm_new_pkg(VM *vm, uint64_t name) { if (vm->pkgs_count >= vm->pkgs_capacity) { vm->pkgs_capacity *= 2; vm->pkgs = realloc(vm->pkgs, sizeof(Package) * vm->pkgs_capacity); @@ -84,13 +62,8 @@ int vm_new_pkg(HyVM *vm, uint64_t name) { return vm->pkgs_count - 1; } -// Creates a new package on a virtual machine. -HyPkg hy_new_pkg(HyVM *vm, char *name) { - return vm_new_pkg(vm, hash_string(name, strlen(name))); -} - // Creates a new function on the VM and returns its index. -int vm_new_fn(HyVM *vm, int pkg_idx) { +int vm_new_fn(VM *vm, int pkg_idx) { if (vm->fns_count >= vm->fns_capacity) { vm->fns_capacity *= 2; vm->fns = realloc(vm->fns, sizeof(Function) * vm->fns_capacity); @@ -104,6 +77,48 @@ int vm_new_fn(HyVM *vm, int pkg_idx) { return vm->fns_count - 1; } +// Creates a new native function on the VM and returns its index. +int vm_new_native(VM *vm, int pkg, uint64_t name, int args, void *fn) { + if (vm->natives_count >= vm->natives_capacity) { + vm->natives_capacity *= 2; + vm->natives = realloc(vm->natives, sizeof(NativeFn) * vm->natives_capacity); + } + + NativeFn *native = &vm->natives[vm->natives_count++]; + native->name = name; + native->pkg = pkg; + native->args_count = args; + native->fn = fn; + return vm->natives_count - 1; +} + +// Adds a constant number to the VM's constants list, returning its index. +int vm_add_num(VM *vm, double num) { + // Convert the number bitwise into a uint64_t + union { + double num; + Value val; + } conversion; + conversion.num = num; + + // Check if the constant already exists + for (int i = 0; i < vm->consts_count; i++) { + if (vm->consts[i] == conversion.val) { + return i; + } + } + + if (vm->consts_count >= vm->consts_capacity) { + vm->consts_capacity *= 2; + vm->consts = realloc(vm->consts, sizeof(Value) * vm->consts_capacity); + } + + // Add the converted number to the constants list if it doesn't already + // exist + vm->consts[vm->consts_count++] = conversion.val; + return vm->consts_count - 1; +} + // Emits a bytecode instruction to a function. int fn_emit(Function *fn, Instruction ins) { if (fn->ins == NULL) { @@ -137,8 +152,94 @@ void fn_dump(Function *fn) { } } +// Maximum length of an error description string. +#define ERR_MAX_DESC_LEN 255 + +// Creates a new error from a format string. +Err * err_new(char *fmt, ...) { + va_list args; + va_start(args, fmt); + char *desc = malloc(sizeof(char) * ERR_MAX_DESC_LEN); + vsnprintf(desc, ERR_MAX_DESC_LEN, fmt, args); + va_end(args); + + Err *err = malloc(sizeof(Err)); + err->desc = desc; + err->line = -1; + err->file = NULL; + return err; +} + +// Creates a new error from a vararg list. +Err * err_vnew(char *fmt, va_list args) { + char *desc = malloc(sizeof(char) * ERR_MAX_DESC_LEN); + vsnprintf(desc, ERR_MAX_DESC_LEN, fmt, args); + + Err *err = malloc(sizeof(Err)); + err->desc = desc; + err->line = -1; + err->file = NULL; + return err; +} + +// Frees resources allocated by an error. +void err_free(Err *err) { + if (err == NULL) { + return; + } + free(err->desc); + free(err->file); + free(err); +} + +// Triggers a longjmp back to the most recent setjmp protection. +void err_trigger(VM *vm, Err *err) { + vm->err = err; + longjmp(vm->guard, 1); +} + +// Copies a file path into a new heap allocated string to save with the error. +void err_file(Err *err, char *path) { + if (path == NULL) { + return; + } + err->file = malloc(sizeof(char) * (strlen(path) + 1)); + strcpy(err->file, path); +} + +// ANSI terminal color codes. +#define TEXT_RESET_ALL "\033[0m" +#define TEXT_BOLD "\033[1m" +#define TEXT_RED "\033[31m" +#define TEXT_GREEN "\033[32m" +#define TEXT_BLUE "\033[34m" +#define TEXT_WHITE "\033[37m" + +// Pretty prints an error to the standard output with terminal color codes. +static void err_print_color(Err *err) { + printf("error: %s\n", err->desc); +} + +// Pretty prints an error to the standard output in black and white. +static void err_print_bw(Err *err) { + printf("error: %s\n", err->desc); +} + +// Pretty prints the error to the standard output. If `use_color` is true, then +// terminal color codes will be printed alongside the error information. +void err_print(Err *err, bool use_color) { + if (err == NULL) { + return; + } + if (use_color) { + err_print_color(err); + } else { + err_print_bw(err); + } +} + // Forward declaration. -static HyErr * vm_run(HyVM *vm, int fn_idx, int ins_idx); +static Err * vm_run(VM *vm, int fn_idx, int ins_idx); // Executes some code. The code is run within the package's "main" function, // and can access any variables, functions, imports, etc. that were created by @@ -150,11 +251,11 @@ static HyErr * vm_run(HyVM *vm, int fn_idx, int ins_idx); // // If an error occurs, then the return value will be non-NULL and the error must // be freed using `hy_free_err`. -HyErr * hy_run_string(HyVM *vm, HyPkg pkg, char *code) { +Err * vm_run_string(VM *vm, int pkg, char *code) { // TODO: save and restore VM state in case of error // Parse the source code - HyErr *err = parse(vm, pkg, NULL, code); + Err *err = parse(vm, pkg, NULL, code); if (err != NULL) { return err; } @@ -172,28 +273,28 @@ HyErr * hy_run_string(HyVM *vm, HyPkg pkg, char *code) { // // If an error occurs, then the return value is non-NULL and the error must be // freed. -HyErr * hy_run_file(HyVM *vm, char *path) { +Err * vm_run_file(VM *vm, char *path) { // TODO: save and restore VM state in case of error // Extract the package name from the file path uint64_t name = extract_pkg_name(path); if (name == ~((uint64_t) 0)) { - HyErr *err = err_new("invalid package name from file path `%s`", path); - err_set_file(err, path); + Err *err = err_new("invalid package name from file path `%s`", path); + err_file(err, path); return err; } // Read the file contents char *code = read_file(path); if (code == NULL) { - HyErr *err = err_new("failed to open file `%s`", path); - err_set_file(err, path); + Err *err = err_new("failed to open file `%s`", path); + err_file(err, path); return err; } // Parse the source code int pkg = vm_new_pkg(vm, name); - HyErr *err = parse(vm, pkg, path, code); + Err *err = parse(vm, pkg, path, code); free(code); if (err != NULL) { return err; @@ -205,10 +306,10 @@ HyErr * hy_run_file(HyVM *vm, char *path) { // Executes some bytecode, starting at a particular instruction within a // function. Returns any runtime errors that might occur. -static HyErr * vm_run(HyVM *vm, int fn_idx, int ins_idx) { +static Err * vm_run(VM *vm, int fn_idx, int ins_idx) { static void *dispatch[] = { // Stores - &&op_MOV, &&op_SET_N, &&op_SET_P, &&op_SET_F, + &&op_MOV, &&op_SET_N, &&op_SET_P, &&op_SET_F, &&op_SET_NF, // Arithmetic operators &&op_ADD_LL, &&op_ADD_LN, &&op_SUB_LL, &&op_SUB_LN, &&op_SUB_NL, @@ -226,13 +327,13 @@ static HyErr * vm_run(HyVM *vm, int fn_idx, int ins_idx) { // Move some variables into the function's local scope Function *fns = vm->fns; - uint64_t *ks = vm->consts; - uint64_t *stk = vm->stack; + Value *ks = vm->consts; + Value *stk = vm->stack; // State information required during runtime Function *fn = &vm->fns[fn_idx]; Instruction *ip = &fn->ins[ins_idx]; - HyErr *err = NULL; + Err *err = NULL; // Some helpful macros to reduce repetition #define DISPATCH() goto *dispatch[ins_op(*ip)] @@ -257,10 +358,13 @@ static HyErr * vm_run(HyVM *vm, int fn_idx, int ins_idx) { stk[ins_arg1(*ip)] = ks[ins_arg16(*ip)]; NEXT(); op_SET_P: - stk[ins_arg1(*ip)] = FLAG_PRIM | ins_arg16(*ip); + stk[ins_arg1(*ip)] = TAG_PRIM | ins_arg16(*ip); NEXT(); op_SET_F: - stk[ins_arg1(*ip)] = FLAG_FN | ins_arg16(*ip); + stk[ins_arg1(*ip)] = TAG_FN | ins_arg16(*ip); + NEXT(); +op_SET_NF: + stk[ins_arg1(*ip)] = TAG_NATIVE | ins_arg16(*ip); NEXT(); // Arithmetic operations @@ -305,7 +409,7 @@ static HyErr * vm_run(HyVM *vm, int fn_idx, int ins_idx) { if (stk[ins_arg1(*ip)] op ks[ins_arg2(*ip)]) { ip++; } \ NEXT(); \ op_ ## name ## _LP: \ - if (stk[ins_arg1(*ip)] op (FLAG_PRIM | ins_arg2(*ip))) { ip++; } \ + if (stk[ins_arg1(*ip)] op (TAG_PRIM | ins_arg2(*ip))) { ip++; } \ NEXT(); // We invert the condition because we want to skip the following JMP only @@ -333,7 +437,7 @@ static HyErr * vm_run(HyVM *vm, int fn_idx, int ins_idx) { // Control flow op_JMP: - ip += ins_arg24(*ip) - JMP_BIAS; + ip += (int32_t) ins_arg24(*ip) - JMP_BIAS; NEXT(); op_CALL: @@ -347,6 +451,6 @@ static HyErr * vm_run(HyVM *vm, int fn_idx, int ins_idx) { // We arrive at this label if an error occurs, or we successfully return // from the top most function finish: - printf("First stack slot: %g\n", v2n(stk[1])); + printf("First stack slot: %g\n", v2n(stk[0])); return err; } diff --git a/src/vm/vm.h b/src/vm.h similarity index 52% rename from src/vm/vm.h rename to src/vm.h index 842b20f..c7065d7 100644 --- a/src/vm/vm.h +++ b/src/vm.h @@ -6,14 +6,14 @@ #ifndef VM_H #define VM_H -#include - #include #include #include +#include #include #include "bytecode.h" +#include "value.h" // A package contains a collection of function definitions. typedef struct { @@ -53,9 +53,60 @@ typedef struct { int ins_count, ins_capacity; } Function; +// Emits a bytecode instruction to a function. +int fn_emit(Function *fn, Instruction ins); + +// Dumps the bytecode for a function to the standard output. +void fn_dump(Function *fn); + +// A native function definition. +typedef struct { + // The name of this native function. + uint64_t name; + + // The index of the package that this function is associated with. + int pkg; + + // The number of arguments required by this function. + int args_count; + + // The native function pointer itself. + void *fn; +} NativeFn; + +// Contains all information about an error. +typedef struct { + // Heap-allocated description string. + char *desc; + + // Path to the file in which the error occurred, or NULL if the error has + // no associated file (e.g. it occurred in a string). + char *file; + + // Line on which the error occurred, or -1 if the error has no associated + // line number. + int line; +} Err; + +// Creates a new error from a format string. +Err * err_new(char *fmt, ...); + +// Creates a new error from a vararg list. +Err * err_vnew(char *fmt, va_list args); + +// Frees resources allocated by an error. +void err_free(Err *err); + +// Copies a file path into a new heap allocated string to save with the error. +void err_file(Err *err, char *path); + +// Pretty prints the error to the standard output. If `use_color` is true, then +// terminal color codes will be printed alongside the error information. +void err_print(Err *err, bool use_color); + // Hydrogen has no global state; everything that's needed is stored in this // struct. You can create multiple VMs and they'll all function independently. -struct HyVM { +typedef struct { // We keep a list of all loaded packages so that if a piece of code attempts // to import a package we've already loaded, we don't have to re-load that // package. @@ -70,34 +121,66 @@ struct HyVM { Function *fns; int fns_count, fns_capacity; + // We keep a global list of native functions, rather than a per-package + // list, for the same reason above. + NativeFn *natives; + int natives_count, natives_capacity; + // Global list of constants that we can reference by index. - uint64_t *consts; + Value *consts; int consts_count, consts_capacity; // The most recent error. This is set just before a longjmp back to the // protecting setjmp call. - HyErr *err; + Err *err; jmp_buf guard; // Memory used for the runtime stack. This is persisted across calls to // `hy_run...` so that we can implement the REPL. - uint64_t *stack; + Value *stack; int stack_size; -}; +} VM; + +// Creates a new virtual machine instance. +VM vm_new(); + +// Frees all the resources allocated by a virtual machine. +void vm_free(VM *vm); // Creates a new package on the VM and returns its index. -int vm_new_pkg(HyVM *vm, uint64_t name); +int vm_new_pkg(VM *vm, uint64_t name); // Creates a new function on the VM and returns its index. -int vm_new_fn(HyVM *vm, int pkg_idx); +int vm_new_fn(VM *vm, int pkg); -// Adds a constant number to the VM's constants list, returning its index. -int vm_add_const_num(HyVM *vm, double num); +// Creates a new native function on the VM and returns its index. +int vm_new_native(VM *vm, int pkg, uint64_t name, int args, void *fn); -// Emits a bytecode instruction to a function. -int fn_emit(Function *fn, Instruction ins); - -// Dumps the bytecode for a function to the standard output. -void fn_dump(Function *fn); +// Adds a constant number to the VM's constants list, returning its index. +int vm_add_num(VM *vm, double num); + +// Triggers a longjmp back to the most recent setjmp protection. +void err_trigger(VM *vm, Err *err); + +// Executes some code. The code is run within the package's "main" function, +// and can access any variables, functions, imports, etc. that were created by +// a previous piece of code run on this package. This functionality is used to +// create the REPL. +// +// Since no file path is specified, any imports are relative to the current +// working directory. +// +// If an error occurs, then the return value will be non-NULL. +Err * vm_run_string(VM *vm, int pkg, char *code); + +// Executes a file. A new package is created for the file and is named based off +// the name of the file. The package can be later imported by other pieces of +// code. +// +// Both the directory containing the file and the current working directory are +// searched when the file attempts to import any other packages. +// +// If an error occurs, then the return value will be non-NULL. +Err * vm_run_file(VM *vm, char *path); #endif diff --git a/src/vm/err.c b/src/vm/err.c deleted file mode 100644 index e71fffe..0000000 --- a/src/vm/err.c +++ /dev/null @@ -1,110 +0,0 @@ - -// err.c -// By Ben Anderson -// July 2018 - -#include "err.h" - -#include -#include - -// Creates a new error from a format string. -HyErr * err_new(char *fmt, ...) { - va_list args; - va_start(args, fmt); - char *desc = malloc(sizeof(char) * ERR_MAX_DESC_LEN); - vsnprintf(desc, ERR_MAX_DESC_LEN, fmt, args); - va_end(args); - - HyErr *err = malloc(sizeof(HyErr)); - err->desc = desc; - err->line = -1; - err->file = NULL; - return err; -} - -// Creates a new error from a vararg list. -HyErr * err_vnew(char *fmt, va_list args) { - char *desc = malloc(sizeof(char) * ERR_MAX_DESC_LEN); - vsnprintf(desc, ERR_MAX_DESC_LEN, fmt, args); - - HyErr *err = malloc(sizeof(HyErr)); - err->desc = desc; - err->line = -1; - err->file = NULL; - return err; -} - -// Frees resources allocated by an error. -void hy_free_err(HyErr *err) { - // Built-in resilience against a dumb user - if (err == NULL) { - return; - } - free(err->desc); - free(err->file); - free(err); -} - -// Triggers a longjmp back to the most recent setjmp protection. -void err_trigger(HyVM *vm, HyErr *err) { - vm->err = err; - longjmp(vm->guard, 1); -} - -// Returns a description of the error that's occurred. -char * hy_err_desc(HyErr *err) { - return err->desc; -} - -// Returns the path to the file that an error occurred in, or NULL if the error -// has no associated file. The returned string should NOT be freed. -char * hy_err_file(HyErr *err) { - return err->file; -} - -// Copies a file path into a new heap allocated string to save with the error. -void err_set_file(HyErr *err, char *path) { - if (path == NULL) { - return; - } - err->file = malloc(sizeof(char) * (strlen(path) + 1)); - strcpy(err->file, path); -} - -// Returns the line number that the error occurred on, or -1 if the error has -// no associated line number. -int hy_err_line(HyErr *err) { - return err->line; -} - -// ANSI terminal color codes. -#define TEXT_RESET_ALL "\033[0m" -#define TEXT_BOLD "\033[1m" -#define TEXT_RED "\033[31m" -#define TEXT_GREEN "\033[32m" -#define TEXT_BLUE "\033[34m" -#define TEXT_WHITE "\033[37m" - -// Pretty prints an error to the standard output with terminal color codes. -static void err_print_color(HyErr *err) { - printf("error: %s\n", err->desc); -} - -// Pretty prints an error to the standard output in black and white. -static void err_print_bw(HyErr *err) { - printf("error: %s\n", err->desc); -} - -// Pretty prints the error to the standard output. If `use_color` is true, then -// terminal color codes will be printed alongside the error information. -void hy_err_print(HyErr *err, bool use_color) { - if (err == NULL) { - return; - } - if (use_color) { - err_print_color(err); - } else { - err_print_bw(err); - } -} diff --git a/src/vm/err.h b/src/vm/err.h deleted file mode 100644 index f735929..0000000 --- a/src/vm/err.h +++ /dev/null @@ -1,42 +0,0 @@ - -// err.h -// By Ben Anderson -// July 2018 - -#ifndef ERR_H -#define ERR_H - -#include "vm.h" - -#include - -// Maximum length of an error description string. -#define ERR_MAX_DESC_LEN 255 - -// Contains all information about an error. -struct HyErr { - // Heap-allocated description string. - char *desc; - - // Path to the file in which the error occurred, or NULL if the error has - // no associated file (e.g. it occurred in a string). - char *file; - - // Line on which the error occurred, or -1 if the error has no associated - // line number. - int line; -}; - -// Creates a new error from a format string. -HyErr * err_new(char *fmt, ...); - -// Creates a new error from a vararg list. -HyErr * err_vnew(char *fmt, va_list args); - -// Copies a file path into a new heap allocated string to save with the error. -void err_set_file(HyErr *err, char *path); - -// Triggers a longjmp back to the most recent setjmp protection. -void err_trigger(HyVM *vm, HyErr *err); - -#endif diff --git a/tests/runtime/test_runtime.py b/tests/runtime/test_runtime.py new file mode 100755 index 0000000..f5fcef6 --- /dev/null +++ b/tests/runtime/test_runtime.py @@ -0,0 +1,237 @@ + +# +# Runtime Tests +# + +# Command line arguments: +# 1) Path to folder containing Hydrogen scripts to test +# 2) Path to Hydrogen CLI binary + +import os +import sys +import platform +import re + +from os.path import join, dirname, basename, isdir, splitext, realpath +from subprocess import Popen, PIPE +from threading import Timer + +# Check we have sufficient command line arguments +if len(sys.argv) != 3: + print("Usage: python test_runtime.py ") + sys.exit(1) + +# The path to the command line interface which will execute the Hydrogen code +# for us +cli_path = sys.argv[2] + +# The amount of time in seconds to let a test case run before killing it +timeout = 2 + +# Available color codes +COLOR_NONE = "\x1B[0m" +COLOR_RED = "\x1B[31m" +COLOR_GREEN = "\x1B[32m" +COLOR_YELLOW = "\x1B[33m" +COLOR_BLUE = "\x1B[34m" +COLOR_MAGENTA = "\x1B[35m" +COLOR_CYAN = "\x1B[36m" +COLOR_WHITE = "\x1B[37m" +COLOR_BOLD = "\x1B[1m" + +# Prints some color code to the standard output +def print_color(color): + if platform.system() != "Windows": + sys.stdout.write(color) + +# Extracts the expected output of a test case from its source code. Returns +# a list of strings, one for each line of expected output, and the expected +# error code for the test +def expected_output(source): + # We need to find every instance of `//> ` and concatenate them + # together, separated by newlines + key = "//> " + result = [] + line_number = 0 + for line in source.splitlines(): + line_number += 1 + + # Find what we're searching for + found = line.find(key) + if found == -1: + continue + + # Append the rest of the line + index = found + len(key) + result.append({"content": line[index:], "line": line_number}) + + return result + +# Runs a test program from its path. Returns the exit code for the process and +# what was written to the standard output. Returns -1 for the error code if the +# process timed out +def run_test(path): + # Create the process + proc = Popen([cli_path, path], stdin=PIPE, stdout=PIPE, stderr=PIPE) + + # Kill test cases that take longer than `timeout` seconds + timed_out = False + def kill(test_case): + timed_out = True + test_case.kill() + timer = Timer(timeout, kill, [proc]) + + # Execute the test case + exit_code = -1 # Default to failure + output = None + error = None + try: + timer.start() + output, error = proc.communicate() + if not timed_out: + exit_code = proc.returncode + finally: + timer.cancel() + + return (output, error, exit_code) + +# Prints an error message to the standard output +def print_error(message): + print_color(COLOR_RED + COLOR_BOLD) + sys.stdout.write("[Error] ") + print_color(COLOR_NONE) + print(message) + +# Validates the output of a test case, returning true if the test was +# successful +def validate(path, expected, output, error, exit_code): + # Parse the output into lines + try: + output = output.decode("utf-8").replace("\r\n", "\n").strip() + except: + print_error("Failed to decode output") + return False + + # Check if the test case timed out + if exit_code == -1: + print_error("Timed out") + return False + + # Check if the test case returned an error + if exit_code != 0: + print_error("Test exited with error") + if len(output) > 0: + print("Output:") + print(output) + if len(error) > 0: + print("Error:") + print(error) + return False + + # Convert output into multiple lines + output_lines = [] + if len(output) > 0: + output_lines = output.strip().split("\n") + + # Check output lengths match + if len(expected) != len(output_lines): + print_error("Incorrect number of output lines") + return False + + # Check each line + for i in range(len(output_lines)): + expected_line = expected[i]["content"] + + # Check the output matched what was expected + if output_lines[i] != expected_line: + line_number = expected[i]["line"] + print_error("Incorrect output on line " + str(line_number) + + ": expected " + expected_line + ", got " + output_lines[i]) + return False + + # Print passed test case message + print_color(COLOR_BOLD + COLOR_GREEN) + sys.stdout.write("[Passed]") + print_color(COLOR_NONE) + print("") + return True + +# Executes the runtime test for the Hydrogen code at `path` +def test(path): + # Print info + print_color(COLOR_BLUE + COLOR_BOLD) + sys.stdout.write("[Test] ") + print_color(COLOR_NONE) + + suite = basename(dirname(path)) + name = splitext(basename(path))[0].replace("_", " ") + print("Testing " + suite + " > " + name) + + # Open the input file + input_file = open(path, "r") + if not input_file: + print_error("Failed to open file") + return False + + # Read the contents of the file + source = input_file.read() + input_file.close() + + # Extract the expected output for the case + expected = expected_output(source) + + # Get the output and exit code for the test case + output, error, exit_code = run_test(path) + + # Validates a test case's output + return validate(path, expected, output, error, exit_code) + +# Tests all Hydrogen files in a directory. Returns the total number of tests, +# and the number of tests passed +def test_dir(path): + # Count the total number of tests executed, and the number of tests that + # passed + total = 0 + passed = 0 + + # Iterate over every file in the directory + files = os.listdir(path) + for case in files: + path = join(path, case) + if isdir(path): + # Test every subdirectory recursively + subdir_total, subdir_passed = test_dir(path) + total += subdir_total + passed += subdir_passed + elif splitext(path)[1] == ".hy": + # Only test files with a file extension `.hy` + if test(path): + passed += 1 + total += 1 + return (total, passed) + +# Test all files in this directory +total, passed = test_dir(sys.argv[1]) + +# Add a newline +if total > 0: + print("") + print_color(COLOR_BOLD) + +# Print number of tests passed +if total == passed: + # All tests passed + print_color(COLOR_GREEN) + sys.stdout.write("[Success] ") + print_color(COLOR_NONE) + print("All tests passed!") +else: + # Some tests failed + print_color(COLOR_RED) + sys.stdout.write("[Failure] ") + print_color(COLOR_NONE) + print(str(passed) + " of " + str(total) + " tests passed") + + # Exit with a failure status code, to let `make test` know that the runtime + # tests failed on the whole + sys.exit(1) diff --git a/tests/test_lexer.cpp b/tests/test_lexer.cpp index f49ec00..9eb0343 100644 --- a/tests/test_lexer.cpp +++ b/tests/test_lexer.cpp @@ -6,27 +6,27 @@ #include extern "C" { - #include - #include + #include + #include } // Stores all the information needed to test the lexer. typedef struct { - HyVM *vm; + VM vm; Lexer lxr; } MockLexer; // Creates a new mock lexer. MockLexer mock_new(const char *code) { MockLexer mock; - mock.vm = hy_new_vm(); - mock.lxr = lex_new(mock.vm, NULL, (char *) code); + mock.vm = vm_new(); + mock.lxr = lex_new(&mock.vm, NULL, (char *) code); return mock; } // Free resources allocated by the mock lexer. void mock_free(MockLexer *mock) { - hy_free_vm(mock->vm); + vm_free(&mock->vm); } TEST(Lexer, SingleCharSymbols) { diff --git a/tests/test_parser.cpp b/tests/test_parser.cpp index 8078970..f47271b 100644 --- a/tests/test_parser.cpp +++ b/tests/test_parser.cpp @@ -6,50 +6,50 @@ #include extern "C" { - #include - #include - #include - #include - #include + #include + #include + #include + #include + #include } // Parses a piece of source code and iterates over the emitted bytecode, // allowing us to easily and sequentially assert instructions. class MockParser { public: - HyVM *vm; + VM vm; size_t cur_fn, cur_ins; // Create a new mock parser object. MockParser(const char *code) { - vm = hy_new_vm(); + vm = vm_new(); cur_fn = 0; cur_ins = 0; // Add a package (its main function is created automatically) - int pkg = vm_new_pkg(vm, hash_string("test", 4)); + int pkg = vm_new_pkg(&vm, hash_string("test", 4)); // Parse the source code - HyErr *err = parse(vm, pkg, NULL, (char *) code); + Err *err = parse(&vm, pkg, NULL, (char *) code); if (err != NULL) { - ADD_FAILURE() << hy_err_desc(err) << " at line " << hy_err_line(err); + ADD_FAILURE() << err->desc << " at line " << err->line; } } // Free resources allocated by a mock parser object. ~MockParser() { - hy_free_vm(vm); + vm_free(&vm); } // Dump all parsed functions to the standard output. void dump() { - fn_dump(&vm->fns[cur_fn]); + fn_dump(&vm.fns[cur_fn]); } // Increment the current instruction counter and return the next instruction // to assert. Instruction next() { - return vm->fns[cur_fn].ins[cur_ins++]; + return vm.fns[cur_fn].ins[cur_ins++]; } }; @@ -59,30 +59,30 @@ class MockParser { mock.cur_fn = (fn_idx); // Asserts the current bytecode instruction's opcode and arguments. -#define INS(opcode, a, b, c) { \ - ASSERT_TRUE(mock.cur_ins < mock.vm->fns[mock.cur_fn].ins_count); \ - Instruction ins = mock.next(); \ - ASSERT_EQ(ins_op(ins), opcode); \ - ASSERT_EQ(ins_arg1(ins), a); \ - ASSERT_EQ(ins_arg2(ins), b); \ - ASSERT_EQ(ins_arg3(ins), c); \ +#define INS(opcode, a, b, c) { \ + ASSERT_TRUE(mock.cur_ins < mock.vm.fns[mock.cur_fn].ins_count); \ + Instruction ins = mock.next(); \ + ASSERT_EQ(ins_op(ins), opcode); \ + ASSERT_EQ(ins_arg1(ins), a); \ + ASSERT_EQ(ins_arg2(ins), b); \ + ASSERT_EQ(ins_arg3(ins), c); \ } // Asserts the current instruction as an extended, 2 argument instruction. -#define INS2(opcode, a, d) { \ - ASSERT_TRUE(mock.cur_ins < mock.vm->fns[mock.cur_fn].ins_count); \ - Instruction ins = mock.next(); \ - ASSERT_EQ(ins_op(ins), opcode); \ - ASSERT_EQ(ins_arg1(ins), a); \ - ASSERT_EQ(ins_arg16(ins), d); \ +#define INS2(opcode, a, d) { \ + ASSERT_TRUE(mock.cur_ins < mock.vm.fns[mock.cur_fn].ins_count); \ + Instruction ins = mock.next(); \ + ASSERT_EQ(ins_op(ins), opcode); \ + ASSERT_EQ(ins_arg1(ins), a); \ + ASSERT_EQ(ins_arg16(ins), d); \ } // Asserts the current instruction is a JMP, with the given offset. -#define JMP(offset) { \ - ASSERT_TRUE(mock.cur_ins < mock.vm->fns[mock.cur_fn].ins_count); \ - Instruction ins = mock.next(); \ - ASSERT_EQ(ins_op(ins), OP_JMP); \ - ASSERT_EQ(ins_arg24(ins), JMP_BIAS + offset - 1); \ +#define JMP(offset) { \ + ASSERT_TRUE(mock.cur_ins < mock.vm.fns[mock.cur_fn].ins_count); \ + Instruction ins = mock.next(); \ + ASSERT_EQ(ins_op(ins), OP_JMP); \ + ASSERT_EQ(ins_arg24(ins), JMP_BIAS + offset - 1); \ } TEST(Assignment, NumberAssignment) { @@ -332,6 +332,8 @@ TEST(Logic, FoldEquality) { "let e = 3 == 8-5\n" ); + mock.dump(); + INS2(OP_SET_N, 0, 0); INS2(OP_SET_N, 1, 1); INS2(OP_SET_P, 2, PRIM_FALSE);