From 8a71735e0f18b2f8f6022f5c8c8fe1044bbc741d Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Fri, 23 Mar 2018 07:54:09 -0700 Subject: [PATCH] Expose an API to let the host resolve relative import strings. This is a breaking API change: wrenInterpret() now takes an additional parameter for the module name to interpret the code in. --- src/cli/vm.c | 4 +- src/include/wren.h | 45 ++++++- src/optional/wren_opt_meta.c | 12 +- src/vm/wren_core.c | 2 +- src/vm/wren_vm.c | 41 +++++- test/api/benchmark.c | 2 +- test/api/main.c | 4 + test/api/new_vm.c | 2 +- test/api/resolution.c | 149 ++++++++++++++++++++++ test/api/resolution.h | 4 + test/api/resolution.wren | 39 ++++++ util/xcode/wren.xcodeproj/project.pbxproj | 6 + 12 files changed, 290 insertions(+), 20 deletions(-) create mode 100644 test/api/resolution.c create mode 100644 test/api/resolution.h create mode 100644 test/api/resolution.wren diff --git a/src/cli/vm.c b/src/cli/vm.c index e49fca74..05f2b2fd 100644 --- a/src/cli/vm.c +++ b/src/cli/vm.c @@ -228,7 +228,7 @@ void runFile(const char* path) initVM(); - WrenInterpretResult result = wrenInterpret(vm, source); + WrenInterpretResult result = wrenInterpret(vm, "main", source); if (afterLoadFn != NULL) afterLoadFn(vm); @@ -256,7 +256,7 @@ int runRepl() printf("\\\\/\"-\n"); printf(" \\_/ wren v%s\n", WREN_VERSION_STRING); - wrenInterpret(vm, "import \"repl\"\n"); + wrenInterpret(vm, "main", "import \"repl\"\n"); uv_run(loop, UV_RUN_DEFAULT); diff --git a/src/include/wren.h b/src/include/wren.h index 2c91afa1..09a3aa15 100644 --- a/src/include/wren.h +++ b/src/include/wren.h @@ -58,16 +58,21 @@ typedef void (*WrenForeignMethodFn)(WrenVM* vm); // collection. typedef void (*WrenFinalizerFn)(void* data); +// Gives the host a chance to canonicalize the imported module name, +// potentially taking into account the (previously resolved) name of the module +// that contains the import. Typically, this is used to implement relative +// imports. +typedef const char* (*WrenResolveModuleFn)(WrenVM* vm, + const char* importer, const char* name); + // Loads and returns the source code for the module [name]. typedef char* (*WrenLoadModuleFn)(WrenVM* vm, const char* name); // Returns a pointer to a foreign method on [className] in [module] with // [signature]. typedef WrenForeignMethodFn (*WrenBindForeignMethodFn)(WrenVM* vm, - const char* module, - const char* className, - bool isStatic, - const char* signature); + const char* module, const char* className, bool isStatic, + const char* signature); // Displays a string of text to the user. typedef void (*WrenWriteFn)(WrenVM* vm, const char* text); @@ -126,6 +131,32 @@ typedef struct // If `NULL`, defaults to a built-in function that uses `realloc` and `free`. WrenReallocateFn reallocateFn; + // The callback Wren uses to resolve a module name. + // + // Some host applications may wish to support "relative" imports, where the + // meaning of an import string depends on the module that contains it. To + // support that without baking any policy into Wren itself, the VM gives the + // host a chance to resolve an import string. + // + // Before an import is loaded, it calls this, passing in the name of the + // module that contains the import and the import string. The host app can + // look at both of those and produce a new "canonical" string that uniquely + // identifies the module. This string is then used as the name of the module + // going forward. It is what is passed to [loadModuleFn], how duplicate + // imports of the same module are detected, and how the module is reported in + // stack traces. + // + // If you leave this function NULL, then the original import string is + // treated as the resolved string. + // + // If an import cannot be resolved by the embedder, it should return NULL and + // Wren will report that as a runtime error. + // + // Wren will take ownership of the string you return and free it for you, so + // it should be allocated using the same allocation function you provide + // above. + WrenResolveModuleFn resolveModuleFn; + // The callback Wren uses to load a module. // // Since Wren does not talk directly to the file system, it relies on the @@ -200,7 +231,8 @@ typedef struct // // For example, say that this is 50. After a garbage collection, when there // are 400 bytes of memory still in use, the next collection will be triggered - // after a total of 600 bytes are allocated (including the 400 already in use.) + // after a total of 600 bytes are allocated (including the 400 already in + // use.) // // Setting this to a smaller number wastes less memory, but triggers more // frequent garbage collections. @@ -256,7 +288,8 @@ void wrenFreeVM(WrenVM* vm); void wrenCollectGarbage(WrenVM* vm); // Runs [source], a string of Wren source code in a new fiber in [vm]. -WrenInterpretResult wrenInterpret(WrenVM* vm, const char* source); +WrenInterpretResult wrenInterpret(WrenVM* vm, const char* module, + const char* source); // Creates a handle that can be used to invoke a method with [signature] on // using a receiver and arguments that are set up on the stack. diff --git a/src/optional/wren_opt_meta.c b/src/optional/wren_opt_meta.c index 80c28947..56b8fbff 100644 --- a/src/optional/wren_opt_meta.c +++ b/src/optional/wren_opt_meta.c @@ -14,9 +14,17 @@ void metaCompile(WrenVM* vm) bool printErrors = wrenGetSlotBool(vm, 3); // TODO: Allow passing in module? - ObjClosure* closure = wrenCompileSource(vm, "main", source, - isExpression, printErrors); + // Look up the module surrounding the callsite. This is brittle. The -2 walks + // up the callstack assuming that the meta module has one level of + // indirection before hitting the user's code. Any change to meta may require + // this constant to be tweaked. + ObjFiber* currentFiber = vm->fiber; + ObjFn* fn = currentFiber->frames[currentFiber->numFrames - 2].closure->fn; + ObjString* module = fn->module->name; + ObjClosure* closure = wrenCompileSource(vm, module->value, source, + isExpression, printErrors); + // Return the result. We can't use the public API for this since we have a // bare ObjClosure*. if (closure == NULL) diff --git a/src/vm/wren_core.c b/src/vm/wren_core.c index a827b057..0debbe51 100644 --- a/src/vm/wren_core.c +++ b/src/vm/wren_core.c @@ -1181,7 +1181,7 @@ void wrenInitializeCore(WrenVM* vm) // '---------' '-------------------' -' // The rest of the classes can now be defined normally. - wrenInterpretInModule(vm, NULL, coreModuleSource); + wrenInterpret(vm, NULL, coreModuleSource); vm->boolClass = AS_CLASS(wrenFindVariable(vm, coreModule, "Bool")); PRIMITIVE(vm->boolClass, "toString", bool_toString); diff --git a/src/vm/wren_vm.c b/src/vm/wren_vm.c index d1eeb7cc..0afb9123 100644 --- a/src/vm/wren_vm.c +++ b/src/vm/wren_vm.c @@ -38,6 +38,7 @@ static void* defaultReallocate(void* ptr, size_t newSize) void wrenInitConfiguration(WrenConfiguration* config) { config->reallocateFn = defaultReallocate; + config->resolveModuleFn = NULL; config->loadModuleFn = NULL; config->bindForeignMethodFn = NULL; config->bindForeignClassFn = NULL; @@ -693,8 +694,39 @@ void wrenFinalizeForeign(WrenVM* vm, ObjForeign* foreign) finalizer(foreign->data); } +// Let the host resolve an imported module name if it wants to. +static Value resolveModule(WrenVM* vm, Value name) +{ + // If the host doesn't care to resolve, leave the name alone. + if (vm->config.resolveModuleFn == NULL) return name; + + ObjFiber* fiber = vm->fiber; + ObjFn* fn = fiber->frames[fiber->numFrames - 1].closure->fn; + ObjString* importer = fn->module->name; + + const char* resolved = vm->config.resolveModuleFn(vm, importer->value, + AS_CSTRING(name)); + if (resolved == NULL) + { + vm->fiber->error = wrenStringFormat(vm, + "Could not resolve module '@' imported from '@'.", + name, OBJ_VAL(importer)); + return NULL_VAL; + } + + // If they resolved to the exact same string, we don't need to copy it. + if (resolved == AS_CSTRING(name)) return name; + + // Copy the string into a Wren String object. + name = wrenNewString(vm, resolved); + DEALLOCATE(vm, (char*)resolved); + return name; +} + Value wrenImportModule(WrenVM* vm, Value name) { + name = resolveModule(vm, name); + // If the module is already loaded, we don't need to do anything. Value existing = wrenMapGet(vm->modules, name); if (!IS_UNDEFINED(existing)) return existing; @@ -1436,13 +1468,8 @@ void wrenReleaseHandle(WrenVM* vm, WrenHandle* handle) DEALLOCATE(vm, handle); } -WrenInterpretResult wrenInterpret(WrenVM* vm, const char* source) -{ - return wrenInterpretInModule(vm, "main", source); -} - -WrenInterpretResult wrenInterpretInModule(WrenVM* vm, const char* module, - const char* source) +WrenInterpretResult wrenInterpret(WrenVM* vm, const char* module, + const char* source) { ObjClosure* closure = wrenCompileSource(vm, module, source, false, true); if (closure == NULL) return WREN_RESULT_COMPILE_ERROR; diff --git a/test/api/benchmark.c b/test/api/benchmark.c index 34f60900..494de450 100644 --- a/test/api/benchmark.c +++ b/test/api/benchmark.c @@ -30,7 +30,7 @@ static void call(WrenVM* vm) wrenInitConfiguration(&config); WrenVM* otherVM = wrenNewVM(&config); - wrenInterpret(otherVM, testScript); + wrenInterpret(otherVM, "main", testScript); WrenHandle* method = wrenMakeCallHandle(otherVM, "method(_,_,_,_)"); diff --git a/test/api/main.c b/test/api/main.c index 5b6bc410..ed3510d2 100644 --- a/test/api/main.c +++ b/test/api/main.c @@ -14,6 +14,7 @@ #include "new_vm.h" #include "reset_stack_after_call_abort.h" #include "reset_stack_after_foreign_construct.h" +#include "resolution.h" #include "slots.h" #include "user_data.h" @@ -58,6 +59,9 @@ static WrenForeignMethodFn bindForeignMethod( method = newVMBindMethod(fullName); if (method != NULL) return method; + method = resolutionBindMethod(fullName); + if (method != NULL) return method; + method = slotsBindMethod(fullName); if (method != NULL) return method; diff --git a/test/api/new_vm.c b/test/api/new_vm.c index 4901a8c5..568c06b9 100644 --- a/test/api/new_vm.c +++ b/test/api/new_vm.c @@ -7,7 +7,7 @@ static void nullConfig(WrenVM* vm) WrenVM* otherVM = wrenNewVM(NULL); // We should be able to execute code. - WrenInterpretResult result = wrenInterpret(otherVM, "1 + 2"); + WrenInterpretResult result = wrenInterpret(otherVM, "main", "1 + 2"); wrenSetSlotBool(vm, 0, result == WREN_RESULT_SUCCESS); wrenFreeVM(otherVM); diff --git a/test/api/resolution.c b/test/api/resolution.c new file mode 100644 index 00000000..937be866 --- /dev/null +++ b/test/api/resolution.c @@ -0,0 +1,149 @@ +#include +#include + +#include "resolution.h" + +static void write(WrenVM* vm, const char* text) +{ + printf("%s", text); +} + +static void reportError(WrenVM* vm, WrenErrorType type, + const char* module, int line, const char* message) +{ + if (type == WREN_ERROR_RUNTIME) printf("%s\n", message); +} + +static char* loadModule(WrenVM* vm, const char* module) +{ + printf("loading %s\n", module); + + const char* source; + if (strcmp(module, "main/baz/bang") == 0) + { + source = "import \"foo|bar\""; + } + else + { + source = "System.print(\"ok\")"; + } + + char* string = malloc(strlen(source) + 1); + strcpy(string, source); + return string; +} + +static void runTestVM(WrenVM* vm, WrenConfiguration* configuration, + const char* source) +{ + configuration->writeFn = write; + configuration->errorFn = reportError; + configuration->loadModuleFn = loadModule; + + WrenVM* otherVM = wrenNewVM(configuration); + + // We should be able to execute code. + WrenInterpretResult result = wrenInterpret(otherVM, "main", source); + if (result != WREN_RESULT_SUCCESS) + { + wrenSetSlotString(vm, 0, "error"); + } + else + { + wrenSetSlotString(vm, 0, "success"); + } + + wrenFreeVM(otherVM); +} + +static void noResolver(WrenVM* vm) +{ + WrenConfiguration configuration; + wrenInitConfiguration(&configuration); + + // Should default to no resolution function. + if (configuration.resolveModuleFn != NULL) + { + wrenSetSlotString(vm, 0, "Did not have null resolve function."); + return; + } + + runTestVM(vm, &configuration, "import \"foo/bar\""); +} + +static const char* resolveToNull(WrenVM* vm, const char* importer, + const char* name) +{ + return NULL; +} + +static void returnsNull(WrenVM* vm) +{ + WrenConfiguration configuration; + wrenInitConfiguration(&configuration); + + configuration.resolveModuleFn = resolveToNull; + runTestVM(vm, &configuration, "import \"foo/bar\""); +} + +static const char* resolveChange(WrenVM* vm, const char* importer, + const char* name) +{ + // Concatenate importer and name. + size_t length = strlen(importer) + 1 + strlen(name) + 1; + char* result = malloc(length); + strcpy(result, importer); + strcat(result, "/"); + strcat(result, name); + + // Replace "|" with "/". + for (size_t i = 0; i < length; i++) + { + if (result[i] == '|') result[i] = '/'; + } + + return result; +} + +static void changesString(WrenVM* vm) +{ + WrenConfiguration configuration; + wrenInitConfiguration(&configuration); + + configuration.resolveModuleFn = resolveChange; + runTestVM(vm, &configuration, "import \"foo|bar\""); +} + +static void shared(WrenVM* vm) +{ + WrenConfiguration configuration; + wrenInitConfiguration(&configuration); + + configuration.resolveModuleFn = resolveChange; + runTestVM(vm, &configuration, "import \"foo|bar\"\nimport \"foo/bar\""); +} + +static void importer(WrenVM* vm) +{ + WrenConfiguration configuration; + wrenInitConfiguration(&configuration); + + configuration.resolveModuleFn = resolveChange; + runTestVM(vm, &configuration, "import \"baz|bang\""); +} + +WrenForeignMethodFn resolutionBindMethod(const char* signature) +{ + if (strcmp(signature, "static Resolution.noResolver()") == 0) return noResolver; + if (strcmp(signature, "static Resolution.returnsNull()") == 0) return returnsNull; + if (strcmp(signature, "static Resolution.changesString()") == 0) return changesString; + if (strcmp(signature, "static Resolution.shared()") == 0) return shared; + if (strcmp(signature, "static Resolution.importer()") == 0) return importer; + + return NULL; +} + +void resolutionBindClass(const char* className, WrenForeignClassMethods* methods) +{ +// methods->allocate = foreignClassAllocate; +} diff --git a/test/api/resolution.h b/test/api/resolution.h new file mode 100644 index 00000000..2ad0ca79 --- /dev/null +++ b/test/api/resolution.h @@ -0,0 +1,4 @@ +#include "wren.h" + +WrenForeignMethodFn resolutionBindMethod(const char* signature); +void resolutionBindClass(const char* className, WrenForeignClassMethods* methods); diff --git a/test/api/resolution.wren b/test/api/resolution.wren new file mode 100644 index 00000000..c03b50db --- /dev/null +++ b/test/api/resolution.wren @@ -0,0 +1,39 @@ +class Resolution { + foreign static noResolver() + foreign static returnsNull() + foreign static changesString() + foreign static shared() + foreign static importer() +} + +// If no resolver function is configured, the default resolver just passes +// along the import string unchanged. +System.print(Resolution.noResolver()) +// expect: loading foo/bar +// expect: ok +// expect: success + +// If the resolver returns NULL, it's reported as an error. +System.print(Resolution.returnsNull()) +// expect: Could not resolve module 'foo/bar' imported from 'main'. +// expect: error + +// The resolver function can change the string. +System.print(Resolution.changesString()) +// expect: loading main/foo/bar +// expect: ok +// expect: success + +// Imports both "foo/bar" and "foo|bar", but only loads the module once because +// they resolve to the same module. +System.print(Resolution.shared()) +// expect: loading main/foo/bar +// expect: ok +// expect: success + +// The string passed as importer is the resolver string of the importing module. +System.print(Resolution.importer()) +// expect: loading main/baz/bang +// expect: loading main/baz/bang/foo/bar +// expect: ok +// expect: success diff --git a/util/xcode/wren.xcodeproj/project.pbxproj b/util/xcode/wren.xcodeproj/project.pbxproj index 0da22634..2ac08e76 100644 --- a/util/xcode/wren.xcodeproj/project.pbxproj +++ b/util/xcode/wren.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 293B25591CEFD8C7005D9537 /* repl.wren.inc in Sources */ = {isa = PBXBuildFile; fileRef = 293B25561CEFD8C7005D9537 /* repl.wren.inc */; }; 293B255A1CEFD8C7005D9537 /* repl.wren.inc in Sources */ = {isa = PBXBuildFile; fileRef = 293B25561CEFD8C7005D9537 /* repl.wren.inc */; }; 293D46961BB43F9900200083 /* call.c in Sources */ = {isa = PBXBuildFile; fileRef = 293D46941BB43F9900200083 /* call.c */; }; + 2940E98D2063EC030054503C /* resolution.c in Sources */ = {isa = PBXBuildFile; fileRef = 2940E98B2063EC020054503C /* resolution.c */; }; 2949AA8D1C2F14F000B106BA /* get_variable.c in Sources */ = {isa = PBXBuildFile; fileRef = 2949AA8B1C2F14F000B106BA /* get_variable.c */; }; 29512C811B91F8EB008C10E6 /* libuv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 29512C801B91F8EB008C10E6 /* libuv.a */; }; 29512C821B91F901008C10E6 /* libuv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 29512C801B91F8EB008C10E6 /* libuv.a */; }; @@ -115,6 +116,8 @@ 293B25561CEFD8C7005D9537 /* repl.wren.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = repl.wren.inc; path = ../../src/module/repl.wren.inc; sourceTree = ""; }; 293D46941BB43F9900200083 /* call.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = call.c; path = ../../test/api/call.c; sourceTree = ""; }; 293D46951BB43F9900200083 /* call.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = call.h; path = ../../test/api/call.h; sourceTree = ""; }; + 2940E98B2063EC020054503C /* resolution.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = resolution.c; path = ../../test/api/resolution.c; sourceTree = ""; }; + 2940E98C2063EC020054503C /* resolution.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = resolution.h; path = ../../test/api/resolution.h; sourceTree = ""; }; 2949AA8B1C2F14F000B106BA /* get_variable.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = get_variable.c; path = ../../test/api/get_variable.c; sourceTree = ""; }; 2949AA8C1C2F14F000B106BA /* get_variable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = get_variable.h; path = ../../test/api/get_variable.h; sourceTree = ""; }; 29512C7F1B91F86E008C10E6 /* api_test */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = api_test; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -304,6 +307,8 @@ 29D880651DC8ECF600025364 /* reset_stack_after_call_abort.h */, 29C80D581D73332A00493837 /* reset_stack_after_foreign_construct.c */, 29C80D591D73332A00493837 /* reset_stack_after_foreign_construct.h */, + 2940E98B2063EC020054503C /* resolution.c */, + 2940E98C2063EC020054503C /* resolution.h */, 29D009AA1B7E39A8000CE58C /* slots.c */, 29D009AB1B7E39A8000CE58C /* slots.h */, 29D24DB01E82C0A2006618CC /* user_data.c */, @@ -394,6 +399,7 @@ 29205C9A1AB4E6430073018D /* wren_core.c in Sources */, 2901D7641B74F4050083A2C8 /* timer.c in Sources */, 29729F331BA70A620099CA20 /* io.wren.inc in Sources */, + 2940E98D2063EC030054503C /* resolution.c in Sources */, 29C8A9331AB71FFF00DEC81D /* vm.c in Sources */, 291647C41BA5EA45006142EE /* scheduler.c in Sources */, 29A427341BDBE435001E6E22 /* wren_opt_meta.c in Sources */,