From bb647d424748ed39e666a484656933741c7a74e0 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Fri, 6 Feb 2015 07:01:15 -0800 Subject: [PATCH] Start getting module loading working. Right now, it uses a weird "import_" method on String which should either be replaced or at least hidden behind some syntax. But it does roughly the right thing. Still lots of corner cases to clean up and stuff to fix. In particular: - Need to handle compilation errors in imported modules. - Need to implicitly import all core and IO types into imported module. - Need to handle circular imports. (Just need to give entry module the right name for this to work.) --- builtin/core.wren | 13 +++- include/wren.h | 21 ++++++ src/main.c | 64 +++++++++++++++++++ src/wren_core.c | 63 +++++++++++++++++- src/wren_vm.c | 52 +++++++++++++++ src/wren_vm.h | 13 ++++ .../change_imported_value.wren | 14 ++++ test/module/change_imported_value/module.wren | 12 ++++ test/module/multiple_variables/module.wren | 8 +++ .../multiple_variables.wren | 14 ++++ test/module/shared_import/a.wren | 5 ++ test/module/shared_import/b.wren | 5 ++ test/module/shared_import/shared.wren | 3 + test/module/shared_import/shared_import.wren | 12 ++++ test/module/simple_import/module.wren | 3 + test/module/simple_import/simple_import.wren | 4 ++ .../module/unknown_module/unknown_module.wren | 1 + test/module/unknown_variable/module.wren | 3 + .../unknown_variable/unknown_variable.wren | 3 + 19 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 test/module/change_imported_value/change_imported_value.wren create mode 100644 test/module/change_imported_value/module.wren create mode 100644 test/module/multiple_variables/module.wren create mode 100644 test/module/multiple_variables/multiple_variables.wren create mode 100644 test/module/shared_import/a.wren create mode 100644 test/module/shared_import/b.wren create mode 100644 test/module/shared_import/shared.wren create mode 100644 test/module/shared_import/shared_import.wren create mode 100644 test/module/simple_import/module.wren create mode 100644 test/module/simple_import/simple_import.wren create mode 100644 test/module/unknown_module/unknown_module.wren create mode 100644 test/module/unknown_variable/module.wren create mode 100644 test/module/unknown_variable/unknown_variable.wren diff --git a/builtin/core.wren b/builtin/core.wren index 8fe04790..4e955b4a 100644 --- a/builtin/core.wren +++ b/builtin/core.wren @@ -58,7 +58,18 @@ class Sequence { } } -class String is Sequence {} +class String is Sequence { + import_(variable) { + var result = loadModule_ + if (result == false) { + Fiber.abort("Could not find module '" + this + "'.") + } else if (result != true) { + result.call + } + + return lookUpVariable_(variable) + } +} class List is Sequence { addAll(other) { diff --git a/include/wren.h b/include/wren.h index ac3315f0..c799fc1b 100644 --- a/include/wren.h +++ b/include/wren.h @@ -25,8 +25,12 @@ typedef struct WrenVM WrenVM; // [oldSize] will be zero. It should return NULL. typedef void* (*WrenReallocateFn)(void* memory, size_t oldSize, size_t newSize); +// A function callable from Wren code, but implemented in C. typedef void (*WrenForeignMethodFn)(WrenVM* vm); +// Loads and returns the source code for the module [name]. +typedef char* (*WrenLoadModuleFn)(WrenVM* vm, const char* name); + typedef struct { // The callback Wren will use to allocate, reallocate, and deallocate memory. @@ -34,6 +38,23 @@ typedef struct // If `NULL`, defaults to a built-in function that uses `realloc` and `free`. WrenReallocateFn reallocateFn; + // The callback Wren uses to load a module. + // + // Since Wren does not talk directly to the file system, it relies on the + // embedder to phyisically locate and read the source code for a module. The + // first time an import appears, Wren will call this and pass in the name of + // the module being imported. The VM should return the soure code for that + // module. Memory for the source should be allocated using [reallocateFn] and + // Wren will take ownership over it. + // + // This will only be called once for any given module name. Wren caches the + // result internally so subsequent imports of the same module will use the + // previous source and not call this. + // + // If a module with the given name could not be found by the embedder, it + // should return NULL and Wren will report that as a runtime error. + WrenLoadModuleFn loadModuleFn; + // The number of bytes Wren will allocate before triggering the first garbage // collection. // diff --git a/src/main.c b/src/main.c index e169b2e9..3039411c 100644 --- a/src/main.c +++ b/src/main.c @@ -11,6 +11,8 @@ // This is the source file for the standalone command line interpreter. It is // not needed if you are embedding Wren in an application. +char* rootDirectory = NULL; + static void failIf(bool condition, int exitCode, const char* format, ...) { if (!condition) return; @@ -48,8 +50,66 @@ static char* readFile(const char* path) return buffer; } +static char* readModule(WrenVM* vm, const char* module) +{ + // The module path is relative to the root directory. + size_t rootLength = strlen(rootDirectory); + size_t moduleLength = strlen(module); + size_t pathLength = rootLength + moduleLength; + char* path = malloc(pathLength + 1); + memcpy(path, rootDirectory, rootLength); + memcpy(path + rootLength, module, moduleLength); + path[pathLength] = '\0'; + + FILE* file = fopen(path, "rb"); + if (file == NULL) + { + free(path); + return NULL; + } + + // Find out how big the file is. + fseek(file, 0L, SEEK_END); + size_t fileSize = ftell(file); + rewind(file); + + // Allocate a buffer for it. + char* buffer = (char*)malloc(fileSize + 1); + if (buffer == NULL) + { + fclose(file); + free(path); + return NULL; + } + + // Read the entire file. + size_t bytesRead = fread(buffer, sizeof(char), fileSize, file); + if (bytesRead < fileSize) + { + free(buffer); + fclose(file); + free(path); + return NULL; + } + + // Terminate the string. + buffer[bytesRead] = '\0'; + + fclose(file); + free(path); + + return buffer; +} + static int runFile(WrenVM* vm, const char* path) { + // Use the directory where the file is as the root to resolve imports + // relative to. + char* lastSlash = strrchr(path, '/'); + rootDirectory = malloc(lastSlash - path + 2); + memcpy(rootDirectory, path, lastSlash - path + 1); + rootDirectory[lastSlash - path + 1] = '\0'; + char* source = readFile(path); int result; @@ -75,6 +135,8 @@ static int runRepl(WrenVM* vm) printf("\\\\/\"-\n"); printf(" \\_/ wren v0.0.0\n"); + // TODO: Set rootDirectory to current working directory so imports work. + char line[MAX_LINE_LENGTH]; for (;;) @@ -109,6 +171,8 @@ int main(int argc, const char* argv[]) WrenConfiguration config; + config.loadModuleFn = readModule; + // Since we're running in a standalone process, be generous with memory. config.initialHeapSize = 1024 * 1024 * 100; diff --git a/src/wren_core.c b/src/wren_core.c index 07853db0..099e8a28 100644 --- a/src/wren_core.c +++ b/src/wren_core.c @@ -101,7 +101,18 @@ static const char* libSource = " }\n" "}\n" "\n" -"class String is Sequence {}\n" +"class String is Sequence {\n" +" import_(variable) {\n" +" var result = loadModule_\n" +" if (result == false) {\n" +" Fiber.abort(\"Could not find module '\" + this + \"'.\")\n" +" } else if (result != true) {\n" +" result.call\n" +" }\n" +"\n" +" return lookUpVariable_(variable)\n" +" }\n" +"}\n" "\n" "class List is Sequence {\n" " addAll(other) {\n" @@ -1294,6 +1305,51 @@ DEF_NATIVE(string_subscript) RETURN_OBJ(result); } +DEF_NATIVE(string_loadModule) +{ + RETURN_VAL(wrenImportModule(vm, AS_CSTRING(args[0]))); +} + +DEF_NATIVE(string_lookUpVariable) +{ + uint32_t moduleEntry = wrenMapFind(vm->modules, args[0]); + ASSERT(moduleEntry != UINT32_MAX, "Should only look up loaded modules."); + + ObjModule* module = AS_MODULE(vm->modules->entries[moduleEntry].value); + + ObjString* variableName = AS_STRING(args[1]); + uint32_t variable = wrenSymbolTableFind(&module->variableNames, + variableName->value, + variableName->length); + + // It's a runtime error if the imported variable does not exist. + if (variable == UINT32_MAX) + { + // TODO: This is pretty verbose. Do something cleaner? + ObjString* moduleName = AS_STRING(args[0]); + int length = 48 + variableName->length + moduleName->length; + ObjString* error = AS_STRING(wrenNewUninitializedString(vm, length)); + + char* start = error->value; + memcpy(start, "Could not find a variable named '", 33); + start += 33; + memcpy(start, variableName->value, variableName->length); + start += variableName->length; + memcpy(start, "' in module '", 13); + start += 13; + memcpy(start, moduleName->value, moduleName->length); + start += moduleName->length; + memcpy(start, "'.", 2); + start += 2; + *start = '\0'; + + args[0] = OBJ_VAL(error); + return PRIM_ERROR; + } + + RETURN_VAL(module->variables.data[variable]); +} + static ObjClass* defineSingleClass(WrenVM* vm, const char* name) { size_t length = strlen(name); @@ -1495,6 +1551,11 @@ void wrenInitializeCore(WrenVM* vm) NATIVE(vm->rangeClass, "iteratorValue ", range_iteratorValue); NATIVE(vm->rangeClass, "toString", range_toString); + // TODO: Putting these on String is pretty strange. Find a better home for + // them. + NATIVE(vm->stringClass, "loadModule_", string_loadModule); + NATIVE(vm->stringClass, "lookUpVariable_ ", string_lookUpVariable); + // While bootstrapping the core types and running the core library, a number // string objects have been created, many of which were instantiated before // stringClass was stored in the VM. Some of them *must* be created first: diff --git a/src/wren_vm.c b/src/wren_vm.c index fbc3b7b1..526b4f14 100644 --- a/src/wren_vm.c +++ b/src/wren_vm.c @@ -37,6 +37,7 @@ WrenVM* wrenNewVM(WrenConfiguration* configuration) vm->reallocate = reallocate; vm->foreignCallSlot = NULL; vm->foreignCallNumArgs = 0; + vm->loadModule = configuration->loadModuleFn; wrenSymbolTableInit(vm, &vm->methodNames); @@ -1123,6 +1124,57 @@ void wrenPopRoot(WrenVM* vm) vm->numTempRoots--; } +Value wrenImportModule(WrenVM* vm, const char* name) +{ + Value nameValue = wrenNewString(vm, name, strlen(name)); + wrenPushRoot(vm, AS_OBJ(nameValue)); + + // If the module is already loaded, we don't need to do anything. + uint32_t index = wrenMapFind(vm->modules, nameValue); + if (index != UINT32_MAX) + { + wrenPopRoot(vm); // nameValue. + return TRUE_VAL; + } + + // Load the module's source code from the embedder. + char* source = vm->loadModule(vm, name); + if (source == NULL) + { + wrenPopRoot(vm); // nameValue. + return FALSE_VAL; + } + + ObjModule* module = wrenNewModule(vm); + wrenPushRoot(vm, (Obj*)module); + + // Implicitly import the core libraries. + // TODO: Import all implicit names. + // TODO: Only import IO if it's loaded. + ObjModule* mainModule = getMainModule(vm); + int symbol = wrenSymbolTableFind(&mainModule->variableNames, "IO", 2); + wrenDefineVariable(vm, module, "IO", 2, mainModule->variables.data[symbol]); + + ObjFn* fn = wrenCompile(vm, module, name, source); + // TODO: Handle NULL fn from compilation error. + + wrenPushRoot(vm, (Obj*)fn); + + // Store it in the VM's module registry so we don't load the same module + // multiple times. + wrenMapSet(vm, vm->modules, nameValue, OBJ_VAL(module)); + + ObjFiber* moduleFiber = wrenNewFiber(vm, (Obj*)fn); + + wrenPopRoot(vm); // fn. + wrenPopRoot(vm); // module. + wrenPopRoot(vm); // nameValue. + + // Return the fiber that executes the module. + return OBJ_VAL(moduleFiber); +} + + static void defineMethod(WrenVM* vm, const char* className, const char* methodName, int numParams, WrenForeignMethodFn methodFn, bool isStatic) diff --git a/src/wren_vm.h b/src/wren_vm.h index b860cd32..86b05a2c 100644 --- a/src/wren_vm.h +++ b/src/wren_vm.h @@ -246,6 +246,9 @@ struct WrenVM // to the function. int foreignCallNumArgs; + // The function used to load modules. + WrenLoadModuleFn loadModule; + // Compiler and debugger data: // The compiler that is currently compiling code. This is used so that heap @@ -308,6 +311,16 @@ void wrenPushRoot(WrenVM* vm, Obj* obj); // Remove the most recently pushed temporary root. void wrenPopRoot(WrenVM* vm); +// Imports the module with [name]. +// +// If the module has already been imported (or is already in the middle of +// being imported, in the case of a circular import), returns true. Otherwise, +// returns a new fiber that will execute the module's code. That fiber should +// be called before any variables are loaded from the module. +// +// If the module could not be found, returns false. +Value wrenImportModule(WrenVM* vm, const char* name); + // Returns the class of [value]. // // Defined here instead of in wren_value.h because it's critical that this be diff --git a/test/module/change_imported_value/change_imported_value.wren b/test/module/change_imported_value/change_imported_value.wren new file mode 100644 index 00000000..73d650e7 --- /dev/null +++ b/test/module/change_imported_value/change_imported_value.wren @@ -0,0 +1,14 @@ +var Module = "module.wren".import_("Module") +var Other = "module.wren".import_("Other") + +IO.print(Module) // expect: before + +// Reassigning the variable in the other module does not affect this one's +// binding. +Other.change +IO.print(Module) // expect: before + +// But it does change there. +Other.show // expect: after + +// TODO: Cyclic import. diff --git a/test/module/change_imported_value/module.wren b/test/module/change_imported_value/module.wren new file mode 100644 index 00000000..fac9ddc9 --- /dev/null +++ b/test/module/change_imported_value/module.wren @@ -0,0 +1,12 @@ +// nontest +var Module = "before" + +class Other { + static change { + Module = "after" + } + + static show { + IO.print(Module) + } +} diff --git a/test/module/multiple_variables/module.wren b/test/module/multiple_variables/module.wren new file mode 100644 index 00000000..ed5932ca --- /dev/null +++ b/test/module/multiple_variables/module.wren @@ -0,0 +1,8 @@ +// nontest +var Module1 = "from module one" +var Module2 = "from module two" +var Module3 = "from module three" +var Module4 = "from module four" +var Module5 = "from module five" + +IO.print("ran module") diff --git a/test/module/multiple_variables/multiple_variables.wren b/test/module/multiple_variables/multiple_variables.wren new file mode 100644 index 00000000..0bd64d96 --- /dev/null +++ b/test/module/multiple_variables/multiple_variables.wren @@ -0,0 +1,14 @@ +var Module1 = "module.wren".import_("Module1") +var Module2 = "module.wren".import_("Module2") +var Module3 = "module.wren".import_("Module3") +var Module4 = "module.wren".import_("Module4") +var Module5 = "module.wren".import_("Module5") + +// Only execute module body once: +// expect: ran module + +IO.print(Module1) // expect: from module one +IO.print(Module2) // expect: from module two +IO.print(Module3) // expect: from module three +IO.print(Module4) // expect: from module four +IO.print(Module5) // expect: from module five diff --git a/test/module/shared_import/a.wren b/test/module/shared_import/a.wren new file mode 100644 index 00000000..7458c349 --- /dev/null +++ b/test/module/shared_import/a.wren @@ -0,0 +1,5 @@ +// nontest +IO.print("a") +var Shared = "shared.wren".import_("Shared") +var A = "a " + Shared +IO.print("a done") diff --git a/test/module/shared_import/b.wren b/test/module/shared_import/b.wren new file mode 100644 index 00000000..f51de40c --- /dev/null +++ b/test/module/shared_import/b.wren @@ -0,0 +1,5 @@ +// nontest +IO.print("b") +var Shared = "shared.wren".import_("Shared") +var B = "b " + Shared +IO.print("b done") diff --git a/test/module/shared_import/shared.wren b/test/module/shared_import/shared.wren new file mode 100644 index 00000000..cd67246c --- /dev/null +++ b/test/module/shared_import/shared.wren @@ -0,0 +1,3 @@ +// nontest +IO.print("shared") +var Shared = "shared" diff --git a/test/module/shared_import/shared_import.wren b/test/module/shared_import/shared_import.wren new file mode 100644 index 00000000..59b099be --- /dev/null +++ b/test/module/shared_import/shared_import.wren @@ -0,0 +1,12 @@ +var A = "a.wren".import_("A") +var B = "b.wren".import_("B") + +// Shared module should only run once: +// expect: a +// expect: shared +// expect: a done +// expect: b +// expect: b done + +IO.print(A) // expect: a shared +IO.print(B) // expect: b shared diff --git a/test/module/simple_import/module.wren b/test/module/simple_import/module.wren new file mode 100644 index 00000000..cf2ce362 --- /dev/null +++ b/test/module/simple_import/module.wren @@ -0,0 +1,3 @@ +// nontest +var Module = "from module" +IO.print("ran module") diff --git a/test/module/simple_import/simple_import.wren b/test/module/simple_import/simple_import.wren new file mode 100644 index 00000000..be5dc0d9 --- /dev/null +++ b/test/module/simple_import/simple_import.wren @@ -0,0 +1,4 @@ +var Module = "module.wren".import_("Module") +// expect: ran module + +IO.print(Module) // expect: from module diff --git a/test/module/unknown_module/unknown_module.wren b/test/module/unknown_module/unknown_module.wren new file mode 100644 index 00000000..2b6965f6 --- /dev/null +++ b/test/module/unknown_module/unknown_module.wren @@ -0,0 +1 @@ +var DoesNotExist = "does_not_exist.wren".import_("DoesNotExist") // expect runtime error: Could not find module 'does_not_exist.wren'. diff --git a/test/module/unknown_variable/module.wren b/test/module/unknown_variable/module.wren new file mode 100644 index 00000000..cf2ce362 --- /dev/null +++ b/test/module/unknown_variable/module.wren @@ -0,0 +1,3 @@ +// nontest +var Module = "from module" +IO.print("ran module") diff --git a/test/module/unknown_variable/unknown_variable.wren b/test/module/unknown_variable/unknown_variable.wren new file mode 100644 index 00000000..567f0b71 --- /dev/null +++ b/test/module/unknown_variable/unknown_variable.wren @@ -0,0 +1,3 @@ +// Should execute the module: +// expect: ran module +var DoesNotExist = "module.wren".import_("DoesNotExist") // expect runtime error: Could not find a variable named 'DoesNotExist' in module 'module.wren'.