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'.