From c5befa72cfda8c8e35d405946ae33cc95121e206 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Tue, 20 Mar 2018 06:54:51 -0700 Subject: [PATCH 1/5] Don't use module import string when loading imported variables. This is an interim step towards supporting relative imports. Previously, the IMPORT_VARIABLE instruction had a constant string operand for the import string of the module to import the variable from. However, with relative imports, the import string needs to be resolved by the host all into a canonical import string. At that point, the original import string in the source is no longer useful. This changes that to have IMPORT_VARIABLE access the imported ObjModule directly. It works in two pieces: 1. When a module is compiled, it ends with an END_MODULE instruction. That instruction stores the current ObjModule in vm->lastModule. 2. The IMPORT_VARIABLE instruction uses vm->lastModule as the module to load the variable from. Since no interesting code can execute between when a module body completes and the subsequent IMPORT_VARIABLE statements, we know vm->lastModule will be the one we imported. --- src/optional/wren_opt_meta.c | 3 +- src/vm/wren_compiler.c | 32 +++++++++--------- src/vm/wren_debug.c | 11 +++--- src/vm/wren_opcodes.h | 11 ++++-- src/vm/wren_primitive.c | 3 +- src/vm/wren_vm.c | 65 +++++++++++++++++++++++------------- src/vm/wren_vm.h | 6 ++++ test/api/main.c | 3 +- 8 files changed, 84 insertions(+), 50 deletions(-) diff --git a/src/optional/wren_opt_meta.c b/src/optional/wren_opt_meta.c index 504543be..80c28947 100644 --- a/src/optional/wren_opt_meta.c +++ b/src/optional/wren_opt_meta.c @@ -29,7 +29,8 @@ void metaCompile(WrenVM* vm) } } -void metaGetModuleVariables(WrenVM* vm) { +void metaGetModuleVariables(WrenVM* vm) +{ wrenEnsureSlots(vm, 3); Value moduleValue = wrenMapGet(vm->modules, vm->apiStack[1]); diff --git a/src/vm/wren_compiler.c b/src/vm/wren_compiler.c index 0c18f8b0..e7de5038 100644 --- a/src/vm/wren_compiler.c +++ b/src/vm/wren_compiler.c @@ -1609,9 +1609,7 @@ static void patchJump(Compiler* compiler, int offset) static bool finishBlock(Compiler* compiler) { // Empty blocks do nothing. - if (match(compiler, TOKEN_RIGHT_BRACE)) { - return false; - } + if (match(compiler, TOKEN_RIGHT_BRACE)) return false; // If there's no line after the "{", it's a single-expression body. if (!matchLine(compiler)) @@ -1622,9 +1620,7 @@ static bool finishBlock(Compiler* compiler) } // Empty blocks (with just a newline inside) do nothing. - if (match(compiler, TOKEN_RIGHT_BRACE)) { - return false; - } + if (match(compiler, TOKEN_RIGHT_BRACE)) return false; // Compile the definition list. do @@ -2719,6 +2715,7 @@ static int getNumArguments(const uint8_t* bytecode, const Value* constants, case CODE_CONSTRUCT: case CODE_FOREIGN_CONSTRUCT: case CODE_FOREIGN_CLASS: + case CODE_END_MODULE: return 0; case CODE_LOAD_LOCAL: @@ -3321,9 +3318,13 @@ static void classDefinition(Compiler* compiler, bool isForeign) // import "foo" for Bar, Baz // // We compile a single IMPORT_MODULE "foo" instruction to load the module -// itself. Then, for each imported name, we declare a variable and them emit a -// IMPORT_VARIABLE instruction to load the variable from the other module and -// assign it to the new variable in this one. +// itself. When that finishes executing the imported module, it leaves the +// ObjModule in vm->lastModule. Then, for Bar and Baz, we: +// +// * Declare a variable in the current scope with that name. +// * Emit an IMPORT_VARIABLE instruction to load the variable's value from the +// other module. +// * Compile the code to store that value in the variable in this scope. static void import(Compiler* compiler) { ignoreNewlines(compiler); @@ -3333,9 +3334,9 @@ static void import(Compiler* compiler) // Load the module. emitShortArg(compiler, CODE_IMPORT_MODULE, moduleConstant); - // Discard the unused result value from calling the module's fiber. + // Discard the unused result value from calling the module body's closure. emitOp(compiler, CODE_POP); - + // The for clause is optional. if (!match(compiler, TOKEN_FOR)) return; @@ -3344,16 +3345,15 @@ static void import(Compiler* compiler) { ignoreNewlines(compiler); int slot = declareNamedVariable(compiler); - + // Define a string constant for the variable name. int variableConstant = addConstant(compiler, wrenNewStringLength(compiler->parser->vm, compiler->parser->previous.start, compiler->parser->previous.length)); - + // Load the variable from the other module. - emitShortArg(compiler, CODE_IMPORT_VARIABLE, moduleConstant); - emitShort(compiler, variableConstant); + emitShortArg(compiler, CODE_IMPORT_VARIABLE, variableConstant); // Store the result in the variable here. defineVariable(compiler, slot); @@ -3466,7 +3466,7 @@ ObjFn* wrenCompile(WrenVM* vm, ObjModule* module, const char* source, } } - emitOp(&compiler, CODE_NULL); + emitOp(&compiler, CODE_END_MODULE); } emitOp(&compiler, CODE_RETURN); diff --git a/src/vm/wren_debug.c b/src/vm/wren_debug.c index f04d9fe6..692839fd 100644 --- a/src/vm/wren_debug.c +++ b/src/vm/wren_debug.c @@ -313,6 +313,10 @@ static int dumpInstruction(WrenVM* vm, ObjFn* fn, int i, int* lastLine) break; } + case CODE_END_MODULE: + printf("END_MODULE\n"); + break; + case CODE_IMPORT_MODULE: { int name = READ_SHORT(); @@ -324,16 +328,13 @@ static int dumpInstruction(WrenVM* vm, ObjFn* fn, int i, int* lastLine) case CODE_IMPORT_VARIABLE: { - int module = READ_SHORT(); int variable = READ_SHORT(); - printf("%-16s %5d '", "IMPORT_VARIABLE", module); - wrenDumpValue(fn->constants.data[module]); - printf("' %5d '", variable); + printf("%-16s %5d '", "IMPORT_VARIABLE", variable); wrenDumpValue(fn->constants.data[variable]); printf("'\n"); break; } - + case CODE_END: printf("END\n"); break; diff --git a/src/vm/wren_opcodes.h b/src/vm/wren_opcodes.h index 0af0c07e..0fa7f781 100644 --- a/src/vm/wren_opcodes.h +++ b/src/vm/wren_opcodes.h @@ -190,6 +190,11 @@ OPCODE(METHOD_INSTANCE, -2) // closure. OPCODE(METHOD_STATIC, -2) +// This is executed at the end of the module's body. Pushes NULL onto the stack +// as the "return value" of the import statement and stores the module as the +// most recently imported one. +OPCODE(END_MODULE, 1) + // Import a module whose name is the string stored at [arg] in the constant // table. // @@ -198,9 +203,9 @@ OPCODE(METHOD_STATIC, -2) // value when resuming a caller.) OPCODE(IMPORT_MODULE, 1) -// Import a variable from a previously-imported module. The module's name is at -// [arg1] in the constant table and the variable name is at [arg2]. Pushes the -// loaded variable onto the stack. +// Import a variable from the most recently imported module. The name of the +// variable to import is at [arg] in the constant table. Pushes the loaded +// variable's value. OPCODE(IMPORT_VARIABLE, 1) // This pseudo-instruction indicates the end of the bytecode. It should diff --git a/src/vm/wren_primitive.c b/src/vm/wren_primitive.c index 34e68581..17b045c1 100644 --- a/src/vm/wren_primitive.c +++ b/src/vm/wren_primitive.c @@ -80,7 +80,8 @@ uint32_t calculateRange(WrenVM* vm, ObjRange* range, uint32_t* length, // list[0..-1] and list[0...list.count] can be used to copy a list even when // empty. if (range->from == *length && - range->to == (range->isInclusive ? -1.0 : (double)*length)) { + range->to == (range->isInclusive ? -1.0 : (double)*length)) + { *length = 0; return 0; } diff --git a/src/vm/wren_vm.c b/src/vm/wren_vm.c index 712dfd81..d1eeb7cc 100644 --- a/src/vm/wren_vm.c +++ b/src/vm/wren_vm.c @@ -696,7 +696,8 @@ void wrenFinalizeForeign(WrenVM* vm, ObjForeign* foreign) Value wrenImportModule(WrenVM* vm, Value name) { // If the module is already loaded, we don't need to do anything. - if (!IS_UNDEFINED(wrenMapGet(vm->modules, name))) return NULL_VAL; + Value existing = wrenMapGet(vm->modules, name); + if (!IS_UNDEFINED(existing)) return existing; const char* source = NULL; bool allocatedSource = true; @@ -747,6 +748,26 @@ Value wrenImportModule(WrenVM* vm, Value name) return OBJ_VAL(moduleClosure); } +static Value getModuleVariable(WrenVM* vm, ObjModule* module, + Value variableName) +{ + ObjString* variable = AS_STRING(variableName); + uint32_t variableEntry = wrenSymbolTableFind(&module->variableNames, + variable->value, + variable->length); + + // It's a runtime error if the imported variable does not exist. + if (variableEntry != UINT32_MAX) + { + return module->variables.data[variableEntry]; + } + + vm->fiber->error = wrenStringFormat(vm, + "Could not find a variable named '@' in module '@'.", + variableName, OBJ_VAL(module->name)); + return NULL_VAL; +} + // The main bytecode interpreter loop. This is where the magic happens. It is // also, as you can imagine, highly performance critical. Returns `true` if the // fiber completed without error. @@ -1244,17 +1265,24 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) DISPATCH(); } + CASE_CODE(END_MODULE): + { + vm->lastModule = fn->module; + PUSH(NULL_VAL); + DISPATCH(); + } + CASE_CODE(IMPORT_MODULE): { Value name = fn->constants.data[READ_SHORT()]; - // Make a slot on the stack for the module's fiber to place the return - // value. It will be popped after this fiber is resumed. - PUSH(NULL_VAL); - Value result = wrenImportModule(vm, name); if (!IS_NULL(fiber->error)) RUNTIME_ERROR(); + // Make a slot on the stack for the module's closure to place the return + // value. It will be popped after the module body code returns. + PUSH(NULL_VAL); + // If we get a closure, call it to execute the module body. if (IS_CLOSURE(result)) { @@ -1263,16 +1291,21 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) callFunction(vm, fiber, closure, 1); LOAD_FRAME(); } + else + { + // The module has already been loaded. Remember it so we can import + // variables from it if needed. + vm->lastModule = AS_MODULE(result); + } DISPATCH(); } CASE_CODE(IMPORT_VARIABLE): { - Value module = fn->constants.data[READ_SHORT()]; Value variable = fn->constants.data[READ_SHORT()]; - - Value result = wrenGetModuleVariable(vm, module, variable); + ASSERT(vm->lastModule != NULL, "Should have already imported module."); + Value result = getModuleVariable(vm, vm->lastModule, variable); if (!IS_NULL(fiber->error)) RUNTIME_ERROR(); PUSH(result); @@ -1448,21 +1481,7 @@ Value wrenGetModuleVariable(WrenVM* vm, Value moduleName, Value variableName) return NULL_VAL; } - ObjString* variable = AS_STRING(variableName); - uint32_t variableEntry = wrenSymbolTableFind(&module->variableNames, - variable->value, - variable->length); - - // It's a runtime error if the imported variable does not exist. - if (variableEntry != UINT32_MAX) - { - return module->variables.data[variableEntry]; - } - - vm->fiber->error = wrenStringFormat(vm, - "Could not find a variable named '@' in module '@'.", - variableName, moduleName); - return NULL_VAL; + return getModuleVariable(vm, module, variableName); } Value wrenFindVariable(WrenVM* vm, ObjModule* module, const char* name) diff --git a/src/vm/wren_vm.h b/src/vm/wren_vm.h index 617f62b0..3cfaeee7 100644 --- a/src/vm/wren_vm.h +++ b/src/vm/wren_vm.h @@ -49,6 +49,12 @@ struct WrenVM // whose key is null) for the module's name and the value is the ObjModule // for the module. ObjMap* modules; + + // The most recently imported module. More specifically, the module whose + // code has most recently finished executing. + // + // Not treated like a GC root since the module is already in [modules]. + ObjModule* lastModule; // Memory management data: diff --git a/test/api/main.c b/test/api/main.c index 1557dd94..5b6bc410 100644 --- a/test/api/main.c +++ b/test/api/main.c @@ -91,7 +91,8 @@ static WrenForeignClassMethods bindForeignClass( return methods; } -static void afterLoad(WrenVM* vm) { +static void afterLoad(WrenVM* vm) +{ if (strstr(testName, "/call.wren") != NULL) { callRunTests(vm); From 8a71735e0f18b2f8f6022f5c8c8fe1044bbc741d Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Fri, 23 Mar 2018 07:54:09 -0700 Subject: [PATCH 2/5] 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 */, From 5539c5975097961147fdb8d4bc0a5ae3eda0dc70 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Sat, 24 Mar 2018 10:52:16 -0700 Subject: [PATCH 3/5] Add a minimal path manipulation C module. This is just for the VM's own internal use, for resolving relative imports. Also added a tiny unit test framework for writing tests of low-level C functionality that isn't exposed directly by the language or VM. --- Makefile | 26 ++- src/cli/path.c | 254 ++++++++++++++++++++++ src/cli/path.h | 52 +++++ test/unit/main.c | 9 + test/unit/path_test.c | 105 +++++++++ test/unit/path_test.h | 1 + test/unit/test.c | 29 +++ test/unit/test.h | 7 + util/benchmark.py | 2 +- util/test.py | 2 +- util/wren.mk | 57 +++-- util/xcode/wren.xcodeproj/project.pbxproj | 128 +++++++++++ 12 files changed, 642 insertions(+), 30 deletions(-) create mode 100644 src/cli/path.c create mode 100644 src/cli/path.h create mode 100644 test/unit/main.c create mode 100644 test/unit/path_test.c create mode 100644 test/unit/path_test.h create mode 100644 test/unit/test.c create mode 100644 test/unit/test.h diff --git a/Makefile b/Makefile index f952c283..68b1d8f6 100644 --- a/Makefile +++ b/Makefile @@ -49,23 +49,23 @@ all: debug release ci: ci_32 ci_64 ci_32: - $(V) $(MAKE) -f util/wren.mk MODE=debug LANG=c ARCH=32 vm cli test + $(V) $(MAKE) -f util/wren.mk MODE=debug LANG=c ARCH=32 vm cli api_test $(V) ./util/test.py --suffix=d-32 $(suite) - $(V) $(MAKE) -f util/wren.mk MODE=debug LANG=cpp ARCH=32 vm cli test + $(V) $(MAKE) -f util/wren.mk MODE=debug LANG=cpp ARCH=32 vm cli api_test $(V) ./util/test.py --suffix=d-cpp-32 $(suite) - $(V) $(MAKE) -f util/wren.mk MODE=release LANG=c ARCH=32 vm cli test + $(V) $(MAKE) -f util/wren.mk MODE=release LANG=c ARCH=32 vm cli api_test $(V) ./util/test.py --suffix=-32 $(suite) - $(V) $(MAKE) -f util/wren.mk MODE=release LANG=cpp ARCH=32 vm cli test + $(V) $(MAKE) -f util/wren.mk MODE=release LANG=cpp ARCH=32 vm cli api_test $(V) ./util/test.py --suffix=-cpp-32 $(suite) ci_64: - $(V) $(MAKE) -f util/wren.mk MODE=debug LANG=c ARCH=64 vm cli test + $(V) $(MAKE) -f util/wren.mk MODE=debug LANG=c ARCH=64 vm cli api_test $(V) ./util/test.py --suffix=d-64 $(suite) - $(V) $(MAKE) -f util/wren.mk MODE=debug LANG=cpp ARCH=64 vm cli test + $(V) $(MAKE) -f util/wren.mk MODE=debug LANG=cpp ARCH=64 vm cli api_test $(V) ./util/test.py --suffix=d-cpp-64 $(suite) - $(V) $(MAKE) -f util/wren.mk MODE=release LANG=c ARCH=64 vm cli test + $(V) $(MAKE) -f util/wren.mk MODE=release LANG=c ARCH=64 vm cli api_test $(V) ./util/test.py --suffix=-64 $(suite) - $(V) $(MAKE) -f util/wren.mk MODE=release LANG=cpp ARCH=64 vm cli test + $(V) $(MAKE) -f util/wren.mk MODE=release LANG=cpp ARCH=64 vm cli api_test $(V) ./util/test.py --suffix=-cpp-64 $(suite) # Remove all build outputs and intermediate files. Does not remove downloaded @@ -77,13 +77,17 @@ clean: # Run the tests against the debug build of Wren. test: debug - $(V) $(MAKE) -f util/wren.mk MODE=debug test + $(V) $(MAKE) -f util/wren.mk MODE=debug api_test $(V) ./util/test.py $(suite) benchmark: release - $(V) $(MAKE) -f util/wren.mk test + $(V) $(MAKE) -f util/wren.mk api_test $(V) ./util/benchmark.py -l wren $(suite) +unit_test: + $(V) $(MAKE) -f util/wren.mk MODE=debug unit_test + $(V) ./build/debug/test/unit_wrend + # Generate the Wren site. docs: $(V) ./util/generate_docs.py @@ -104,4 +108,4 @@ gh-pages: docs amalgamation: src/include/wren.h src/vm/*.h src/vm/*.c ./util/generate_amalgamation.py > build/wren.c -.PHONY: all amalgamation builtin clean debug docs gh-pages release test vm watchdocs ci ci_32 ci_64 +.PHONY: all amalgamation benchmark builtin clean debug docs gh-pages release test vm watchdocs ci ci_32 ci_64 diff --git a/src/cli/path.c b/src/cli/path.c new file mode 100644 index 00000000..a3362a3a --- /dev/null +++ b/src/cli/path.c @@ -0,0 +1,254 @@ +#include +#include +#include +#include + +#include "path.h" + +// The maximum number of components in a path. We can't normalize a path that +// contains more than this number of parts. The number here assumes a max path +// length of 4096, which is common on Linux, and then assumes each component is +// at least two characters, "/", and a single-letter directory name. +#define MAX_COMPONENTS 2048 + +typedef struct { + const char* start; + const char* end; +} Slice; + +static void ensureCapacity(Path* path, size_t capacity) +{ + // Capacity always needs to be one greater than the actual length to have + // room for the null byte, which is stored in the buffer, but not counted in + // the length. A zero-character path still needs a one-character array to + // store the '\0'. + capacity++; + + if (path->capacity >= capacity) return; + + // Grow by doubling in size. + size_t newCapacity = 16; + while (newCapacity < capacity) newCapacity *= 2; + + path->chars = (char*)realloc(path->chars, newCapacity); + path->capacity = newCapacity; +} + +static void appendSlice(Path* path, Slice slice) +{ + size_t length = slice.end - slice.start; + ensureCapacity(path, path->length + length); + memcpy(path->chars + path->length, slice.start, length); + path->length += length; + path->chars[path->length] = '\0'; +} + +Path* pathNew(const char* string) +{ + Path* path = (Path*)malloc(sizeof(Path)); + path->chars = (char*)malloc(1); + path->chars[0] = '\0'; + path->length = 0; + path->capacity = 0; + + pathAppendString(path, string); + + return path; +} + +void pathFree(Path* path) +{ + if (path->chars) free(path->chars); + free(path); +} + +void pathDirName(Path* path) +{ + // Find the last path separator. + for (size_t i = path->length - 1; i < path->length; i--) + { + if (path->chars[i] == '/') + { + path->length = i; + path->chars[i] = '\0'; + return; + } + } + + // If we got here, there was no separator so it must be a single component. + path->length = 0; + path->chars[0] = '\0'; +} + +void pathRemoveExtension(Path* path) +{ + for (size_t i = path->length - 1; i < path->length; i--) + { + // If we hit a path separator before finding the extension, then the last + // component doesn't have one. + if (path->chars[i] == '/') return; + + if (path->chars[i] == '.') + { + path->length = i; + path->chars[path->length] = '\0'; + } + } +} + +void pathJoin(Path* path, const char* string) +{ + if (path->length > 0 && path->chars[path->length - 1] != '/') + { + pathAppendChar(path, '/'); + } + + pathAppendString(path, string); +} + + +#ifdef _WIN32 +static bool isDriveLetter(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} +#endif + +bool pathIsAbsolute(Path* path) +{ +#ifdef _WIN32 + // Absolute path in the current drive. + if (path->length >= 1 && path->chars[0] == '\\') return true; + + // Drive letter. + if (path->length >= 3 && + isDriveLetter(path->chars[0]) && + path->chars[1] == ':' && + path->chars[2] == '\\') + { + return true; + } + + // UNC path. + return path->length >= 2 && path->chars[0] == '\\' && path->chars[1] == '\\'; +#else + // Otherwise, assume POSIX-style paths. + return path->length >= 1 && path->chars[0] == '/'; +#endif +} + +void pathAppendChar(Path* path, char c) +{ + ensureCapacity(path, path->length + 1); + path->chars[path->length++] = c; + path->chars[path->length] = '\0'; +} + +void pathAppendString(Path* path, const char* string) +{ + Slice slice; + slice.start = string; + slice.end = string + strlen(string); + appendSlice(path, slice); +} + +Path* pathNormalize(Path* path) +{ + Path* result = pathNew(""); + + // Split the path into components. + Slice components[MAX_COMPONENTS]; + int numComponents = 0; + + char* start = path->chars; + char* end = path->chars; + + // Split into parts and handle "." and "..". + int leadingDoubles = 0; + for (;;) + { + if (*end == '\0' || *end == '/') + { + // Add the current component. + if (start != end) + { + size_t length = end - start; + if (length == 1 && start[0] == '.') + { + // Skip "." components. + } + else if (length == 2 && start[0] == '.' && start[1] == '.') + { + // Walk out of directories on "..". + if (numComponents > 0) + { + // Discard the previous component. + numComponents--; + } + else + { + // Can't back out any further, so preserve the "..". + leadingDoubles++; + } + } + else + { + if (numComponents >= MAX_COMPONENTS) + { + fprintf(stderr, "Path cannot have more than %d path components.\n", + MAX_COMPONENTS); + exit(1); + } + + components[numComponents].start = start; + components[numComponents].end = end; + numComponents++; + } + } + + // Skip over separators. + while (*end != '\0' && *end == '/') end++; + + start = end; + if (*end == '\0') break; + } + + end++; + } + + // Preserve the absolute prefix, if any. + bool needsSeparator = false; + if (path->length > 0 && path->chars[0] == '/') + { + pathAppendChar(result, '/'); + } + else + { + // Add any leading "..". + for (int i = 0; i < leadingDoubles; i++) + { + if (needsSeparator) pathAppendChar(result, '/'); + pathAppendString(result, ".."); + needsSeparator = true; + } + } + + for (int i = 0; i < numComponents; i++) + { + if (needsSeparator) pathAppendChar(result, '/'); + appendSlice(result, components[i]); + needsSeparator = true; + } + + if (result->length == 0) pathAppendChar(result, '.'); + + return result; +} + +char* pathToString(Path* path) +{ + char* string = (char*)malloc(path->length + 1); + memcpy(string, path->chars, path->length); + string[path->length] = '\0'; + return string; +} diff --git a/src/cli/path.h b/src/cli/path.h new file mode 100644 index 00000000..114346ee --- /dev/null +++ b/src/cli/path.h @@ -0,0 +1,52 @@ +#ifndef path_h +#define path_h + +// Path manipulation functions. + +typedef struct +{ + // Dynamically allocated array of characters. + char* chars; + + // The number of characters currently in use in [chars], not including the + // null terminator. + size_t length; + + // Size of the allocated [chars] buffer. + size_t capacity; +} Path; + +// Creates a new empty path. +Path* pathNew(const char* string); + +// Releases the method associated with [path]. +void pathFree(Path* path); + +// Strips off the last component of the path name. +void pathDirName(Path* path); + +// Strips off the file extension from the last component of the path. +void pathRemoveExtension(Path* path); + +// Appends [string] to [path]. +void pathJoin(Path* path, const char* string); + +// Return true if [path] is an absolute path for the host operating system. +bool pathIsAbsolute(Path* path); + +// Appends [c] to the path, growing the buffer if needed. +void pathAppendChar(Path* path, char c); + +// Appends [string] to the path, growing the buffer if needed. +void pathAppendString(Path* path, const char* string); + +// Simplifies the path string as much as possible. +// +// Applies and removes any "." or ".." components, collapses redundant "/" +// characters, etc. +Path* pathNormalize(Path* path); + +// Allocates a new string exactly the right length and copies this path to it. +char* pathToString(Path* path); + +#endif diff --git a/test/unit/main.c b/test/unit/main.c new file mode 100644 index 00000000..2da088bb --- /dev/null +++ b/test/unit/main.c @@ -0,0 +1,9 @@ +#include "path_test.h" +#include "test.h" + +int main() +{ + testPath(); + + return showTestResults(); +} diff --git a/test/unit/path_test.c b/test/unit/path_test.c new file mode 100644 index 00000000..a175dc49 --- /dev/null +++ b/test/unit/path_test.c @@ -0,0 +1,105 @@ +#include +#include +#include +#include + +#include "path.h" +#include "test.h" + +static void expectNormalize(const char* input, const char* expected) +{ + Path* path = pathNew(input); + Path* result = pathNormalize(path); + + if (strcmp(result->chars, expected) != 0) + { + printf("FAIL %-30s Want %s\n", input, expected); + printf(" Got %s\n\n", result->chars); + fail(); + } + else + { +#if SHOW_PASSES + printf("PASS %-30s -> %s\n", input, result->chars); +#endif + pass(); + } + + pathFree(path); + pathFree(result); +} + +static void testNormalize() +{ + // simple cases + expectNormalize("", "."); + expectNormalize(".", "."); + expectNormalize("..", ".."); + expectNormalize("a", "a"); + expectNormalize("/", "/"); + + // collapses redundant separators + expectNormalize("a/b/c", "a/b/c"); + expectNormalize("a//b///c////d", "a/b/c/d"); + + // eliminates "." parts + expectNormalize("./", "."); + expectNormalize("/.", "/"); + expectNormalize("/./", "/"); + expectNormalize("./.", "."); + expectNormalize("a/./b", "a/b"); + expectNormalize("a/.b/c", "a/.b/c"); + expectNormalize("a/././b/./c", "a/b/c"); + expectNormalize("././a", "a"); + expectNormalize("a/./.", "a"); + + // eliminates ".." parts + expectNormalize("..", ".."); + expectNormalize("../", ".."); + expectNormalize("../../..", "../../.."); + expectNormalize("../../../", "../../.."); + expectNormalize("/..", "/"); + expectNormalize("/../../..", "/"); + expectNormalize("/../../../a", "/a"); + expectNormalize("a/..", "."); + expectNormalize("a/b/..", "a"); + expectNormalize("a/../b", "b"); + expectNormalize("a/./../b", "b"); + expectNormalize("a/b/c/../../d/e/..", "a/d"); + expectNormalize("a/b/../../../../c", "../../c"); + + // does not walk before root on absolute paths + expectNormalize("..", ".."); + expectNormalize("../", ".."); + expectNormalize("/..", "/"); + expectNormalize("a/..", "."); + expectNormalize("../a", "../a"); + expectNormalize("/../a", "/a"); + expectNormalize("/../a", "/a"); + expectNormalize("a/b/..", "a"); + expectNormalize("../a/b/..", "../a"); + expectNormalize("a/../b", "b"); + expectNormalize("a/./../b", "b"); + expectNormalize("a/b/c/../../d/e/..", "a/d"); + expectNormalize("a/b/../../../../c", "../../c"); + expectNormalize("a/b/c/../../..d/./.e/f././", "a/..d/.e/f."); + + // removes trailing separators + expectNormalize("./", "."); + expectNormalize(".//", "."); + expectNormalize("a/", "a"); + expectNormalize("a/b/", "a/b"); + expectNormalize("a/b///", "a/b"); + + expectNormalize("foo/bar/baz", "foo/bar/baz"); + expectNormalize("foo", "foo"); + expectNormalize("foo/bar/", "foo/bar"); + expectNormalize("./foo/././bar/././", "foo/bar"); +} + +void testPath() +{ + // TODO: Test other functions. + testNormalize(); +} + diff --git a/test/unit/path_test.h b/test/unit/path_test.h new file mode 100644 index 00000000..b36436bc --- /dev/null +++ b/test/unit/path_test.h @@ -0,0 +1 @@ +void testPath(); diff --git a/test/unit/test.c b/test/unit/test.c new file mode 100644 index 00000000..b10ede95 --- /dev/null +++ b/test/unit/test.c @@ -0,0 +1,29 @@ +#include + +#include "test.h" + +int passes = 0; +int failures = 0; + +void pass() +{ + passes++; +} + +void fail() +{ + failures++; +} + +int showTestResults() +{ + if (failures > 0) + { + printf("%d out of %d tests failed. :(\n", failures, passes + failures); + return 1; + } + + printf("All %d tests passed!\n", passes + failures); + return 0; +} + diff --git a/test/unit/test.h b/test/unit/test.h new file mode 100644 index 00000000..d51319c0 --- /dev/null +++ b/test/unit/test.h @@ -0,0 +1,7 @@ +// Set this to 1 to output passing tests. +#define SHOW_PASSES 0 + +void pass(); +void fail(); + +int showTestResults(); diff --git a/util/benchmark.py b/util/benchmark.py index cb00a649..612bbea9 100755 --- a/util/benchmark.py +++ b/util/benchmark.py @@ -154,7 +154,7 @@ def run_trial(benchmark, language): # of the normal Wren build. if benchmark[0].startswith("api_"): executable_args = [ - os.path.join(WREN_DIR, "build", "release", "test", "wren") + os.path.join(WREN_DIR, "build", "release", "test", "api_wren") ] args = [] diff --git a/util/test.py b/util/test.py index b6507cd7..dac46ad5 100755 --- a/util/test.py +++ b/util/test.py @@ -25,7 +25,7 @@ config_dir = ("debug" if is_debug else "release") + config WREN_DIR = dirname(dirname(realpath(__file__))) WREN_APP = join(WREN_DIR, 'bin', 'wren' + args.suffix) -TEST_APP = join(WREN_DIR, 'build', config_dir, 'test', 'wren' + args.suffix) +TEST_APP = join(WREN_DIR, 'build', config_dir, 'test', 'api_wren' + args.suffix) EXPECT_PATTERN = re.compile(r'// expect: ?(.*)') EXPECT_ERROR_PATTERN = re.compile(r'// expect error(?! line)') diff --git a/util/wren.mk b/util/wren.mk index f37fa82b..d8273276 100644 --- a/util/wren.mk +++ b/util/wren.mk @@ -31,8 +31,11 @@ MODULE_SOURCES := $(wildcard src/module/*.c) VM_HEADERS := $(wildcard src/vm/*.h) $(wildcard src/vm/*.wren.inc) VM_SOURCES := $(wildcard src/vm/*.c) -TEST_HEADERS := $(wildcard test/api/*.h) -TEST_SOURCES := $(wildcard test/api/*.c) +API_TEST_HEADERS := $(wildcard test/api/*.h) +API_TEST_SOURCES := $(wildcard test/api/*.c) + +UNIT_TEST_HEADERS := $(wildcard test/unit/*.h) +UNIT_TEST_SOURCES := $(wildcard test/unit/*.c) BUILD_DIR := build @@ -120,11 +123,12 @@ endif CFLAGS := $(C_OPTIONS) $(C_WARNINGS) -OPT_OBJECTS := $(addprefix $(BUILD_DIR)/optional/, $(notdir $(OPT_SOURCES:.c=.o))) -CLI_OBJECTS := $(addprefix $(BUILD_DIR)/cli/, $(notdir $(CLI_SOURCES:.c=.o))) -MODULE_OBJECTS := $(addprefix $(BUILD_DIR)/module/, $(notdir $(MODULE_SOURCES:.c=.o))) -VM_OBJECTS := $(addprefix $(BUILD_DIR)/vm/, $(notdir $(VM_SOURCES:.c=.o))) -TEST_OBJECTS := $(patsubst test/api/%.c, $(BUILD_DIR)/test/%.o, $(TEST_SOURCES)) +OPT_OBJECTS := $(addprefix $(BUILD_DIR)/optional/, $(notdir $(OPT_SOURCES:.c=.o))) +CLI_OBJECTS := $(addprefix $(BUILD_DIR)/cli/, $(notdir $(CLI_SOURCES:.c=.o))) +MODULE_OBJECTS := $(addprefix $(BUILD_DIR)/module/, $(notdir $(MODULE_SOURCES:.c=.o))) +VM_OBJECTS := $(addprefix $(BUILD_DIR)/vm/, $(notdir $(VM_SOURCES:.c=.o))) +API_TEST_OBJECTS := $(patsubst test/api/%.c, $(BUILD_DIR)/test/api/%.o, $(API_TEST_SOURCES)) +UNIT_TEST_OBJECTS := $(patsubst test/unit/%.c, $(BUILD_DIR)/test/unit/%.o, $(UNIT_TEST_SOURCES)) LIBUV_DIR := deps/libuv LIBUV := build/libuv$(LIBUV_ARCH).a @@ -152,7 +156,10 @@ static: lib/lib$(WREN).a cli: bin/$(WREN) # Builds the API test executable. -test: $(BUILD_DIR)/test/$(WREN) +api_test: $(BUILD_DIR)/test/api_$(WREN) + +# Builds the unit test executable. +unit_test: $(BUILD_DIR)/test/unit_$(WREN) # Command-line interpreter. bin/$(WREN): $(OPT_OBJECTS) $(CLI_OBJECTS) $(MODULE_OBJECTS) $(VM_OBJECTS) \ @@ -173,13 +180,22 @@ lib/lib$(WREN).$(SHARED_EXT): $(OPT_OBJECTS) $(VM_OBJECTS) $(V) mkdir -p lib $(V) $(CC) $(CFLAGS) -shared $(SHARED_LIB_FLAGS) -o $@ $^ -# Test executable. -$(BUILD_DIR)/test/$(WREN): $(OPT_OBJECTS) $(MODULE_OBJECTS) $(TEST_OBJECTS) \ - $(VM_OBJECTS) $(BUILD_DIR)/cli/modules.o $(BUILD_DIR)/cli/vm.o $(LIBUV) +# API test executable. +$(BUILD_DIR)/test/api_$(WREN): $(OPT_OBJECTS) $(MODULE_OBJECTS) $(API_TEST_OBJECTS) \ + $(VM_OBJECTS) $(BUILD_DIR)/cli/modules.o $(BUILD_DIR)/cli/vm.o \ + $(BUILD_DIR)/cli/path.o $(LIBUV) @ printf "%10s %-30s %s\n" $(CC) $@ "$(C_OPTIONS)" - $(V) mkdir -p $(BUILD_DIR)/test + $(V) mkdir -p $(BUILD_DIR)/test/api $(V) $(CC) $(CFLAGS) $^ -o $@ -lm $(LIBUV_LIBS) +# Unit test executable. +$(BUILD_DIR)/test/unit_$(WREN): $(OPT_OBJECTS) $(MODULE_OBJECTS) $(UNIT_TEST_OBJECTS) \ + $(VM_OBJECTS) $(BUILD_DIR)/cli/modules.o $(BUILD_DIR)/cli/vm.o \ + $(BUILD_DIR)/cli/path.o $(LIBUV) + @ printf "%10s %-30s %s\n" $(CC) $@ "$(C_OPTIONS)" + $(V) mkdir -p $(BUILD_DIR)/test/unit + $(V) $(CC) $(CFLAGS) $^ -o $@ + # CLI object files. $(BUILD_DIR)/cli/%.o: src/cli/%.c $(CLI_HEADERS) $(MODULE_HEADERS) \ $(VM_HEADERS) $(LIBUV) @@ -194,7 +210,7 @@ $(BUILD_DIR)/module/%.o: src/module/%.c $(CLI_HEADERS) $(MODULE_HEADERS) \ $(V) mkdir -p $(BUILD_DIR)/module $(V) $(CC) -c $(CFLAGS) $(CLI_FLAGS) -o $@ $(FILE_FLAG) $< -# Aux object files. +# Optional object files. $(BUILD_DIR)/optional/%.o: src/optional/%.c $(VM_HEADERS) $(OPT_HEADERS) @ printf "%10s %-30s %s\n" $(CC) $< "$(C_OPTIONS)" $(V) mkdir -p $(BUILD_DIR)/optional @@ -206,9 +222,16 @@ $(BUILD_DIR)/vm/%.o: src/vm/%.c $(VM_HEADERS) $(V) mkdir -p $(BUILD_DIR)/vm $(V) $(CC) -c $(CFLAGS) -Isrc/include -Isrc/optional -Isrc/vm -o $@ $(FILE_FLAG) $< -# Test object files. -$(BUILD_DIR)/test/%.o: test/api/%.c $(OPT_HEADERS) $(MODULE_HEADERS) \ - $(VM_HEADERS) $(TEST_HEADERS) $(LIBUV) +# API test object files. +$(BUILD_DIR)/test/api/%.o: test/api/%.c $(OPT_HEADERS) $(MODULE_HEADERS) \ + $(VM_HEADERS) $(API_TEST_HEADERS) $(LIBUV) + @ printf "%10s %-30s %s\n" $(CC) $< "$(C_OPTIONS)" + $(V) mkdir -p $(dir $@) + $(V) $(CC) -c $(CFLAGS) $(CLI_FLAGS) -o $@ $(FILE_FLAG) $< + +# Unit test object files. +$(BUILD_DIR)/test/unit/%.o: test/unit/%.c $(OPT_HEADERS) $(MODULE_HEADERS) \ + $(VM_HEADERS) $(UNIT_TEST_HEADERS) $(LIBUV) @ printf "%10s %-30s %s\n" $(CC) $< "$(C_OPTIONS)" $(V) mkdir -p $(dir $@) $(V) $(CC) -c $(CFLAGS) $(CLI_FLAGS) -o $@ $(FILE_FLAG) $< @@ -231,4 +254,4 @@ src/module/%.wren.inc: src/module/%.wren util/wren_to_c_string.py @ printf "%10s %-30s %s\n" str $< $(V) ./util/wren_to_c_string.py $@ $< -.PHONY: all cli test vm +.PHONY: all api_test cli unit_test vm diff --git a/util/xcode/wren.xcodeproj/project.pbxproj b/util/xcode/wren.xcodeproj/project.pbxproj index 2ac08e76..3e729748 100644 --- a/util/xcode/wren.xcodeproj/project.pbxproj +++ b/util/xcode/wren.xcodeproj/project.pbxproj @@ -24,9 +24,14 @@ 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 */; }; + 2940E9BB2066067D0054503C /* path_test.c in Sources */ = {isa = PBXBuildFile; fileRef = 2940E9BA2066067D0054503C /* path_test.c */; }; + 2940E9BC206607830054503C /* path.c in Sources */ = {isa = PBXBuildFile; fileRef = 2952CD1B1FA9941700985F5F /* path.c */; }; + 2940E9BE2066C3300054503C /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 2940E9BD2066C3300054503C /* main.c */; }; + 2940E9C12066C35E0054503C /* test.c in Sources */ = {isa = PBXBuildFile; fileRef = 2940E9BF2066C35E0054503C /* test.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 */; }; + 2952CD1D1FA9941700985F5F /* path.c in Sources */ = {isa = PBXBuildFile; fileRef = 2952CD1B1FA9941700985F5F /* path.c */; }; 29729F311BA70A620099CA20 /* io.c in Sources */ = {isa = PBXBuildFile; fileRef = 29729F2E1BA70A620099CA20 /* io.c */; }; 29729F321BA70A620099CA20 /* io.c in Sources */ = {isa = PBXBuildFile; fileRef = 29729F2E1BA70A620099CA20 /* io.c */; }; 29729F331BA70A620099CA20 /* io.wren.inc in Sources */ = {isa = PBXBuildFile; fileRef = 29729F301BA70A620099CA20 /* io.wren.inc */; }; @@ -68,6 +73,15 @@ /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ + 2940E9B4206605DE0054503C /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; 29AB1F041816E3AD004B501E /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -118,10 +132,18 @@ 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 = ""; }; + 2940E9B8206605DE0054503C /* unit_test */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = unit_test; sourceTree = BUILT_PRODUCTS_DIR; }; + 2940E9B92066067D0054503C /* path_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = path_test.h; path = ../../../test/unit/path_test.h; sourceTree = ""; }; + 2940E9BA2066067D0054503C /* path_test.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = path_test.c; path = ../../../test/unit/path_test.c; sourceTree = ""; }; + 2940E9BD2066C3300054503C /* main.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = main.c; path = ../../../test/unit/main.c; sourceTree = ""; }; + 2940E9BF2066C35E0054503C /* test.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = test.c; path = ../../../test/unit/test.c; sourceTree = ""; }; + 2940E9C02066C35E0054503C /* test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = test.h; path = ../../../test/unit/test.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; }; 29512C801B91F8EB008C10E6 /* libuv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libuv.a; path = ../../build/libuv.a; sourceTree = ""; }; + 2952CD1B1FA9941700985F5F /* path.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = path.c; path = ../../src/cli/path.c; sourceTree = ""; }; + 2952CD1C1FA9941700985F5F /* path.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = path.h; path = ../../src/cli/path.h; sourceTree = ""; }; 296371B31AC713D000079FDA /* wren_opcodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = wren_opcodes.h; path = ../../src/vm/wren_opcodes.h; sourceTree = ""; }; 29729F2E1BA70A620099CA20 /* io.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = io.c; path = ../../src/module/io.c; sourceTree = ""; }; 29729F301BA70A620099CA20 /* io.wren.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = io.wren.inc; path = ../../src/module/io.wren.inc; sourceTree = ""; }; @@ -164,6 +186,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2940E9B2206605DE0054503C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 29AB1F031816E3AD004B501E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -233,6 +262,8 @@ 29205C8E1AB4E5C90073018D /* main.c */, 291647C61BA5EC5E006142EE /* modules.h */, 291647C51BA5EC5E006142EE /* modules.c */, + 2952CD1C1FA9941700985F5F /* path.h */, + 2952CD1B1FA9941700985F5F /* path.c */, 29C8A9321AB71FFF00DEC81D /* vm.h */, 29C8A9311AB71FFF00DEC81D /* vm.c */, ); @@ -247,6 +278,18 @@ name = include; sourceTree = ""; }; + 2940E98E206605CB0054503C /* unit_test */ = { + isa = PBXGroup; + children = ( + 2940E9BD2066C3300054503C /* main.c */, + 2940E9BA2066067D0054503C /* path_test.c */, + 2940E9B92066067D0054503C /* path_test.h */, + 2940E9BF2066C35E0054503C /* test.c */, + 2940E9C02066C35E0054503C /* test.h */, + ); + path = unit_test; + sourceTree = ""; + }; 29AB1EFD1816E3AD004B501E = { isa = PBXGroup; children = ( @@ -256,6 +299,7 @@ 29AF31EE1BD2E37F00AAD156 /* optional */, 29205CA01AB4E6470073018D /* vm */, 29D0099A1B7E394F000CE58C /* api_test */, + 2940E98E206605CB0054503C /* unit_test */, 29512C801B91F8EB008C10E6 /* libuv.a */, 29AB1F071816E3AD004B501E /* Products */, ); @@ -266,6 +310,7 @@ children = ( 29AB1F061816E3AD004B501E /* wren */, 29512C7F1B91F86E008C10E6 /* api_test */, + 2940E9B8206605DE0054503C /* unit_test */, ); name = Products; sourceTree = ""; @@ -320,6 +365,23 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 2940E98F206605DE0054503C /* unit_test */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2940E9B5206605DE0054503C /* Build configuration list for PBXNativeTarget "unit_test" */; + buildPhases = ( + 2940E990206605DE0054503C /* Sources */, + 2940E9B2206605DE0054503C /* Frameworks */, + 2940E9B4206605DE0054503C /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = unit_test; + productName = api_test; + productReference = 2940E9B8206605DE0054503C /* unit_test */; + productType = "com.apple.product-type.tool"; + }; 29AB1F051816E3AD004B501E /* wren */ = { isa = PBXNativeTarget; buildConfigurationList = 29AB1F0F1816E3AD004B501E /* Build configuration list for PBXNativeTarget "wren" */; @@ -382,11 +444,23 @@ targets = ( 29AB1F051816E3AD004B501E /* wren */, 29D0099E1B7E397D000CE58C /* api_test */, + 2940E98F206605DE0054503C /* unit_test */, ); }; /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ + 2940E990206605DE0054503C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2940E9BC206607830054503C /* path.c in Sources */, + 2940E9BB2066067D0054503C /* path_test.c in Sources */, + 2940E9BE2066C3300054503C /* main.c in Sources */, + 2940E9C12066C35E0054503C /* test.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 29AB1F021816E3AD004B501E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -395,6 +469,7 @@ 29205C991AB4E6430073018D /* wren_compiler.c in Sources */, 2986F6D71ACF93BA00BCE26C /* wren_primitive.c in Sources */, 291647C71BA5EC5E006142EE /* modules.c in Sources */, + 2952CD1D1FA9941700985F5F /* path.c in Sources */, 29A4273A1BDBE435001E6E22 /* wren_opt_random.wren.inc in Sources */, 29205C9A1AB4E6430073018D /* wren_core.c in Sources */, 2901D7641B74F4050083A2C8 /* timer.c in Sources */, @@ -460,6 +535,50 @@ /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ + 2940E9B6206605DE0054503C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_PEDANTIC = NO; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + LIBRARY_SEARCH_PATHS = ../../build; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + USER_HEADER_SEARCH_PATHS = "../../deps/libuv/include ../../src/module"; + }; + name = Debug; + }; + 2940E9B7206605DE0054503C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = c99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_PEDANTIC = NO; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + LIBRARY_SEARCH_PATHS = ../../build; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + USER_HEADER_SEARCH_PATHS = "../../deps/libuv/include ../../src/module"; + }; + name = Release; + }; 29AB1F0D1816E3AD004B501E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -605,6 +724,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 2940E9B5206605DE0054503C /* Build configuration list for PBXNativeTarget "unit_test" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2940E9B6206605DE0054503C /* Debug */, + 2940E9B7206605DE0054503C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 29AB1F011816E3AD004B501E /* Build configuration list for PBXProject "wren" */ = { isa = XCConfigurationList; buildConfigurations = ( From 8210452970eaa2e25c1a1a68318553a11f65b317 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Sat, 24 Mar 2018 11:10:36 -0700 Subject: [PATCH 4/5] Relative imports! This is a breaking change because existing imports in user Wren code that assume the path is relative to the entrypoint file will now likely fail. Also, stack trace output and host API calls that take a module string now need the resolved module string, not the short name that appears in the import. --- .../cthulu.wren | 0 .../lovecraft.wren | 2 +- src/cli/vm.c | 128 ++++++++++-------- src/include/wren.h | 25 ++-- test/api/call.c | 2 +- test/api/get_variable.c | 10 +- test/api/get_variable.wren | 2 +- test/api/main.c | 6 +- test/api/reset_stack_after_call_abort.c | 4 +- .../api/reset_stack_after_foreign_construct.c | 4 +- test/core/fiber/yield_from_import.wren | 2 +- test/language/foreign/unknown_method.wren | 2 +- .../change_imported_value.wren | 2 +- .../module/compile_error/compile_error.wren | 2 +- test/language/module/cyclic_import/a.wren | 2 +- test/language/module/cyclic_import/b.wren | 2 +- .../module/cyclic_import/cyclic_import.wren | 2 +- .../implicitly_imports_core.wren | 2 +- .../module/inside_block/inside_block.wren | 2 +- test/language/module/missing_for.wren | 2 +- .../module/module_dir/module_dir.wren | 2 +- .../multiple_variables.wren | 2 +- test/language/module/name_collision.wren | 2 +- test/language/module/newlines/newlines.wren | 4 +- .../module/no_variable/no_variable.wren | 2 +- .../module/relative_import/module_3.wren | 2 + .../relative_import/relative_import.wren | 8 ++ .../relative_import/sub/dir/module.wren | 3 + .../relative_import/sub/dir/module_2.wren | 4 + .../module/relative_import/sub/module.wren | 3 + .../module/relative_import/sub/module_2.wren | 2 + .../module/relative_import/sub/module_3.wren | 2 + test/language/module/shared_import/a.wren | 2 +- test/language/module/shared_import/b.wren | 2 +- .../module/shared_import/shared_import.wren | 4 +- .../module/simple_import/simple_import.wren | 2 +- test/language/module/unknown_module.wren | 2 +- .../unknown_variable/unknown_variable.wren | 2 +- test/meta/get_module_variables.wren | 2 +- util/benchmark.py | 2 +- util/test.py | 2 +- 41 files changed, 152 insertions(+), 107 deletions(-) rename example/{import-module => import_module}/cthulu.wren (100%) rename example/{import-module => import_module}/lovecraft.wren (79%) create mode 100644 test/language/module/relative_import/module_3.wren create mode 100644 test/language/module/relative_import/relative_import.wren create mode 100644 test/language/module/relative_import/sub/dir/module.wren create mode 100644 test/language/module/relative_import/sub/dir/module_2.wren create mode 100644 test/language/module/relative_import/sub/module.wren create mode 100644 test/language/module/relative_import/sub/module_2.wren create mode 100644 test/language/module/relative_import/sub/module_3.wren diff --git a/example/import-module/cthulu.wren b/example/import_module/cthulu.wren similarity index 100% rename from example/import-module/cthulu.wren rename to example/import_module/cthulu.wren diff --git a/example/import-module/lovecraft.wren b/example/import_module/lovecraft.wren similarity index 79% rename from example/import-module/lovecraft.wren rename to example/import_module/lovecraft.wren index 57b11331..08e46ad5 100644 --- a/example/import-module/lovecraft.wren +++ b/example/import_module/lovecraft.wren @@ -1,4 +1,4 @@ -import "cthulu" for Cthulu +import "./cthulu" for Cthulu class Lovecraft { construct new() {} diff --git a/src/cli/vm.c b/src/cli/vm.c index 05f2b2fd..863e85e6 100644 --- a/src/cli/vm.c +++ b/src/cli/vm.c @@ -3,6 +3,7 @@ #include "io.h" #include "modules.h" +#include "path.h" #include "scheduler.h" #include "vm.h" @@ -15,7 +16,9 @@ static WrenForeignMethodFn afterLoadFn = NULL; static uv_loop_t* loop; -static char const* rootDirectory = NULL; +// TODO: This isn't currently used, but probably will be when package imports +// are supported. If not then, then delete this. +static char* rootDirectory = NULL; // The exit code to use unless some other error overrides it. int defaultExitCode = 0; @@ -44,7 +47,7 @@ static char* readFile(const char* path) } // Read the entire file. - size_t bytesRead = fread(buffer, sizeof(char), fileSize, file); + size_t bytesRead = fread(buffer, 1, fileSize, file); if (bytesRead < fileSize) { fprintf(stderr, "Could not read file \"%s\".\n", path); @@ -58,25 +61,46 @@ static char* readFile(const char* path) return buffer; } -// Converts the module [name] to a file path. -static char* wrenFilePath(const char* name) +// Applies the CLI's import resolution policy. The rules are: +// +// * If [name] starts with "./" or "../", it is a relative import, relative to +// [importer]. The resolved path is [name] concatenated onto the directory +// containing [importer] and then normalized. +// +// For example, importing "./a/./b/../c" from "d/e/f" gives you "d/e/a/c". +// +// * Otherwise, it is a "package" import. This isn't implemented yet. +// +static const char* resolveModule(WrenVM* vm, const char* importer, + const char* name) { - // The module path is relative to the root directory and with ".wren". - size_t rootLength = rootDirectory == NULL ? 0 : strlen(rootDirectory); size_t nameLength = strlen(name); - size_t pathLength = rootLength + nameLength + 5; - char* path = (char*)malloc(pathLength + 1); - if (rootDirectory != NULL) + // See if it's a relative import. + if (nameLength > 2 && + ((name[0] == '.' && name[1] == '/') || + (name[0] == '.' && name[1] == '.' && name[2] == '/'))) { - memcpy(path, rootDirectory, rootLength); + // Get the directory containing the importing module. + Path* relative = pathNew(importer); + pathDirName(relative); + + // Add the relative import path. + pathJoin(relative, name); + Path* normal = pathNormalize(relative); + pathFree(relative); + + char* resolved = pathToString(normal); + pathFree(normal); + return resolved; + } + else + { + // TODO: Implement package imports. For now, treat any non-relative import + // as an import relative to the current working directory. } - memcpy(path + rootLength, name, nameLength); - memcpy(path + rootLength + nameLength, ".wren", 5); - path[pathLength] = '\0'; - - return path; + return name; } // Attempts to read the source for [module] relative to the current root @@ -86,32 +110,26 @@ static char* wrenFilePath(const char* name) // module was found but could not be read. static char* readModule(WrenVM* vm, const char* module) { - char* source = readBuiltInModule(module); + // Since the module has already been resolved, it should now be either a + // valid relative path, or a package-style name. + + // TODO: Implement package imports. + + // Add a ".wren" file extension. + Path* modulePath = pathNew(module); + pathAppendString(modulePath, ".wren"); + + char* source = readFile(modulePath->chars); + pathFree(modulePath); + if (source != NULL) return source; - - // First try to load the module with a ".wren" extension. - char* modulePath = wrenFilePath(module); - char* moduleContents = readFile(modulePath); - free(modulePath); - - if (moduleContents != NULL) return moduleContents; - - // If no contents could be loaded treat the module name as specifying a - // directory and try to load the "module.wren" file in the directory. - size_t moduleLength = strlen(module); - size_t moduleDirLength = moduleLength + 7; - char* moduleDir = (char*)malloc(moduleDirLength + 1); - memcpy(moduleDir, module, moduleLength); - memcpy(moduleDir + moduleLength, "/module", 7); - moduleDir[moduleDirLength] = '\0'; - - char* moduleDirPath = wrenFilePath(moduleDir); - free(moduleDir); - - moduleContents = readFile(moduleDirPath); - free(moduleDirPath); - - return moduleContents; + + // TODO: This used to look for a file named "/module.wren" if + // ".wren" could not be found. Do we still want to support that with + // the new relative import and package stuff? + + // Otherwise, see if it's a built-in module. + return readBuiltInModule(module); } // Binds foreign methods declared in either built in modules, or the injected @@ -179,6 +197,7 @@ static void initVM() config.bindForeignMethodFn = bindForeignMethod; config.bindForeignClassFn = bindForeignClass; + config.resolveModuleFn = resolveModule; config.loadModuleFn = readModule; config.writeFn = write; config.errorFn = reportError; @@ -207,18 +226,6 @@ static void freeVM() void runFile(const char* path) { - // Use the directory where the file is as the root to resolve imports - // relative to. - char* root = NULL; - const char* lastSlash = strrchr(path, '/'); - if (lastSlash != NULL) - { - root = (char*)malloc(lastSlash - path + 2); - memcpy(root, path, lastSlash - path + 1); - root[lastSlash - path + 1] = '\0'; - rootDirectory = root; - } - char* source = readFile(path); if (source == NULL) { @@ -226,9 +233,19 @@ void runFile(const char* path) exit(66); } + // Use the directory where the file is as the root to resolve imports + // relative to. + Path* directory = pathNew(path); + pathDirName(directory); + rootDirectory = pathToString(directory); + pathFree(directory); + + Path* moduleName = pathNew(path); + pathRemoveExtension(moduleName); + initVM(); - WrenInterpretResult result = wrenInterpret(vm, "main", source); + WrenInterpretResult result = wrenInterpret(vm, moduleName->chars, source); if (afterLoadFn != NULL) afterLoadFn(vm); @@ -240,7 +257,8 @@ void runFile(const char* path) freeVM(); free(source); - free(root); + free(rootDirectory); + pathFree(moduleName); // Exit with an error code if the script failed. if (result == WREN_RESULT_COMPILE_ERROR) exit(65); // EX_DATAERR. @@ -256,7 +274,7 @@ int runRepl() printf("\\\\/\"-\n"); printf(" \\_/ wren v%s\n", WREN_VERSION_STRING); - wrenInterpret(vm, "main", "import \"repl\"\n"); + wrenInterpret(vm, "repl", "import \"repl\"\n"); uv_run(loop, UV_RUN_DEFAULT); diff --git a/src/include/wren.h b/src/include/wren.h index 09a3aa15..dbea3077 100644 --- a/src/include/wren.h +++ b/src/include/wren.h @@ -92,14 +92,15 @@ typedef enum // Reports an error to the user. // // An error detected during compile time is reported by calling this once with -// `WREN_ERROR_COMPILE`, the name of the module and line where the error occurs, -// and the compiler's error message. +// [type] `WREN_ERROR_COMPILE`, the resolved name of the [module] and [line] +// where the error occurs, and the compiler's error [message]. // -// A runtime error is reported by calling this once with `WREN_ERROR_RUNTIME`, -// no module or line, and the runtime error's message. After that, a series of -// `WREN_ERROR_STACK_TRACE` calls are made for each line in the stack trace. -// Each of those has the module and line where the method or function is -// defined and [message] is the name of the method or function. +// A runtime error is reported by calling this once with [type] +// `WREN_ERROR_RUNTIME`, no [module] or [line], and the runtime error's +// [message]. After that, a series of [type] `WREN_ERROR_STACK_TRACE` calls are +// made for each line in the stack trace. Each of those has the resolved +// [module] and [line] where the method or function is defined and [message] is +// the name of the method or function. typedef void (*WrenErrorFn)( WrenVM* vm, WrenErrorType type, const char* module, int line, const char* message); @@ -120,7 +121,7 @@ typedef struct } WrenForeignClassMethods; // Returns a pair of pointers to the foreign methods used to allocate and -// finalize the data for instances of [className] in [module]. +// finalize the data for instances of [className] in resolved [module]. typedef WrenForeignClassMethods (*WrenBindForeignClassFn)( WrenVM* vm, const char* module, const char* className); @@ -287,7 +288,8 @@ void wrenFreeVM(WrenVM* vm); // Immediately run the garbage collector to free unused memory. void wrenCollectGarbage(WrenVM* vm); -// Runs [source], a string of Wren source code in a new fiber in [vm]. +// Runs [source], a string of Wren source code in a new fiber in [vm] in the +// context of resolved [module]. WrenInterpretResult wrenInterpret(WrenVM* vm, const char* module, const char* source); @@ -468,10 +470,11 @@ void wrenGetListElement(WrenVM* vm, int listSlot, int index, int elementSlot); // an element, use `-1` for the index. void wrenInsertInList(WrenVM* vm, int listSlot, int index, int elementSlot); -// Looks up the top level variable with [name] in [module] and stores it in -// [slot]. +// Looks up the top level variable with [name] in resolved [module] and stores +// it in [slot]. void wrenGetVariable(WrenVM* vm, const char* module, const char* name, int slot); + // Sets the current fiber to be aborted, and uses the value in [slot] as the // runtime error object. void wrenAbortFiber(WrenVM* vm, int slot); diff --git a/test/api/call.c b/test/api/call.c index 8dadcc35..7f4264e2 100644 --- a/test/api/call.c +++ b/test/api/call.c @@ -6,7 +6,7 @@ void callRunTests(WrenVM* vm) { wrenEnsureSlots(vm, 1); - wrenGetVariable(vm, "main", "Call", 0); + wrenGetVariable(vm, "test/api/call", "Call", 0); WrenHandle* callClass = wrenGetSlotHandle(vm, 0); WrenHandle* noParams = wrenMakeCallHandle(vm, "noParams"); diff --git a/test/api/get_variable.c b/test/api/get_variable.c index c5237214..5dece4cf 100644 --- a/test/api/get_variable.c +++ b/test/api/get_variable.c @@ -4,23 +4,23 @@ static void beforeDefined(WrenVM* vm) { - wrenGetVariable(vm, "main", "A", 0); + wrenGetVariable(vm, "test/api/get_variable", "A", 0); } static void afterDefined(WrenVM* vm) { - wrenGetVariable(vm, "main", "A", 0); + wrenGetVariable(vm, "test/api/get_variable", "A", 0); } static void afterAssigned(WrenVM* vm) { - wrenGetVariable(vm, "main", "A", 0); + wrenGetVariable(vm, "test/api/get_variable", "A", 0); } static void otherSlot(WrenVM* vm) { wrenEnsureSlots(vm, 3); - wrenGetVariable(vm, "main", "B", 2); + wrenGetVariable(vm, "test/api/get_variable", "B", 2); // Move it into return position. const char* string = wrenGetSlotString(vm, 2); @@ -29,7 +29,7 @@ static void otherSlot(WrenVM* vm) static void otherModule(WrenVM* vm) { - wrenGetVariable(vm, "get_variable_module", "Variable", 0); + wrenGetVariable(vm, "test/api/get_variable_module", "Variable", 0); } WrenForeignMethodFn getVariableBindMethod(const char* signature) diff --git a/test/api/get_variable.wren b/test/api/get_variable.wren index 89b92e4b..48cdf84c 100644 --- a/test/api/get_variable.wren +++ b/test/api/get_variable.wren @@ -1,4 +1,4 @@ -import "get_variable_module" +import "./get_variable_module" class GetVariable { foreign static beforeDefined() diff --git a/test/api/main.c b/test/api/main.c index ed3510d2..52610afa 100644 --- a/test/api/main.c +++ b/test/api/main.c @@ -24,8 +24,8 @@ const char* testName; static WrenForeignMethodFn bindForeignMethod( WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) -{ - if (strcmp(module, "main") != 0) return NULL; +{ + if (strncmp(module, "test/", 5) != 0) return NULL; // For convenience, concatenate all of the method qualifiers into a single // signature string. @@ -78,7 +78,7 @@ static WrenForeignClassMethods bindForeignClass( WrenVM* vm, const char* module, const char* className) { WrenForeignClassMethods methods = { NULL, NULL }; - if (strcmp(module, "main") != 0) return methods; + if (strncmp(module, "test/", 5) != 0) return methods; foreignClassBindClass(className, &methods); if (methods.allocate != NULL) return methods; diff --git a/test/api/reset_stack_after_call_abort.c b/test/api/reset_stack_after_call_abort.c index 8136796e..c94687fe 100644 --- a/test/api/reset_stack_after_call_abort.c +++ b/test/api/reset_stack_after_call_abort.c @@ -6,7 +6,7 @@ void resetStackAfterCallAbortRunTests(WrenVM* vm) { wrenEnsureSlots(vm, 1); - wrenGetVariable(vm, "main", "Test", 0); + wrenGetVariable(vm, "test/api/reset_stack_after_call_abort", "Test", 0); WrenHandle* testClass = wrenGetSlotHandle(vm, 0); WrenHandle* abortFiber = wrenMakeCallHandle(vm, "abortFiber()"); @@ -25,4 +25,4 @@ void resetStackAfterCallAbortRunTests(WrenVM* vm) wrenReleaseHandle(vm, testClass); wrenReleaseHandle(vm, abortFiber); wrenReleaseHandle(vm, afterConstruct); -} \ No newline at end of file +} diff --git a/test/api/reset_stack_after_foreign_construct.c b/test/api/reset_stack_after_foreign_construct.c index f24783d8..ce829a8b 100644 --- a/test/api/reset_stack_after_foreign_construct.c +++ b/test/api/reset_stack_after_foreign_construct.c @@ -22,7 +22,7 @@ void resetStackAfterForeignConstructBindClass( void resetStackAfterForeignConstructRunTests(WrenVM* vm) { wrenEnsureSlots(vm, 1); - wrenGetVariable(vm, "main", "Test", 0); + wrenGetVariable(vm, "test/api/reset_stack_after_foreign_construct", "Test", 0); WrenHandle* testClass = wrenGetSlotHandle(vm, 0); WrenHandle* callConstruct = wrenMakeCallHandle(vm, "callConstruct()"); @@ -41,4 +41,4 @@ void resetStackAfterForeignConstructRunTests(WrenVM* vm) wrenReleaseHandle(vm, testClass); wrenReleaseHandle(vm, callConstruct); wrenReleaseHandle(vm, afterConstruct); -} \ No newline at end of file +} diff --git a/test/core/fiber/yield_from_import.wren b/test/core/fiber/yield_from_import.wren index dbb0e77d..80062e9d 100644 --- a/test/core/fiber/yield_from_import.wren +++ b/test/core/fiber/yield_from_import.wren @@ -1,7 +1,7 @@ var fiber = Fiber.new { System.print("fiber 1") - import "yield_from_import_module" + import "./yield_from_import_module" System.print("fiber 2") } diff --git a/test/language/foreign/unknown_method.wren b/test/language/foreign/unknown_method.wren index 9b7c7c45..2cdf01be 100644 --- a/test/language/foreign/unknown_method.wren +++ b/test/language/foreign/unknown_method.wren @@ -1,3 +1,3 @@ class Foo { - foreign someUnknownMethod // expect runtime error: Could not find foreign method 'someUnknownMethod' for class Foo in module 'main'. + foreign someUnknownMethod // expect runtime error: Could not find foreign method 'someUnknownMethod' for class Foo in module 'test/language/foreign/unknown_method'. } diff --git a/test/language/module/change_imported_value/change_imported_value.wren b/test/language/module/change_imported_value/change_imported_value.wren index 0f5aa0ff..1f7bc986 100644 --- a/test/language/module/change_imported_value/change_imported_value.wren +++ b/test/language/module/change_imported_value/change_imported_value.wren @@ -1,4 +1,4 @@ -import "module" for Module, Other +import "./module" for Module, Other System.print(Module) // expect: before diff --git a/test/language/module/compile_error/compile_error.wren b/test/language/module/compile_error/compile_error.wren index 86141630..f136e882 100644 --- a/test/language/module/compile_error/compile_error.wren +++ b/test/language/module/compile_error/compile_error.wren @@ -1,2 +1,2 @@ System.print("before") // expect: before -import "module" for Module // expect runtime error: Could not compile module 'module'. +import "./module" for Module // expect runtime error: Could not compile module 'test/language/module/compile_error/module'. diff --git a/test/language/module/cyclic_import/a.wren b/test/language/module/cyclic_import/a.wren index c2806adb..68fda993 100644 --- a/test/language/module/cyclic_import/a.wren +++ b/test/language/module/cyclic_import/a.wren @@ -3,7 +3,7 @@ System.print("start a") var A = "a value" System.print("a defined %(A)") -import "b" for B +import "./b" for B System.print("a imported %(B)") System.print("end a") diff --git a/test/language/module/cyclic_import/b.wren b/test/language/module/cyclic_import/b.wren index 21fb44ae..968b4d26 100644 --- a/test/language/module/cyclic_import/b.wren +++ b/test/language/module/cyclic_import/b.wren @@ -3,7 +3,7 @@ System.print("start b") var B = "b value" System.print("b defined %(B)") -import "a" for A +import "./a" for A System.print("b imported %(A)") System.print("end b") diff --git a/test/language/module/cyclic_import/cyclic_import.wren b/test/language/module/cyclic_import/cyclic_import.wren index 1cb4de97..afd67b35 100644 --- a/test/language/module/cyclic_import/cyclic_import.wren +++ b/test/language/module/cyclic_import/cyclic_import.wren @@ -1,4 +1,4 @@ -import "a" +import "./a" // Shared module should only run once: // expect: start a diff --git a/test/language/module/implicitly_imports_core/implicitly_imports_core.wren b/test/language/module/implicitly_imports_core/implicitly_imports_core.wren index 1fef7723..e36dc794 100644 --- a/test/language/module/implicitly_imports_core/implicitly_imports_core.wren +++ b/test/language/module/implicitly_imports_core/implicitly_imports_core.wren @@ -1,4 +1,4 @@ -import "module" +import "./module" // expect: Bool // expect: Class // expect: Fiber diff --git a/test/language/module/inside_block/inside_block.wren b/test/language/module/inside_block/inside_block.wren index f19bc0c7..27742004 100644 --- a/test/language/module/inside_block/inside_block.wren +++ b/test/language/module/inside_block/inside_block.wren @@ -1,7 +1,7 @@ var Module = "outer" if (true) { - import "module" for Module + import "./module" for Module // expect: ran module System.print(Module) // expect: from module diff --git a/test/language/module/missing_for.wren b/test/language/module/missing_for.wren index 7de00259..1c0153f3 100644 --- a/test/language/module/missing_for.wren +++ b/test/language/module/missing_for.wren @@ -1 +1 @@ -import "module" NoString // expect error +import "./module" NoString // expect error diff --git a/test/language/module/module_dir/module_dir.wren b/test/language/module/module_dir/module_dir.wren index e2867d02..a2c56349 100644 --- a/test/language/module/module_dir/module_dir.wren +++ b/test/language/module/module_dir/module_dir.wren @@ -1,3 +1,3 @@ -import "something" for Index +import "./something/module" for Index System.print(Index) // expect: index \ No newline at end of file diff --git a/test/language/module/multiple_variables/multiple_variables.wren b/test/language/module/multiple_variables/multiple_variables.wren index abf387c6..50c05bd8 100644 --- a/test/language/module/multiple_variables/multiple_variables.wren +++ b/test/language/module/multiple_variables/multiple_variables.wren @@ -1,4 +1,4 @@ -import "module" for Module1, Module2, Module3, Module4, Module5 +import "./module" for Module1, Module2, Module3, Module4, Module5 // Only execute module body once: // expect: ran module diff --git a/test/language/module/name_collision.wren b/test/language/module/name_collision.wren index 468c2448..d36cec5f 100644 --- a/test/language/module/name_collision.wren +++ b/test/language/module/name_collision.wren @@ -1,2 +1,2 @@ var Collides -import "module" for Collides // expect error +import "./module" for Collides // expect error diff --git a/test/language/module/newlines/newlines.wren b/test/language/module/newlines/newlines.wren index 559ce949..947a0b8c 100644 --- a/test/language/module/newlines/newlines.wren +++ b/test/language/module/newlines/newlines.wren @@ -1,9 +1,9 @@ import -"module" +"./module" -import "module" for +import "./module" for A, diff --git a/test/language/module/no_variable/no_variable.wren b/test/language/module/no_variable/no_variable.wren index 38b440d3..d88b47e3 100644 --- a/test/language/module/no_variable/no_variable.wren +++ b/test/language/module/no_variable/no_variable.wren @@ -1,2 +1,2 @@ -import "module" +import "./module" // expect: ran module diff --git a/test/language/module/relative_import/module_3.wren b/test/language/module/relative_import/module_3.wren new file mode 100644 index 00000000..a80f3ce9 --- /dev/null +++ b/test/language/module/relative_import/module_3.wren @@ -0,0 +1,2 @@ +// nontest +System.print("module_3") diff --git a/test/language/module/relative_import/relative_import.wren b/test/language/module/relative_import/relative_import.wren new file mode 100644 index 00000000..6cff529a --- /dev/null +++ b/test/language/module/relative_import/relative_import.wren @@ -0,0 +1,8 @@ +import "./sub/module" +import "./sub/././///dir/module" +// expect: sub/module +// expect: sub/module_2 +// expect: sub/dir/module +// expect: sub/dir/module_2 +// expect: sub/module_3 +// expect: module_3 diff --git a/test/language/module/relative_import/sub/dir/module.wren b/test/language/module/relative_import/sub/dir/module.wren new file mode 100644 index 00000000..99390971 --- /dev/null +++ b/test/language/module/relative_import/sub/dir/module.wren @@ -0,0 +1,3 @@ +// nontest +System.print("sub/dir/module") +import "./module_2" diff --git a/test/language/module/relative_import/sub/dir/module_2.wren b/test/language/module/relative_import/sub/dir/module_2.wren new file mode 100644 index 00000000..ef03e323 --- /dev/null +++ b/test/language/module/relative_import/sub/dir/module_2.wren @@ -0,0 +1,4 @@ +// nontest +System.print("sub/dir/module_2") +import "../module_3" +import "../../module_3" diff --git a/test/language/module/relative_import/sub/module.wren b/test/language/module/relative_import/sub/module.wren new file mode 100644 index 00000000..89263f7e --- /dev/null +++ b/test/language/module/relative_import/sub/module.wren @@ -0,0 +1,3 @@ +// nontest +System.print("sub/module") +import "./module_2" diff --git a/test/language/module/relative_import/sub/module_2.wren b/test/language/module/relative_import/sub/module_2.wren new file mode 100644 index 00000000..24af1ed7 --- /dev/null +++ b/test/language/module/relative_import/sub/module_2.wren @@ -0,0 +1,2 @@ +// nontest +System.print("sub/module_2") diff --git a/test/language/module/relative_import/sub/module_3.wren b/test/language/module/relative_import/sub/module_3.wren new file mode 100644 index 00000000..cb62156c --- /dev/null +++ b/test/language/module/relative_import/sub/module_3.wren @@ -0,0 +1,2 @@ +// nontest +System.print("sub/module_3") diff --git a/test/language/module/shared_import/a.wren b/test/language/module/shared_import/a.wren index 6478e5a7..7aa918df 100644 --- a/test/language/module/shared_import/a.wren +++ b/test/language/module/shared_import/a.wren @@ -1,5 +1,5 @@ // nontest System.print("a") -import "shared" for Shared +import "./shared" for Shared var A = "a %(Shared)" System.print("a done") diff --git a/test/language/module/shared_import/b.wren b/test/language/module/shared_import/b.wren index 9449134f..0317bf58 100644 --- a/test/language/module/shared_import/b.wren +++ b/test/language/module/shared_import/b.wren @@ -1,5 +1,5 @@ // nontest System.print("b") -import "shared" for Shared +import "./shared" for Shared var B = "b %(Shared)" System.print("b done") diff --git a/test/language/module/shared_import/shared_import.wren b/test/language/module/shared_import/shared_import.wren index 970cfe62..9324b25d 100644 --- a/test/language/module/shared_import/shared_import.wren +++ b/test/language/module/shared_import/shared_import.wren @@ -1,5 +1,5 @@ -import "a" for A -import "b" for B +import "./a" for A +import "./b" for B // Shared module should only run once: // expect: a diff --git a/test/language/module/simple_import/simple_import.wren b/test/language/module/simple_import/simple_import.wren index eb46932b..67764c32 100644 --- a/test/language/module/simple_import/simple_import.wren +++ b/test/language/module/simple_import/simple_import.wren @@ -1,4 +1,4 @@ -import "module" for Module +import "./module" for Module // expect: ran module System.print(Module) // expect: from module diff --git a/test/language/module/unknown_module.wren b/test/language/module/unknown_module.wren index 6b096cb2..a80cc297 100644 --- a/test/language/module/unknown_module.wren +++ b/test/language/module/unknown_module.wren @@ -1 +1 @@ -import "does_not_exist" for DoesNotExist // expect runtime error: Could not load module 'does_not_exist'. +import "./does_not_exist" for DoesNotExist // expect runtime error: Could not load module 'test/language/module/does_not_exist'. diff --git a/test/language/module/unknown_variable/unknown_variable.wren b/test/language/module/unknown_variable/unknown_variable.wren index 12029e5e..23f347c9 100644 --- a/test/language/module/unknown_variable/unknown_variable.wren +++ b/test/language/module/unknown_variable/unknown_variable.wren @@ -1,3 +1,3 @@ // Should execute the module: // expect: ran module -import "module" for DoesNotExist // expect runtime error: Could not find a variable named 'DoesNotExist' in module 'module'. +import "./module" for DoesNotExist // expect runtime error: Could not find a variable named 'DoesNotExist' in module 'test/language/module/unknown_variable/module'. diff --git a/test/meta/get_module_variables.wren b/test/meta/get_module_variables.wren index c5ab61b9..34dc6a7d 100644 --- a/test/meta/get_module_variables.wren +++ b/test/meta/get_module_variables.wren @@ -1,6 +1,6 @@ import "meta" for Meta -var variables = Meta.getModuleVariables("main") +var variables = Meta.getModuleVariables("test/meta/get_module_variables") // Includes implicitly imported core stuff. System.print(variables.contains("Object")) // expect: true diff --git a/util/benchmark.py b/util/benchmark.py index 612bbea9..f21e1979 100755 --- a/util/benchmark.py +++ b/util/benchmark.py @@ -40,7 +40,7 @@ import sys WREN_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) WREN_BIN = os.path.join(WREN_DIR, 'bin') -BENCHMARK_DIR = os.path.join(WREN_DIR, 'test', 'benchmark') +BENCHMARK_DIR = os.path.join('test', 'benchmark') # How many times to run a given benchmark. NUM_TRIALS = 10 diff --git a/util/test.py b/util/test.py index dac46ad5..adfa9b62 100755 --- a/util/test.py +++ b/util/test.py @@ -32,7 +32,7 @@ EXPECT_ERROR_PATTERN = re.compile(r'// expect error(?! line)') EXPECT_ERROR_LINE_PATTERN = re.compile(r'// expect error line (\d+)') EXPECT_RUNTIME_ERROR_PATTERN = re.compile(r'// expect (handled )?runtime error: (.+)') ERROR_PATTERN = re.compile(r'\[.* line (\d+)\] Error') -STACK_TRACE_PATTERN = re.compile(r'\[main line (\d+)\] in') +STACK_TRACE_PATTERN = re.compile(r'\[test/.* line (\d+)\] in') STDIN_PATTERN = re.compile(r'// stdin: (.*)') SKIP_PATTERN = re.compile(r'// skip: (.*)') NONTEST_PATTERN = re.compile(r'// nontest') From c367fc3bfc712fa7366e57bd2ad8d896172558a1 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Sun, 15 Jul 2018 20:09:41 -0700 Subject: [PATCH 5/5] Get logical imports in "wren_modules" working. There's a lot of changes here and surely some rough edges to iron out. Also, I need to update the docs. But I want to get closer to landing this so I can build on it. --- doc/notes/import syntax.md | 261 ++++++++++++++++++ src/cli/main.c | 11 +- src/cli/path.c | 144 +++++++--- src/cli/path.h | 23 +- src/cli/stat.h | 22 ++ src/cli/vm.c | 222 ++++++++++----- src/cli/vm.h | 9 +- src/module/io.c | 17 +- src/vm/wren_vm.c | 4 +- test/api/call.c | 2 +- test/api/get_variable.c | 10 +- test/api/main.c | 4 +- test/api/reset_stack_after_call_abort.c | 2 +- .../api/reset_stack_after_foreign_construct.c | 3 +- test/language/foreign/unknown_method.wren | 2 +- .../module/compile_error/compile_error.wren | 2 +- .../module/logical_dir/logical_dir.wren | 6 + .../wren_modules/bar/within_bar.wren | 5 + .../wren_modules/foo/within_foo.wren | 5 + .../module/logical_short/logical_short.wren | 5 + .../logical_short/wren_modules/foo/foo.wren | 3 + test/language/module/unknown_module.wren | 2 +- .../unknown_variable/unknown_variable.wren | 2 +- .../a/b/use_nearest_modules_dir.wren | 3 + .../wren_modules/foo/foo.wren | 2 + .../a/b/walk_up_for_modules_dir.wren | 3 + .../wren_modules/foo/foo.wren | 2 + test/meta/get_module_variables.wren | 2 +- test/unit/path_test.c | 25 +- util/test.py | 2 +- util/xcode/wren.xcodeproj/project.pbxproj | 50 ++-- 31 files changed, 665 insertions(+), 190 deletions(-) create mode 100644 doc/notes/import syntax.md create mode 100644 src/cli/stat.h create mode 100644 test/language/module/logical_dir/logical_dir.wren create mode 100644 test/language/module/logical_dir/wren_modules/bar/within_bar.wren create mode 100644 test/language/module/logical_dir/wren_modules/foo/within_foo.wren create mode 100644 test/language/module/logical_short/logical_short.wren create mode 100644 test/language/module/logical_short/wren_modules/foo/foo.wren create mode 100644 test/language/module/use_nearest_modules_dir/a/b/use_nearest_modules_dir.wren create mode 100644 test/language/module/use_nearest_modules_dir/wren_modules/foo/foo.wren create mode 100644 test/language/module/walk_up_for_modules_dir/a/b/walk_up_for_modules_dir.wren create mode 100644 test/language/module/walk_up_for_modules_dir/wren_modules/foo/foo.wren diff --git a/doc/notes/import syntax.md b/doc/notes/import syntax.md new file mode 100644 index 00000000..4718a0eb --- /dev/null +++ b/doc/notes/import syntax.md @@ -0,0 +1,261 @@ +So we need some syntax to distinguish between a relative import and a logical +import. I'm not sure which way to go, and I'd like some feedback (or possibly +other alternate ideas I haven't considered). + +My two favorites are: + +``` +// Use +use "relative/path" +import "logical/path" + +// Node-style +import "./relative/path" +import "logical/path" +``` + +If you folks are OK with "use", that's my preference. But otherwise, the Node +style will definitely work too. I'm open to other ideas as well, including a few +below, but I'd like to not bikeshed this forever. + +## Background + +There are four general approaches we can take: + +### Use a modifier ("modifier") + +Both kinds of imports start with `import`, but then we use a second keyword +afterwards to identify either a relative or logical import. We could use *two* +keywords -- one for each kind -- but that's unnecessarily verbose. Instead, we +use the presence or absence of the keyword to distinguish. In other words: + +``` +import foo "string" +import "string" +``` + +The specific questions we have to answer are: + +1. Which kind of import gets the keyword? Ideally, the most common kind of + import would be the one that doesn't need an extra keyword. + +2. What keyword? This is surprisingly hard. Probably some kind of preposition. + +### Use different keywords ("keyword") + +Instead of using `import` for both logical and relative imports, we could have +two keywords, one for each kind. The specific questions to answer then are: + +1. Which kind of import gets `import`? +2. What's the other keyword? + +### Use different syntax for the path ("syntax") + +Instead of always using a string literal to identify what's being imported, we +could use a different kind of token or tokens for the different kinds of import. +For example, a string literal for one kind, and an identifier token for the +other: + +import identifier +import "string literal" + +The specific questions are: + +1. Which kind of import uses a string literal? +2. What's the syntax for the other kind? + +### Use a signifier in the import string itself to distinguish ("string") + +An import is always `import` followed by a string literal. Then we use some +specific markers inside the string literal itself to distinguish the two kinds. +For example, Node says that an import string starting with "./" or "../" is +relative and other import strings are logical. + +The specific question to answer is what kind of signifier we'd use. I think +Node's convention is the only real contender here, though. + +One feature this style has that none of the others do is that it means the +language syntax itself has no notion of logical and relative imports. This +means there is no overhead or complexity for host applications where that +distinction isn't meaningful. + +## Contenders + +These are options I'm open to, in roughly descending order of preference: + +### Node-style (string) + +If the string starts with "./" or "../", it's relative. + +``` +import "./relative/path" +import "logical/path" +``` + +This is how Node works, so there's prior art. It keeps the language completely +simple. It does feel sort of arbitrary and magical to me, but it's the simplest, +most expedient solution. + +### Use (keyword) + +The `use` keyword is for relative imports, `import` is for logical. + +``` +use "relative/path" +import "logical/path" +``` + +The `use` keyword comes from Pascal, but that's not very widely known. I kind +of like this. It's short, and `use` feels "nearer" to me than "import" so it +has the right connotation. (You can't "use" something unless you have it near +to hand.) + +It adds a little complexity to the language and VM. We have to support both +keywords and pass that "use versus import" bit through the name resolution +process. But that's pretty minor. + +### Slashes (syntax) + +If the path is a string literal, it's relative. Otherwise, it is a +slash-separated series of unquoted identifiers. + +``` +import "relative/path" +import logical/path +``` + +This means you can't (easily) use reserved words as names of logical imports. +This was my initial pitch. I still like how it looks, but I seem to be in the +minority. + +### Relative (modifier) + +The `relative` modifier is for relative imports. + +``` +import relative "relative/path" +import "logical/path" +``` + +It's explicit, which is good. It is unfortunately verbose. I think `relative` +is too useful of a word to make into a reserved word, which means it would have +to be a contextual keyword (i.e. treated like a reserved word after `import` +but behaving like a regular identifier elsewhere). I'm not generally a fan of +contextual keywords—they tend to make things like syntax highlighters more +difficult to create—so I try to avoid them. + +## Rejected + +I considered these ideas, but don't think they are good enough approaches for +various reasons: + +### Package identifier (syntax) + +If an unquoted identifier appears before the import string, then it's a logical +import within that package. Otherwise, it's relative. + +``` +import "relative/path" +import logical "path" +``` + +This was one of my initial ideas. It has the same problem as other unquoted +imports in that it makes it harder to have odd package names. It means the VM +has to understand this syntax and figure out how to display package names in +stack traces and stuff, so there is some extra complexity involved. + +The form where you have both a package name and a relative path within that +package is pretty unusual and likely unintuitive to users. + +### Dotted (syntax) + +If the path is a string literal, it's relative. Otherwise, it is a +dot-separated series of unquoted identifiers. + +``` +import "relative/path" +import logical.path +``` + +Similar to slashes, but using dots. This helps make logical imports look more +visually distinct from relative ones. But it also makes them look more similar +to getter calls, which they aren't related to at all. + +### Include (keyword) + +The `include` keyword is for relative imports, `import` is for logical. + +``` +include "relative/path" +import "logical/path" +``` + +Ruby uses `include` for applying mixins. "Include" reads to me more like some +kind of transclusion thing, so it feels a little weird. + +### Require (keyword) + +The `require` keyword is for relative imports, `import` is for logical. + +``` +require "relative/path" +import "logical/path" +``` + +Node uses "require" and ES6 uses "import" so this is kind of confusing. Ruby +uses `require` and `require_relative`, so using `require` for a relative import +is kind of confusing. Lua also uses `require`, but for both relative and +logical. Overall, this feels murky and unhelpful to me. + +### Angle-brackets (syntax) + +As in C/C++, an import string can be in angle brackets or quotes. Angle brackets +are for logical imports, quotes for relative. + +``` +import "relative/path" +import +``` + +Hard pass. It requires context-sensitive tokenization (!) in C and we definitely +don't want to go there. + +### URI scheme (string) + +An import string starting with "package:" and maybe "wren:" is treated as +logical, like they are URIs with an explicit scheme. Others are relative. + +``` +import "relative/path" +import "package:logical/path" +import "wren:random" +``` + +This is (roughly) how Dart works. I'm not a fan. I think it's too verbose for +logical imports. + +### Package (modifier) + +A `package` modifier indicates a logical import. Others are relative. + +``` +import "relative/path" +import package "logical/path" +``` + +Pretty long, and I'm not too crazy about baking "package" into the language and +VM. + +### From (modifier) + +A `from` modifier indicates, uh, one kind of import. + +``` +import "some/path" +import from "other/path" +``` + +It looks nice, but it's totally unclear to me whether logical imports should +get `from` or relative ones. Also kind of confusing in that Python and ES6 use +`from` in their notation for importing explicit variables from a module (where +Wren uses `for`). diff --git a/src/cli/main.c b/src/cli/main.c index 786ba31c..56f0c9d0 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -22,14 +22,19 @@ int main(int argc, const char* argv[]) osSetArguments(argc, argv); + WrenInterpretResult result; if (argc == 1) { - runRepl(); + result = runRepl(); } else { - runFile(argv[1]); + result = runFile(argv[1]); } - return 0; + // Exit with an error code if the script failed. + if (result == WREN_RESULT_COMPILE_ERROR) return 65; // EX_DATAERR. + if (result == WREN_RESULT_RUNTIME_ERROR) return 70; // EX_SOFTWARE. + + return getExitCode(); } diff --git a/src/cli/path.c b/src/cli/path.c index a3362a3a..460cef07 100644 --- a/src/cli/path.c +++ b/src/cli/path.c @@ -43,6 +43,75 @@ static void appendSlice(Path* path, Slice slice) path->chars[path->length] = '\0'; } +static bool isSeparator(char c) +{ + // Slash is a separator on POSIX and Windows. + if (c == '/') return true; + + // Backslash is only a separator on Windows. +#ifdef _WIN32 + if (c == '\\') return true; +#endif + + return false; +} + +#ifdef _WIN32 +static bool isDriveLetter(char c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} +#endif + +// Gets the length of the prefix of [path] that defines its absolute root. +// +// Returns 1 the leading "/". On Windows, also handles drive letters ("C:" or +// "C:\"). +// +// If the path is not absolute, returns 0. +static size_t absolutePrefixLength(const char* path) +{ +#ifdef _WIN32 + // Drive letter. + if (isDriveLetter(path[0]) && path[1] == ':') + { + if (isSeparator(path->chars[2])) + { + // Fully absolute path. + return 3; + } else { + // "Half-absolute" path like "C:", which is relative to the current + // working directory on drive. It's absolute for our purposes. + return 2; + } + } + + // TODO: UNC paths. + +#endif + + // POSIX-style absolute path or absolute path in the current drive on Windows. + if (isSeparator(path[0])) return 1; + + // Not absolute. + return 0; +} + +PathType pathType(const char* path) +{ + if (absolutePrefixLength(path) > 0) return PATH_TYPE_ABSOLUTE; + + // See if it must be relative. + if ((path[0] == '.' && isSeparator(path[1])) || + (path[0] == '.' && path[1] == '.' && isSeparator(path[2]))) + { + return PATH_TYPE_RELATIVE; + } + + // Otherwise, we don't know. + return PATH_TYPE_SIMPLE; +} + Path* pathNew(const char* string) { Path* path = (Path*)malloc(sizeof(Path)); @@ -67,7 +136,7 @@ void pathDirName(Path* path) // Find the last path separator. for (size_t i = path->length - 1; i < path->length; i--) { - if (path->chars[i] == '/') + if (isSeparator(path->chars[i])) { path->length = i; path->chars[i] = '\0'; @@ -86,7 +155,7 @@ void pathRemoveExtension(Path* path) { // If we hit a path separator before finding the extension, then the last // component doesn't have one. - if (path->chars[i] == '/') return; + if (isSeparator(path->chars[i])) return; if (path->chars[i] == '.') { @@ -98,7 +167,7 @@ void pathRemoveExtension(Path* path) void pathJoin(Path* path, const char* string) { - if (path->length > 0 && path->chars[path->length - 1] != '/') + if (path->length > 0 && !isSeparator(path->chars[path->length - 1])) { pathAppendChar(path, '/'); } @@ -106,37 +175,6 @@ void pathJoin(Path* path, const char* string) pathAppendString(path, string); } - -#ifdef _WIN32 -static bool isDriveLetter(char c) -{ - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); -} -#endif - -bool pathIsAbsolute(Path* path) -{ -#ifdef _WIN32 - // Absolute path in the current drive. - if (path->length >= 1 && path->chars[0] == '\\') return true; - - // Drive letter. - if (path->length >= 3 && - isDriveLetter(path->chars[0]) && - path->chars[1] == ':' && - path->chars[2] == '\\') - { - return true; - } - - // UNC path. - return path->length >= 2 && path->chars[0] == '\\' && path->chars[1] == '\\'; -#else - // Otherwise, assume POSIX-style paths. - return path->length >= 1 && path->chars[0] == '/'; -#endif -} - void pathAppendChar(Path* path, char c) { ensureCapacity(path, path->length + 1); @@ -152,10 +190,8 @@ void pathAppendString(Path* path, const char* string) appendSlice(path, slice); } -Path* pathNormalize(Path* path) +void pathNormalize(Path* path) { - Path* result = pathNew(""); - // Split the path into components. Slice components[MAX_COMPONENTS]; int numComponents = 0; @@ -167,7 +203,7 @@ Path* pathNormalize(Path* path) int leadingDoubles = 0; for (;;) { - if (*end == '\0' || *end == '/') + if (*end == '\0' || isSeparator(*end)) { // Add the current component. if (start != end) @@ -207,7 +243,7 @@ Path* pathNormalize(Path* path) } // Skip over separators. - while (*end != '\0' && *end == '/') end++; + while (*end != '\0' && isSeparator(*end)) end++; start = end; if (*end == '\0') break; @@ -216,13 +252,22 @@ Path* pathNormalize(Path* path) end++; } - // Preserve the absolute prefix, if any. + // Preserve the path type. We don't want to turn, say, "./foo" into "foo" + // because that changes the semantics of how that path is handled when used + // as an import string. bool needsSeparator = false; - if (path->length > 0 && path->chars[0] == '/') + + Path* result = pathNew(""); + size_t prefixLength = absolutePrefixLength(path->chars); + if (prefixLength > 0) { - pathAppendChar(result, '/'); + // It's an absolute path, so preserve the absolute prefix. + Slice slice; + slice.start = path->chars; + slice.end = path->chars + prefixLength; + appendSlice(result, slice); } - else + else if (leadingDoubles > 0) { // Add any leading "..". for (int i = 0; i < leadingDoubles; i++) @@ -232,6 +277,13 @@ Path* pathNormalize(Path* path) needsSeparator = true; } } + else if (path->chars[0] == '.' && isSeparator(path->chars[1])) + { + // Preserve a leading "./", since we use that to distinguish relative from + // logical imports. + pathAppendChar(result, '.'); + needsSeparator = true; + } for (int i = 0; i < numComponents; i++) { @@ -242,7 +294,11 @@ Path* pathNormalize(Path* path) if (result->length == 0) pathAppendChar(result, '.'); - return result; + // Copy back into the original path. + free(path->chars); + path->capacity = result->capacity; + path->chars = result->chars; + path->length = result->length; } char* pathToString(Path* path) diff --git a/src/cli/path.h b/src/cli/path.h index 114346ee..e87bc5da 100644 --- a/src/cli/path.h +++ b/src/cli/path.h @@ -16,6 +16,22 @@ typedef struct size_t capacity; } Path; +// Categorizes what form a path is. +typedef enum +{ + // An absolute path, starting with "/" on POSIX systems, a drive letter on + // Windows, etc. + PATH_TYPE_ABSOLUTE, + + // An explicitly relative path, starting with "./" or "../". + PATH_TYPE_RELATIVE, + + // A path that has no leading prefix, like "foo/bar". + PATH_TYPE_SIMPLE, +} PathType; + +PathType pathType(const char* path); + // Creates a new empty path. Path* pathNew(const char* string); @@ -31,9 +47,6 @@ void pathRemoveExtension(Path* path); // Appends [string] to [path]. void pathJoin(Path* path, const char* string); -// Return true if [path] is an absolute path for the host operating system. -bool pathIsAbsolute(Path* path); - // Appends [c] to the path, growing the buffer if needed. void pathAppendChar(Path* path, char c); @@ -43,8 +56,8 @@ void pathAppendString(Path* path, const char* string); // Simplifies the path string as much as possible. // // Applies and removes any "." or ".." components, collapses redundant "/" -// characters, etc. -Path* pathNormalize(Path* path); +// characters, and normalizes all path separators to "/". +void pathNormalize(Path* path); // Allocates a new string exactly the right length and copies this path to it. char* pathToString(Path* path); diff --git a/src/cli/stat.h b/src/cli/stat.h new file mode 100644 index 00000000..f54e6b6f --- /dev/null +++ b/src/cli/stat.h @@ -0,0 +1,22 @@ +#ifndef stat_h +#define stat_h + +// Utilities to smooth over working with stat() in a cross-platform way. + +// Windows doesn't define all of the Unix permission and mode flags by default, +// so map them ourselves. +#if defined(WIN32) || defined(WIN64) + #include + + // Map to Windows permission flags. + #define S_IRUSR _S_IREAD + #define S_IWUSR _S_IWRITE + + #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) + #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) + + // Not supported on Windows. + #define O_SYNC 0 +#endif + +#endif diff --git a/src/cli/vm.c b/src/cli/vm.c index 863e85e6..51ce1d17 100644 --- a/src/cli/vm.c +++ b/src/cli/vm.c @@ -5,6 +5,7 @@ #include "modules.h" #include "path.h" #include "scheduler.h" +#include "stat.h" #include "vm.h" // The single VM instance that the CLI uses. @@ -19,6 +20,7 @@ static uv_loop_t* loop; // TODO: This isn't currently used, but probably will be when package imports // are supported. If not then, then delete this. static char* rootDirectory = NULL; +static Path* wrenModulesDirectory = NULL; // The exit code to use unless some other error overrides it. int defaultExitCode = 0; @@ -61,46 +63,99 @@ static char* readFile(const char* path) return buffer; } +static bool isDirectory(Path* path) +{ + uv_fs_t request; + uv_fs_stat(loop, &request, path->chars, NULL); + // TODO: Check request.result value? + + bool result = request.result == 0 && S_ISDIR(request.statbuf.st_mode); + + uv_fs_req_cleanup(&request); + return result; +} + +static Path* realPath(Path* path) +{ + uv_fs_t request; + uv_fs_realpath(loop, &request, path->chars, NULL); + + Path* result = pathNew((char*)request.ptr); + + uv_fs_req_cleanup(&request); + return result; +} + +// Starting at [rootDirectory], walks up containing directories looking for a +// nearby "wren_modules" directory. If found, stores it in +// [wrenModulesDirectory]. +// +// If [wrenModulesDirectory] has already been found, does nothing. +static void findModulesDirectory() +{ + if (wrenModulesDirectory != NULL) return; + + Path* searchDirectory = pathNew(rootDirectory); + Path* lastPath = realPath(searchDirectory); + + // Keep walking up directories as long as we find them. + for (;;) + { + Path* modulesDirectory = pathNew(searchDirectory->chars); + pathJoin(modulesDirectory, "wren_modules"); + + if (isDirectory(modulesDirectory)) + { + pathNormalize(modulesDirectory); + wrenModulesDirectory = modulesDirectory; + pathFree(lastPath); + break; + } + + pathFree(modulesDirectory); + + // Walk up directories until we hit the root. We can tell that because + // adding ".." yields the same real path. + pathJoin(searchDirectory, ".."); + Path* thisPath = realPath(searchDirectory); + if (strcmp(lastPath->chars, thisPath->chars) == 0) + { + pathFree(thisPath); + break; + } + + pathFree(lastPath); + lastPath = thisPath; + } + + pathFree(searchDirectory); +} + // Applies the CLI's import resolution policy. The rules are: // -// * If [name] starts with "./" or "../", it is a relative import, relative to -// [importer]. The resolved path is [name] concatenated onto the directory +// * If [module] starts with "./" or "../", it is a relative import, relative +// to [importer]. The resolved path is [name] concatenated onto the directory // containing [importer] and then normalized. // -// For example, importing "./a/./b/../c" from "d/e/f" gives you "d/e/a/c". -// -// * Otherwise, it is a "package" import. This isn't implemented yet. -// +// For example, importing "./a/./b/../c" from "./d/e/f" gives you "./d/e/a/c". static const char* resolveModule(WrenVM* vm, const char* importer, - const char* name) + const char* module) { - size_t nameLength = strlen(name); + // Logical import strings are used as-is and need no resolution. + if (pathType(module) == PATH_TYPE_SIMPLE) return module; - // See if it's a relative import. - if (nameLength > 2 && - ((name[0] == '.' && name[1] == '/') || - (name[0] == '.' && name[1] == '.' && name[2] == '/'))) - { - // Get the directory containing the importing module. - Path* relative = pathNew(importer); - pathDirName(relative); - - // Add the relative import path. - pathJoin(relative, name); - Path* normal = pathNormalize(relative); - pathFree(relative); - - char* resolved = pathToString(normal); - pathFree(normal); - return resolved; - } - else - { - // TODO: Implement package imports. For now, treat any non-relative import - // as an import relative to the current working directory. - } + // Get the directory containing the importing module. + Path* path = pathNew(importer); + pathDirName(path); - return name; + // Add the relative import path. + pathJoin(path, module); + + pathNormalize(path); + char* resolved = pathToString(path); + + pathFree(path); + return resolved; } // Attempts to read the source for [module] relative to the current root @@ -110,23 +165,40 @@ static const char* resolveModule(WrenVM* vm, const char* importer, // module was found but could not be read. static char* readModule(WrenVM* vm, const char* module) { - // Since the module has already been resolved, it should now be either a - // valid relative path, or a package-style name. - - // TODO: Implement package imports. + Path* filePath; + if (pathType(module) == PATH_TYPE_SIMPLE) + { + // If there is no "wren_modules" directory, then the only logical imports + // we can handle are built-in ones. Let the VM try to handle it. + findModulesDirectory(); + if (wrenModulesDirectory == NULL) return readBuiltInModule(module); + + // TODO: Should we explicitly check for the existence of the module's base + // directory inside "wren_modules" here? + + // Look up the module in "wren_modules". + filePath = pathNew(wrenModulesDirectory->chars); + pathJoin(filePath, module); + + // If the module is a single bare name, treat it as a module with the same + // name inside the package. So "foo" means "foo/foo". + if (strchr(module, '/') == NULL) pathJoin(filePath, module); + } + else + { + // The module path is already a file path. + filePath = pathNew(module); + } // Add a ".wren" file extension. - Path* modulePath = pathNew(module); - pathAppendString(modulePath, ".wren"); - - char* source = readFile(modulePath->chars); - pathFree(modulePath); - - if (source != NULL) return source; + pathAppendString(filePath, ".wren"); - // TODO: This used to look for a file named "/module.wren" if - // ".wren" could not be found. Do we still want to support that with - // the new relative import and package stuff? + char* source = readFile(filePath->chars); + pathFree(filePath); + + // If we didn't find it, it may be a module built into the CLI or VM, so keep + // going. + if (source != NULL) return source; // Otherwise, see if it's a built-in module. return readBuiltInModule(module); @@ -222,9 +294,11 @@ static void freeVM() wrenFreeVM(vm); uv_tty_reset_mode(); + + if (wrenModulesDirectory != NULL) pathFree(wrenModulesDirectory); } -void runFile(const char* path) +WrenInterpretResult runFile(const char* path) { char* source = readFile(path); if (source == NULL) @@ -233,19 +307,36 @@ void runFile(const char* path) exit(66); } + // If it looks like a relative path, make it explicitly relative so that we + // can distinguish it from logical paths. + // TODO: It might be nice to be able to run scripts from within a surrounding + // "wren_modules" directory by passing in a simple path like "foo/bar". In + // that case, here, we could check to see whether the give path exists inside + // "wren_modules" or as a relative path and choose to add "./" or not based + // on that. + Path* module = pathNew(path); + if (pathType(module->chars) == PATH_TYPE_SIMPLE) + { + Path* relative = pathNew("."); + pathJoin(relative, path); + + pathFree(module); + module = relative; + } + + pathRemoveExtension(module); + // Use the directory where the file is as the root to resolve imports // relative to. - Path* directory = pathNew(path); + Path* directory = pathNew(module->chars); + pathDirName(directory); rootDirectory = pathToString(directory); pathFree(directory); - Path* moduleName = pathNew(path); - pathRemoveExtension(moduleName); - initVM(); - WrenInterpretResult result = wrenInterpret(vm, moduleName->chars, source); + WrenInterpretResult result = wrenInterpret(vm, module->chars, source); if (afterLoadFn != NULL) afterLoadFn(vm); @@ -258,29 +349,29 @@ void runFile(const char* path) free(source); free(rootDirectory); - pathFree(moduleName); + pathFree(module); - // Exit with an error code if the script failed. - if (result == WREN_RESULT_COMPILE_ERROR) exit(65); // EX_DATAERR. - if (result == WREN_RESULT_RUNTIME_ERROR) exit(70); // EX_SOFTWARE. - - if (defaultExitCode != 0) exit(defaultExitCode); + return result; } -int runRepl() +WrenInterpretResult runRepl() { + rootDirectory = "."; initVM(); printf("\\\\/\"-\n"); printf(" \\_/ wren v%s\n", WREN_VERSION_STRING); - wrenInterpret(vm, "repl", "import \"repl\"\n"); + WrenInterpretResult result = wrenInterpret(vm, "", "import \"repl\"\n"); - uv_run(loop, UV_RUN_DEFAULT); + if (result == WREN_RESULT_SUCCESS) + { + uv_run(loop, UV_RUN_DEFAULT); + } freeVM(); - - return 0; + + return result; } WrenVM* getVM() @@ -293,6 +384,11 @@ uv_loop_t* getLoop() return loop; } +int getExitCode() +{ + return defaultExitCode; +} + void setExitCode(int exitCode) { defaultExitCode = exitCode; diff --git a/src/cli/vm.h b/src/cli/vm.h index 6018dcee..cb5f8d15 100644 --- a/src/cli/vm.h +++ b/src/cli/vm.h @@ -5,12 +5,10 @@ #include "wren.h" // Executes the Wren script at [path] in a new VM. -// -// Exits if the script failed or could not be loaded. -void runFile(const char* path); +WrenInterpretResult runFile(const char* path); // Runs the Wren interactive REPL. -int runRepl(); +WrenInterpretResult runRepl(); // Gets the currently running VM. WrenVM* getVM(); @@ -18,6 +16,9 @@ WrenVM* getVM(); // Gets the event loop the VM is using. uv_loop_t* getLoop(); +// Get the exit code the CLI should exit with when done. +int getExitCode(); + // Set the exit code the CLI should exit with when done. void setExitCode(int exitCode); diff --git a/src/module/io.c b/src/module/io.c index ee4ee895..ac5a5f6d 100644 --- a/src/module/io.c +++ b/src/module/io.c @@ -4,28 +4,13 @@ #include "uv.h" #include "scheduler.h" +#include "stat.h" #include "vm.h" #include "wren.h" #include #include -// Windows doesn't define all of the Unix permission and mode flags by default, -// so map them ourselves. -#if defined(WIN32) || defined(WIN64) - #include - - // Map to Windows permission flags. - #define S_IRUSR _S_IREAD - #define S_IWUSR _S_IWRITE - - #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) - #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) - - // Not supported on Windows. - #define O_SYNC 0 -#endif - typedef struct sFileRequestData { WrenHandle* fiber; diff --git a/src/vm/wren_vm.c b/src/vm/wren_vm.c index 0afb9123..dc016f00 100644 --- a/src/vm/wren_vm.c +++ b/src/vm/wren_vm.c @@ -723,7 +723,7 @@ static Value resolveModule(WrenVM* vm, Value name) return name; } -Value wrenImportModule(WrenVM* vm, Value name) +static Value importModule(WrenVM* vm, Value name) { name = resolveModule(vm, name); @@ -1308,7 +1308,7 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) { Value name = fn->constants.data[READ_SHORT()]; - Value result = wrenImportModule(vm, name); + Value result = importModule(vm, name); if (!IS_NULL(fiber->error)) RUNTIME_ERROR(); // Make a slot on the stack for the module's closure to place the return diff --git a/test/api/call.c b/test/api/call.c index 7f4264e2..f8d5c7d7 100644 --- a/test/api/call.c +++ b/test/api/call.c @@ -6,7 +6,7 @@ void callRunTests(WrenVM* vm) { wrenEnsureSlots(vm, 1); - wrenGetVariable(vm, "test/api/call", "Call", 0); + wrenGetVariable(vm, "./test/api/call", "Call", 0); WrenHandle* callClass = wrenGetSlotHandle(vm, 0); WrenHandle* noParams = wrenMakeCallHandle(vm, "noParams"); diff --git a/test/api/get_variable.c b/test/api/get_variable.c index 5dece4cf..b6d1d706 100644 --- a/test/api/get_variable.c +++ b/test/api/get_variable.c @@ -4,23 +4,23 @@ static void beforeDefined(WrenVM* vm) { - wrenGetVariable(vm, "test/api/get_variable", "A", 0); + wrenGetVariable(vm, "./test/api/get_variable", "A", 0); } static void afterDefined(WrenVM* vm) { - wrenGetVariable(vm, "test/api/get_variable", "A", 0); + wrenGetVariable(vm, "./test/api/get_variable", "A", 0); } static void afterAssigned(WrenVM* vm) { - wrenGetVariable(vm, "test/api/get_variable", "A", 0); + wrenGetVariable(vm, "./test/api/get_variable", "A", 0); } static void otherSlot(WrenVM* vm) { wrenEnsureSlots(vm, 3); - wrenGetVariable(vm, "test/api/get_variable", "B", 2); + wrenGetVariable(vm, "./test/api/get_variable", "B", 2); // Move it into return position. const char* string = wrenGetSlotString(vm, 2); @@ -29,7 +29,7 @@ static void otherSlot(WrenVM* vm) static void otherModule(WrenVM* vm) { - wrenGetVariable(vm, "test/api/get_variable_module", "Variable", 0); + wrenGetVariable(vm, "./test/api/get_variable_module", "Variable", 0); } WrenForeignMethodFn getVariableBindMethod(const char* signature) diff --git a/test/api/main.c b/test/api/main.c index 52610afa..bf8445d1 100644 --- a/test/api/main.c +++ b/test/api/main.c @@ -25,7 +25,7 @@ static WrenForeignMethodFn bindForeignMethod( WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) { - if (strncmp(module, "test/", 5) != 0) return NULL; + if (strncmp(module, "./test/", 7) != 0) return NULL; // For convenience, concatenate all of the method qualifiers into a single // signature string. @@ -78,7 +78,7 @@ static WrenForeignClassMethods bindForeignClass( WrenVM* vm, const char* module, const char* className) { WrenForeignClassMethods methods = { NULL, NULL }; - if (strncmp(module, "test/", 5) != 0) return methods; + if (strncmp(module, "./test/", 7) != 0) return methods; foreignClassBindClass(className, &methods); if (methods.allocate != NULL) return methods; diff --git a/test/api/reset_stack_after_call_abort.c b/test/api/reset_stack_after_call_abort.c index c94687fe..a4d09af3 100644 --- a/test/api/reset_stack_after_call_abort.c +++ b/test/api/reset_stack_after_call_abort.c @@ -6,7 +6,7 @@ void resetStackAfterCallAbortRunTests(WrenVM* vm) { wrenEnsureSlots(vm, 1); - wrenGetVariable(vm, "test/api/reset_stack_after_call_abort", "Test", 0); + wrenGetVariable(vm, "./test/api/reset_stack_after_call_abort", "Test", 0); WrenHandle* testClass = wrenGetSlotHandle(vm, 0); WrenHandle* abortFiber = wrenMakeCallHandle(vm, "abortFiber()"); diff --git a/test/api/reset_stack_after_foreign_construct.c b/test/api/reset_stack_after_foreign_construct.c index ce829a8b..52e1f720 100644 --- a/test/api/reset_stack_after_foreign_construct.c +++ b/test/api/reset_stack_after_foreign_construct.c @@ -22,7 +22,8 @@ void resetStackAfterForeignConstructBindClass( void resetStackAfterForeignConstructRunTests(WrenVM* vm) { wrenEnsureSlots(vm, 1); - wrenGetVariable(vm, "test/api/reset_stack_after_foreign_construct", "Test", 0); + wrenGetVariable(vm, + "./test/api/reset_stack_after_foreign_construct", "Test", 0); WrenHandle* testClass = wrenGetSlotHandle(vm, 0); WrenHandle* callConstruct = wrenMakeCallHandle(vm, "callConstruct()"); diff --git a/test/language/foreign/unknown_method.wren b/test/language/foreign/unknown_method.wren index 2cdf01be..1c38ed8c 100644 --- a/test/language/foreign/unknown_method.wren +++ b/test/language/foreign/unknown_method.wren @@ -1,3 +1,3 @@ class Foo { - foreign someUnknownMethod // expect runtime error: Could not find foreign method 'someUnknownMethod' for class Foo in module 'test/language/foreign/unknown_method'. + foreign someUnknownMethod // expect runtime error: Could not find foreign method 'someUnknownMethod' for class Foo in module './test/language/foreign/unknown_method'. } diff --git a/test/language/module/compile_error/compile_error.wren b/test/language/module/compile_error/compile_error.wren index f136e882..8c8df453 100644 --- a/test/language/module/compile_error/compile_error.wren +++ b/test/language/module/compile_error/compile_error.wren @@ -1,2 +1,2 @@ System.print("before") // expect: before -import "./module" for Module // expect runtime error: Could not compile module 'test/language/module/compile_error/module'. +import "./module" for Module // expect runtime error: Could not compile module './test/language/module/compile_error/module'. diff --git a/test/language/module/logical_dir/logical_dir.wren b/test/language/module/logical_dir/logical_dir.wren new file mode 100644 index 00000000..421143fa --- /dev/null +++ b/test/language/module/logical_dir/logical_dir.wren @@ -0,0 +1,6 @@ +// Import a module from within a named package. +import "foo/within_foo" for Foo +// expect: ran foo module +// expect: ran bar module + +System.print(Foo) // expect: from foo diff --git a/test/language/module/logical_dir/wren_modules/bar/within_bar.wren b/test/language/module/logical_dir/wren_modules/bar/within_bar.wren new file mode 100644 index 00000000..701bee91 --- /dev/null +++ b/test/language/module/logical_dir/wren_modules/bar/within_bar.wren @@ -0,0 +1,5 @@ +// nontest +var Bar = "from bar" +System.print("ran bar module") + +import "foo/within_foo" diff --git a/test/language/module/logical_dir/wren_modules/foo/within_foo.wren b/test/language/module/logical_dir/wren_modules/foo/within_foo.wren new file mode 100644 index 00000000..4d55dd19 --- /dev/null +++ b/test/language/module/logical_dir/wren_modules/foo/within_foo.wren @@ -0,0 +1,5 @@ +// nontest +var Foo = "from foo" +System.print("ran foo module") + +import "bar/within_bar" diff --git a/test/language/module/logical_short/logical_short.wren b/test/language/module/logical_short/logical_short.wren new file mode 100644 index 00000000..baaa5d66 --- /dev/null +++ b/test/language/module/logical_short/logical_short.wren @@ -0,0 +1,5 @@ +// Import a module whose name is the same as the package. +import "foo" for Module +// expect: ran module + +System.print(Module) // expect: from module diff --git a/test/language/module/logical_short/wren_modules/foo/foo.wren b/test/language/module/logical_short/wren_modules/foo/foo.wren new file mode 100644 index 00000000..a1289a13 --- /dev/null +++ b/test/language/module/logical_short/wren_modules/foo/foo.wren @@ -0,0 +1,3 @@ +// nontest +var Module = "from module" +System.print("ran module") diff --git a/test/language/module/unknown_module.wren b/test/language/module/unknown_module.wren index a80cc297..1cc88d13 100644 --- a/test/language/module/unknown_module.wren +++ b/test/language/module/unknown_module.wren @@ -1 +1 @@ -import "./does_not_exist" for DoesNotExist // expect runtime error: Could not load module 'test/language/module/does_not_exist'. +import "./does_not_exist" for DoesNotExist // expect runtime error: Could not load module './test/language/module/does_not_exist'. diff --git a/test/language/module/unknown_variable/unknown_variable.wren b/test/language/module/unknown_variable/unknown_variable.wren index 23f347c9..dc172f45 100644 --- a/test/language/module/unknown_variable/unknown_variable.wren +++ b/test/language/module/unknown_variable/unknown_variable.wren @@ -1,3 +1,3 @@ // Should execute the module: // expect: ran module -import "./module" for DoesNotExist // expect runtime error: Could not find a variable named 'DoesNotExist' in module 'test/language/module/unknown_variable/module'. +import "./module" for DoesNotExist // expect runtime error: Could not find a variable named 'DoesNotExist' in module './test/language/module/unknown_variable/module'. diff --git a/test/language/module/use_nearest_modules_dir/a/b/use_nearest_modules_dir.wren b/test/language/module/use_nearest_modules_dir/a/b/use_nearest_modules_dir.wren new file mode 100644 index 00000000..b9208661 --- /dev/null +++ b/test/language/module/use_nearest_modules_dir/a/b/use_nearest_modules_dir.wren @@ -0,0 +1,3 @@ +// Stops as soon as it finds a wren_modules directory, regardless of whether or +// not it contains the desired module. +import "foo" // expect runtime error: Could not load module 'foo'. diff --git a/test/language/module/use_nearest_modules_dir/wren_modules/foo/foo.wren b/test/language/module/use_nearest_modules_dir/wren_modules/foo/foo.wren new file mode 100644 index 00000000..21d3f865 --- /dev/null +++ b/test/language/module/use_nearest_modules_dir/wren_modules/foo/foo.wren @@ -0,0 +1,2 @@ +// nontest +System.print("ran foo module") diff --git a/test/language/module/walk_up_for_modules_dir/a/b/walk_up_for_modules_dir.wren b/test/language/module/walk_up_for_modules_dir/a/b/walk_up_for_modules_dir.wren new file mode 100644 index 00000000..345c728d --- /dev/null +++ b/test/language/module/walk_up_for_modules_dir/a/b/walk_up_for_modules_dir.wren @@ -0,0 +1,3 @@ +// Walk up parent directories from the root script to find "wren_modules". +import "foo" +// expect: ran foo module diff --git a/test/language/module/walk_up_for_modules_dir/wren_modules/foo/foo.wren b/test/language/module/walk_up_for_modules_dir/wren_modules/foo/foo.wren new file mode 100644 index 00000000..21d3f865 --- /dev/null +++ b/test/language/module/walk_up_for_modules_dir/wren_modules/foo/foo.wren @@ -0,0 +1,2 @@ +// nontest +System.print("ran foo module") diff --git a/test/meta/get_module_variables.wren b/test/meta/get_module_variables.wren index 34dc6a7d..0347b344 100644 --- a/test/meta/get_module_variables.wren +++ b/test/meta/get_module_variables.wren @@ -1,6 +1,6 @@ import "meta" for Meta -var variables = Meta.getModuleVariables("test/meta/get_module_variables") +var variables = Meta.getModuleVariables("./test/meta/get_module_variables") // Includes implicitly imported core stuff. System.print(variables.contains("Object")) // expect: true diff --git a/test/unit/path_test.c b/test/unit/path_test.c index a175dc49..14ce2ac9 100644 --- a/test/unit/path_test.c +++ b/test/unit/path_test.c @@ -9,40 +9,39 @@ static void expectNormalize(const char* input, const char* expected) { Path* path = pathNew(input); - Path* result = pathNormalize(path); + pathNormalize(path); - if (strcmp(result->chars, expected) != 0) + if (strcmp(path->chars, expected) != 0) { printf("FAIL %-30s Want %s\n", input, expected); - printf(" Got %s\n\n", result->chars); + printf(" Got %s\n\n", path->chars); fail(); } else { #if SHOW_PASSES - printf("PASS %-30s -> %s\n", input, result->chars); + printf("PASS %-30s -> %s\n", input, path->chars); #endif pass(); } pathFree(path); - pathFree(result); } static void testNormalize() { - // simple cases + // Simple cases. expectNormalize("", "."); expectNormalize(".", "."); expectNormalize("..", ".."); expectNormalize("a", "a"); expectNormalize("/", "/"); - // collapses redundant separators + // Collapses redundant separators. expectNormalize("a/b/c", "a/b/c"); expectNormalize("a//b///c////d", "a/b/c/d"); - // eliminates "." parts + // Eliminates "." parts, except one at the beginning. expectNormalize("./", "."); expectNormalize("/.", "/"); expectNormalize("/./", "/"); @@ -50,10 +49,10 @@ static void testNormalize() expectNormalize("a/./b", "a/b"); expectNormalize("a/.b/c", "a/.b/c"); expectNormalize("a/././b/./c", "a/b/c"); - expectNormalize("././a", "a"); + expectNormalize("././a", "./a"); expectNormalize("a/./.", "a"); - // eliminates ".." parts + // Eliminates ".." parts. expectNormalize("..", ".."); expectNormalize("../", ".."); expectNormalize("../../..", "../../.."); @@ -68,7 +67,7 @@ static void testNormalize() expectNormalize("a/b/c/../../d/e/..", "a/d"); expectNormalize("a/b/../../../../c", "../../c"); - // does not walk before root on absolute paths + // Does not walk before root on absolute paths. expectNormalize("..", ".."); expectNormalize("../", ".."); expectNormalize("/..", "/"); @@ -84,7 +83,7 @@ static void testNormalize() expectNormalize("a/b/../../../../c", "../../c"); expectNormalize("a/b/c/../../..d/./.e/f././", "a/..d/.e/f."); - // removes trailing separators + // Removes trailing separators. expectNormalize("./", "."); expectNormalize(".//", "."); expectNormalize("a/", "a"); @@ -94,7 +93,7 @@ static void testNormalize() expectNormalize("foo/bar/baz", "foo/bar/baz"); expectNormalize("foo", "foo"); expectNormalize("foo/bar/", "foo/bar"); - expectNormalize("./foo/././bar/././", "foo/bar"); + expectNormalize("./foo/././bar/././", "./foo/bar"); } void testPath() diff --git a/util/test.py b/util/test.py index adfa9b62..7010f431 100755 --- a/util/test.py +++ b/util/test.py @@ -32,7 +32,7 @@ EXPECT_ERROR_PATTERN = re.compile(r'// expect error(?! line)') EXPECT_ERROR_LINE_PATTERN = re.compile(r'// expect error line (\d+)') EXPECT_RUNTIME_ERROR_PATTERN = re.compile(r'// expect (handled )?runtime error: (.+)') ERROR_PATTERN = re.compile(r'\[.* line (\d+)\] Error') -STACK_TRACE_PATTERN = re.compile(r'\[test/.* line (\d+)\] in') +STACK_TRACE_PATTERN = re.compile(r'\[\./test/.* line (\d+)\] in') STDIN_PATTERN = re.compile(r'// stdin: (.*)') SKIP_PATTERN = re.compile(r'// skip: (.*)') NONTEST_PATTERN = re.compile(r'// nontest') diff --git a/util/xcode/wren.xcodeproj/project.pbxproj b/util/xcode/wren.xcodeproj/project.pbxproj index 3e729748..4250c61d 100644 --- a/util/xcode/wren.xcodeproj/project.pbxproj +++ b/util/xcode/wren.xcodeproj/project.pbxproj @@ -24,10 +24,7 @@ 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 */; }; - 2940E9BB2066067D0054503C /* path_test.c in Sources */ = {isa = PBXBuildFile; fileRef = 2940E9BA2066067D0054503C /* path_test.c */; }; 2940E9BC206607830054503C /* path.c in Sources */ = {isa = PBXBuildFile; fileRef = 2952CD1B1FA9941700985F5F /* path.c */; }; - 2940E9BE2066C3300054503C /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 2940E9BD2066C3300054503C /* main.c */; }; - 2940E9C12066C35E0054503C /* test.c in Sources */ = {isa = PBXBuildFile; fileRef = 2940E9BF2066C35E0054503C /* test.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 */; }; @@ -47,6 +44,9 @@ 29A4273A1BDBE435001E6E22 /* wren_opt_random.wren.inc in Sources */ = {isa = PBXBuildFile; fileRef = 29A427331BDBE435001E6E22 /* wren_opt_random.wren.inc */; }; 29A4273B1BDBE435001E6E22 /* wren_opt_random.wren.inc in Sources */ = {isa = PBXBuildFile; fileRef = 29A427331BDBE435001E6E22 /* wren_opt_random.wren.inc */; }; 29AD96611D0A57F800C4DFE7 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 29AD965F1D0A57F800C4DFE7 /* error.c */; }; + 29B59F0820FC37B700767E48 /* path_test.c in Sources */ = {isa = PBXBuildFile; fileRef = 29B59F0320FC37B600767E48 /* path_test.c */; }; + 29B59F0920FC37B700767E48 /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 29B59F0420FC37B700767E48 /* main.c */; }; + 29B59F0A20FC37B700767E48 /* test.c in Sources */ = {isa = PBXBuildFile; fileRef = 29B59F0520FC37B700767E48 /* test.c */; }; 29C80D5A1D73332A00493837 /* reset_stack_after_foreign_construct.c in Sources */ = {isa = PBXBuildFile; fileRef = 29C80D581D73332A00493837 /* reset_stack_after_foreign_construct.c */; }; 29C8A9331AB71FFF00DEC81D /* vm.c in Sources */ = {isa = PBXBuildFile; fileRef = 29C8A9311AB71FFF00DEC81D /* vm.c */; }; 29C946981C88F14F00B4A4F3 /* new_vm.c in Sources */ = {isa = PBXBuildFile; fileRef = 29C946961C88F14F00B4A4F3 /* new_vm.c */; }; @@ -133,11 +133,6 @@ 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 = ""; }; 2940E9B8206605DE0054503C /* unit_test */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = unit_test; sourceTree = BUILT_PRODUCTS_DIR; }; - 2940E9B92066067D0054503C /* path_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = path_test.h; path = ../../../test/unit/path_test.h; sourceTree = ""; }; - 2940E9BA2066067D0054503C /* path_test.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = path_test.c; path = ../../../test/unit/path_test.c; sourceTree = ""; }; - 2940E9BD2066C3300054503C /* main.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = main.c; path = ../../../test/unit/main.c; sourceTree = ""; }; - 2940E9BF2066C35E0054503C /* test.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = test.c; path = ../../../test/unit/test.c; sourceTree = ""; }; - 2940E9C02066C35E0054503C /* test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = test.h; path = ../../../test/unit/test.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; }; @@ -145,6 +140,7 @@ 2952CD1B1FA9941700985F5F /* path.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = path.c; path = ../../src/cli/path.c; sourceTree = ""; }; 2952CD1C1FA9941700985F5F /* path.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = path.h; path = ../../src/cli/path.h; sourceTree = ""; }; 296371B31AC713D000079FDA /* wren_opcodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = wren_opcodes.h; path = ../../src/vm/wren_opcodes.h; sourceTree = ""; }; + 29703E57206DC7B7004004DC /* stat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = stat.h; path = ../../src/cli/stat.h; sourceTree = ""; }; 29729F2E1BA70A620099CA20 /* io.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = io.c; path = ../../src/module/io.c; sourceTree = ""; }; 29729F301BA70A620099CA20 /* io.wren.inc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.pascal; name = io.wren.inc; path = ../../src/module/io.wren.inc; sourceTree = ""; }; 2986F6D51ACF93BA00BCE26C /* wren_primitive.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = wren_primitive.c; path = ../../src/vm/wren_primitive.c; sourceTree = ""; }; @@ -162,6 +158,11 @@ 29AB1F061816E3AD004B501E /* wren */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = wren; sourceTree = BUILT_PRODUCTS_DIR; }; 29AD965F1D0A57F800C4DFE7 /* error.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = error.c; path = ../../test/api/error.c; sourceTree = ""; }; 29AD96601D0A57F800C4DFE7 /* error.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = error.h; path = ../../test/api/error.h; sourceTree = ""; }; + 29B59F0320FC37B600767E48 /* path_test.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = path_test.c; path = ../../test/unit/path_test.c; sourceTree = ""; }; + 29B59F0420FC37B700767E48 /* main.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = main.c; path = ../../test/unit/main.c; sourceTree = ""; }; + 29B59F0520FC37B700767E48 /* test.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = test.c; path = ../../test/unit/test.c; sourceTree = ""; }; + 29B59F0620FC37B700767E48 /* path_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = path_test.h; path = ../../test/unit/path_test.h; sourceTree = ""; }; + 29B59F0720FC37B700767E48 /* test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = test.h; path = ../../test/unit/test.h; sourceTree = ""; }; 29C80D581D73332A00493837 /* reset_stack_after_foreign_construct.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = reset_stack_after_foreign_construct.c; path = ../../test/api/reset_stack_after_foreign_construct.c; sourceTree = ""; }; 29C80D591D73332A00493837 /* reset_stack_after_foreign_construct.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = reset_stack_after_foreign_construct.h; path = ../../test/api/reset_stack_after_foreign_construct.h; sourceTree = ""; }; 29C8A9311AB71FFF00DEC81D /* vm.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = vm.c; path = ../../src/cli/vm.c; sourceTree = ""; }; @@ -264,6 +265,7 @@ 291647C51BA5EC5E006142EE /* modules.c */, 2952CD1C1FA9941700985F5F /* path.h */, 2952CD1B1FA9941700985F5F /* path.c */, + 29703E57206DC7B7004004DC /* stat.h */, 29C8A9321AB71FFF00DEC81D /* vm.h */, 29C8A9311AB71FFF00DEC81D /* vm.c */, ); @@ -278,18 +280,6 @@ name = include; sourceTree = ""; }; - 2940E98E206605CB0054503C /* unit_test */ = { - isa = PBXGroup; - children = ( - 2940E9BD2066C3300054503C /* main.c */, - 2940E9BA2066067D0054503C /* path_test.c */, - 2940E9B92066067D0054503C /* path_test.h */, - 2940E9BF2066C35E0054503C /* test.c */, - 2940E9C02066C35E0054503C /* test.h */, - ); - path = unit_test; - sourceTree = ""; - }; 29AB1EFD1816E3AD004B501E = { isa = PBXGroup; children = ( @@ -299,7 +289,7 @@ 29AF31EE1BD2E37F00AAD156 /* optional */, 29205CA01AB4E6470073018D /* vm */, 29D0099A1B7E394F000CE58C /* api_test */, - 2940E98E206605CB0054503C /* unit_test */, + 29B59F0B20FC37BD00767E48 /* unit_test */, 29512C801B91F8EB008C10E6 /* libuv.a */, 29AB1F071816E3AD004B501E /* Products */, ); @@ -328,6 +318,18 @@ name = optional; sourceTree = ""; }; + 29B59F0B20FC37BD00767E48 /* unit_test */ = { + isa = PBXGroup; + children = ( + 29B59F0420FC37B700767E48 /* main.c */, + 29B59F0320FC37B600767E48 /* path_test.c */, + 29B59F0620FC37B700767E48 /* path_test.h */, + 29B59F0520FC37B700767E48 /* test.c */, + 29B59F0720FC37B700767E48 /* test.h */, + ); + name = unit_test; + sourceTree = ""; + }; 29D0099A1B7E394F000CE58C /* api_test */ = { isa = PBXGroup; children = ( @@ -454,10 +456,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 29B59F0820FC37B700767E48 /* path_test.c in Sources */, 2940E9BC206607830054503C /* path.c in Sources */, - 2940E9BB2066067D0054503C /* path_test.c in Sources */, - 2940E9BE2066C3300054503C /* main.c in Sources */, - 2940E9C12066C35E0054503C /* test.c in Sources */, + 29B59F0920FC37B700767E48 /* main.c in Sources */, + 29B59F0A20FC37B700767E48 /* test.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };