diff --git a/Makefile b/Makefile index 21791bdc..0b01324d 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: mkdir -p build @@ -105,4 +109,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/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/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/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 new file mode 100644 index 00000000..460cef07 --- /dev/null +++ b/src/cli/path.c @@ -0,0 +1,310 @@ +#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'; +} + +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)); + 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 (isSeparator(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 (isSeparator(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 && !isSeparator(path->chars[path->length - 1])) + { + pathAppendChar(path, '/'); + } + + pathAppendString(path, string); +} + +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); +} + +void pathNormalize(Path* path) +{ + // 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' || isSeparator(*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' && isSeparator(*end)) end++; + + start = end; + if (*end == '\0') break; + } + + end++; + } + + // 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; + + Path* result = pathNew(""); + size_t prefixLength = absolutePrefixLength(path->chars); + if (prefixLength > 0) + { + // 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 if (leadingDoubles > 0) + { + // Add any leading "..". + for (int i = 0; i < leadingDoubles; i++) + { + if (needsSeparator) pathAppendChar(result, '/'); + pathAppendString(result, ".."); + 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++) + { + if (needsSeparator) pathAppendChar(result, '/'); + appendSlice(result, components[i]); + needsSeparator = true; + } + + if (result->length == 0) pathAppendChar(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) +{ + 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..e87bc5da --- /dev/null +++ b/src/cli/path.h @@ -0,0 +1,65 @@ +#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; + +// 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); + +// 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); + +// 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, 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); + +#endif diff --git a/src/cli/stat.h b/src/cli/stat.h new file mode 100644 index 00000000..b333bce5 --- /dev/null +++ b/src/cli/stat.h @@ -0,0 +1,32 @@ +#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. + #ifndef S_IRUSR + #define S_IRUSR _S_IREAD + #endif + + #ifndef S_IWUSR + #define S_IWUSR _S_IWRITE + #endif + + #ifndef S_ISREG + #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) + #endif + + #ifndef S_ISDIR + #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) + #endif + + // Not supported on Windows. + #define O_SYNC 0 +#endif + +#endif diff --git a/src/cli/vm.c b/src/cli/vm.c index e49fca74..51ce1d17 100644 --- a/src/cli/vm.c +++ b/src/cli/vm.c @@ -3,7 +3,9 @@ #include "io.h" #include "modules.h" +#include "path.h" #include "scheduler.h" +#include "stat.h" #include "vm.h" // The single VM instance that the CLI uses. @@ -15,7 +17,10 @@ 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; +static Path* wrenModulesDirectory = NULL; // The exit code to use unless some other error overrides it. int defaultExitCode = 0; @@ -44,7 +49,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 +63,99 @@ static char* readFile(const char* path) return buffer; } -// Converts the module [name] to a file path. -static char* wrenFilePath(const char* name) +static bool isDirectory(Path* path) { - // 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); + uv_fs_t request; + uv_fs_stat(loop, &request, path->chars, NULL); + // TODO: Check request.result value? - if (rootDirectory != NULL) + 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 (;;) { - memcpy(path, rootDirectory, rootLength); + 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; } - memcpy(path + rootLength, name, nameLength); - memcpy(path + rootLength + nameLength, ".wren", 5); - path[pathLength] = '\0'; + pathFree(searchDirectory); +} + +// Applies the CLI's import resolution policy. The rules are: +// +// * 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". +static const char* resolveModule(WrenVM* vm, const char* importer, + const char* module) +{ + // Logical import strings are used as-is and need no resolution. + if (pathType(module) == PATH_TYPE_SIMPLE) return module; - return path; + // Get the directory containing the importing module. + Path* path = pathNew(importer); + pathDirName(path); + + // 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 @@ -86,32 +165,43 @@ 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); + 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. + pathAppendString(filePath, ".wren"); + + 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; - - // 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; + + // 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 +269,7 @@ static void initVM() config.bindForeignMethodFn = bindForeignMethod; config.bindForeignClassFn = bindForeignClass; + config.resolveModuleFn = resolveModule; config.loadModuleFn = readModule; config.writeFn = write; config.errorFn = reportError; @@ -203,22 +294,12 @@ static void freeVM() wrenFreeVM(vm); uv_tty_reset_mode(); + + if (wrenModulesDirectory != NULL) pathFree(wrenModulesDirectory); } -void runFile(const char* path) +WrenInterpretResult 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 +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(module->chars); + + pathDirName(directory); + rootDirectory = pathToString(directory); + pathFree(directory); + initVM(); - WrenInterpretResult result = wrenInterpret(vm, source); + WrenInterpretResult result = wrenInterpret(vm, module->chars, source); if (afterLoadFn != NULL) afterLoadFn(vm); @@ -240,29 +348,30 @@ void runFile(const char* path) freeVM(); free(source); - free(root); + free(rootDirectory); + 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, "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() @@ -275,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/include/wren.h b/src/include/wren.h index 2c91afa1..dbea3077 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); @@ -87,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); @@ -115,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); @@ -126,6 +132,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 +232,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. @@ -255,8 +288,10 @@ 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]. -WrenInterpretResult wrenInterpret(WrenVM* vm, const char* source); +// 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); // 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. @@ -435,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/src/module/io.c b/src/module/io.c index 7ef3d265..ac5a5f6d 100644 --- a/src/module/io.c +++ b/src/module/io.c @@ -4,38 +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. - #ifndef S_IRUSR - #define S_IRUSR _S_IREAD - #endif - - #ifndef S_IWUSR - #define S_IWUSR _S_IWRITE - #endif - - #ifndef S_ISREG - #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) - #endif - - #ifndef S_ISDIR - #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) - #endif - - // Not supported on Windows. - #define O_SYNC 0 -#endif - typedef struct sFileRequestData { WrenHandle* fiber; diff --git a/src/optional/wren_opt_meta.c b/src/optional/wren_opt_meta.c index ed05bf3c..d161cf28 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) @@ -29,7 +37,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 bd8ab724..adb261f6 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 @@ -2723,6 +2719,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: @@ -3325,9 +3322,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); @@ -3337,9 +3338,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; @@ -3348,16 +3349,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); @@ -3474,7 +3474,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_core.c b/src/vm/wren_core.c index f45823b3..56b6222c 100644 --- a/src/vm/wren_core.c +++ b/src/vm/wren_core.c @@ -1173,7 +1173,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_debug.c b/src/vm/wren_debug.c index 1558a0bd..d0b8b743 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 5a7c132b..cdeb869f 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; @@ -696,11 +697,45 @@ void wrenFinalizeForeign(WrenVM* vm, ObjForeign* foreign) finalizer(foreign->data); } -Value wrenImportModule(WrenVM* vm, Value name) +// Let the host resolve an imported module name if it wants to. +static Value resolveModule(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; + // 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; +} + +static Value importModule(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; + + wrenPushRoot(vm, AS_OBJ(name)); + const char* source = NULL; bool allocatedSource = true; @@ -729,6 +764,7 @@ Value wrenImportModule(WrenVM* vm, Value name) if (source == NULL) { vm->fiber->error = wrenStringFormat(vm, "Could not load module '@'.", name); + wrenPopRoot(vm); // name. return NULL_VAL; } @@ -743,13 +779,36 @@ Value wrenImportModule(WrenVM* vm, Value name) { vm->fiber->error = wrenStringFormat(vm, "Could not compile module '@'.", name); + wrenPopRoot(vm); // name. return NULL_VAL; } - + + wrenPopRoot(vm); // name. + // Return the closure that executes the module. 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. @@ -1247,18 +1306,22 @@ 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. Store the // imported module's closure in the slot in case a GC happens when // invoking the closure. - PUSH(wrenImportModule(vm, name)); - + PUSH(importModule(vm, fn->constants.data[READ_SHORT()])); if (!IS_NULL(fiber->error)) RUNTIME_ERROR(); - + // If we get a closure, call it to execute the module body. if (IS_CLOSURE(PEEK())) { @@ -1267,16 +1330,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(PEEK()); + } 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); @@ -1407,13 +1475,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; @@ -1452,21 +1515,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/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/call.c b/test/api/call.c index 8dadcc35..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, "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..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, "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 1557dd94..bf8445d1 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" @@ -23,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/", 7) != 0) return NULL; // For convenience, concatenate all of the method qualifiers into a single // signature string. @@ -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; @@ -74,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/", 7) != 0) return methods; foreignClassBindClass(className, &methods); if (methods.allocate != NULL) return methods; @@ -91,7 +95,8 @@ static WrenForeignClassMethods bindForeignClass( return methods; } -static void afterLoad(WrenVM* vm) { +static void afterLoad(WrenVM* vm) +{ if (strstr(testName, "/call.wren") != NULL) { callRunTests(vm); 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/reset_stack_after_call_abort.c b/test/api/reset_stack_after_call_abort.c index 8136796e..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, "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..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, "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 +42,4 @@ void resetStackAfterForeignConstructRunTests(WrenVM* vm) wrenReleaseHandle(vm, testClass); wrenReleaseHandle(vm, callConstruct); wrenReleaseHandle(vm, afterConstruct); -} \ No newline at end of file +} 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/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..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 '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..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 '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/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/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..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 '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..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 '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 c5ab61b9..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("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/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..14ce2ac9 --- /dev/null +++ b/test/unit/path_test.c @@ -0,0 +1,104 @@ +#include +#include +#include +#include + +#include "path.h" +#include "test.h" + +static void expectNormalize(const char* input, const char* expected) +{ + Path* path = pathNew(input); + pathNormalize(path); + + if (strcmp(path->chars, expected) != 0) + { + printf("FAIL %-30s Want %s\n", input, expected); + printf(" Got %s\n\n", path->chars); + fail(); + } + else + { +#if SHOW_PASSES + printf("PASS %-30s -> %s\n", input, path->chars); +#endif + pass(); + } + + pathFree(path); +} + +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, except one at the beginning. + 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..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 @@ -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..7010f431 100755 --- a/util/test.py +++ b/util/test.py @@ -25,14 +25,14 @@ 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)') 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') diff --git a/util/wren.mk b/util/wren.mk index febb06bb..c9eefcae 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 0da22634..4250c61d 100644 --- a/util/xcode/wren.xcodeproj/project.pbxproj +++ b/util/xcode/wren.xcodeproj/project.pbxproj @@ -23,9 +23,12 @@ 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 */; }; + 2940E9BC206607830054503C /* path.c in Sources */ = {isa = PBXBuildFile; fileRef = 2952CD1B1FA9941700985F5F /* path.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 */; }; @@ -41,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 */; }; @@ -67,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; @@ -115,11 +130,17 @@ 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 = ""; }; + 2940E9B8206605DE0054503C /* unit_test */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = unit_test; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; + 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 = ""; }; @@ -137,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 = ""; }; @@ -161,6 +187,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 2940E9B2206605DE0054503C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 29AB1F031816E3AD004B501E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -230,6 +263,9 @@ 29205C8E1AB4E5C90073018D /* main.c */, 291647C61BA5EC5E006142EE /* modules.h */, 291647C51BA5EC5E006142EE /* modules.c */, + 2952CD1C1FA9941700985F5F /* path.h */, + 2952CD1B1FA9941700985F5F /* path.c */, + 29703E57206DC7B7004004DC /* stat.h */, 29C8A9321AB71FFF00DEC81D /* vm.h */, 29C8A9311AB71FFF00DEC81D /* vm.c */, ); @@ -253,6 +289,7 @@ 29AF31EE1BD2E37F00AAD156 /* optional */, 29205CA01AB4E6470073018D /* vm */, 29D0099A1B7E394F000CE58C /* api_test */, + 29B59F0B20FC37BD00767E48 /* unit_test */, 29512C801B91F8EB008C10E6 /* libuv.a */, 29AB1F071816E3AD004B501E /* Products */, ); @@ -263,6 +300,7 @@ children = ( 29AB1F061816E3AD004B501E /* wren */, 29512C7F1B91F86E008C10E6 /* api_test */, + 2940E9B8206605DE0054503C /* unit_test */, ); name = Products; sourceTree = ""; @@ -280,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 = ( @@ -304,6 +354,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 */, @@ -315,6 +367,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" */; @@ -377,11 +446,23 @@ targets = ( 29AB1F051816E3AD004B501E /* wren */, 29D0099E1B7E397D000CE58C /* api_test */, + 2940E98F206605DE0054503C /* unit_test */, ); }; /* End PBXProject section */ /* Begin PBXSourcesBuildPhase section */ + 2940E990206605DE0054503C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29B59F0820FC37B700767E48 /* path_test.c in Sources */, + 2940E9BC206607830054503C /* path.c in Sources */, + 29B59F0920FC37B700767E48 /* main.c in Sources */, + 29B59F0A20FC37B700767E48 /* test.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 29AB1F021816E3AD004B501E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -390,10 +471,12 @@ 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 */, 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 */, @@ -454,6 +537,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 = { @@ -599,6 +726,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 = (