feat: add WrenResolveModuleFn callback and module name parameter to wrenInterpret for relative import resolution

This breaking API change introduces a new `WrenResolveModuleFn` callback in the configuration that allows the host to canonicalize import module names, enabling relative imports. The `wrenInterpret()` function now requires a `module` parameter to specify the module name for the code being interpreted. The `wrenInterpretInModule()` function is removed in favor of the updated `wrenInterpret()`. The `metaCompile` function now dynamically looks up the caller's module name instead of hardcoding "main". New test files `resolution.c`, `resolution.h`, and `resolution.wren` are added to verify resolver behavior including null default, NULL return error, string rewriting, shared module deduplication, and importer propagation.
This commit is contained in:
Bob Nystrom
2018-03-23 14:54:09 +00:00
parent 88ad0fc138
commit 761032c0fc
12 changed files with 290 additions and 20 deletions
+1 -1
View File
@@ -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(_,_,_,_)");
+4
View File
@@ -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;
+1 -1
View File
@@ -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);
+149
View File
@@ -0,0 +1,149 @@
#include <stdio.h>
#include <string.h>
#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;
}
+4
View File
@@ -0,0 +1,4 @@
#include "wren.h"
WrenForeignMethodFn resolutionBindMethod(const char* signature);
void resolutionBindClass(const char* className, WrenForeignClassMethods* methods);
+39
View File
@@ -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