Get logical imports in "wren_modules" working.

There's a lot of changes here and surely some rough edges to iron out.
Also, I need to update the docs. But I want to get closer to landing
this so I can build on it.
This commit is contained in:
Bob Nystrom
2018-07-15 20:09:41 -07:00
parent 8210452970
commit c367fc3bfc
31 changed files with 665 additions and 190 deletions
+8 -3
View File
@@ -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();
}
+100 -44
View File
@@ -43,6 +43,75 @@ static void appendSlice(Path* path, Slice slice)
path->chars[path->length] = '\0';
}
static bool isSeparator(char c)
{
// Slash is a separator on POSIX and Windows.
if (c == '/') return true;
// Backslash is only a separator on Windows.
#ifdef _WIN32
if (c == '\\') return true;
#endif
return false;
}
#ifdef _WIN32
static bool isDriveLetter(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
#endif
// Gets the length of the prefix of [path] that defines its absolute root.
//
// Returns 1 the leading "/". On Windows, also handles drive letters ("C:" or
// "C:\").
//
// If the path is not absolute, returns 0.
static size_t absolutePrefixLength(const char* path)
{
#ifdef _WIN32
// Drive letter.
if (isDriveLetter(path[0]) && path[1] == ':')
{
if (isSeparator(path->chars[2]))
{
// Fully absolute path.
return 3;
} else {
// "Half-absolute" path like "C:", which is relative to the current
// working directory on drive. It's absolute for our purposes.
return 2;
}
}
// TODO: UNC paths.
#endif
// POSIX-style absolute path or absolute path in the current drive on Windows.
if (isSeparator(path[0])) return 1;
// Not absolute.
return 0;
}
PathType pathType(const char* path)
{
if (absolutePrefixLength(path) > 0) return PATH_TYPE_ABSOLUTE;
// See if it must be relative.
if ((path[0] == '.' && isSeparator(path[1])) ||
(path[0] == '.' && path[1] == '.' && isSeparator(path[2])))
{
return PATH_TYPE_RELATIVE;
}
// Otherwise, we don't know.
return PATH_TYPE_SIMPLE;
}
Path* pathNew(const char* string)
{
Path* path = (Path*)malloc(sizeof(Path));
@@ -67,7 +136,7 @@ void pathDirName(Path* path)
// Find the last path separator.
for (size_t i = path->length - 1; i < path->length; i--)
{
if (path->chars[i] == '/')
if (isSeparator(path->chars[i]))
{
path->length = i;
path->chars[i] = '\0';
@@ -86,7 +155,7 @@ void pathRemoveExtension(Path* path)
{
// If we hit a path separator before finding the extension, then the last
// component doesn't have one.
if (path->chars[i] == '/') return;
if (isSeparator(path->chars[i])) return;
if (path->chars[i] == '.')
{
@@ -98,7 +167,7 @@ void pathRemoveExtension(Path* path)
void pathJoin(Path* path, const char* string)
{
if (path->length > 0 && path->chars[path->length - 1] != '/')
if (path->length > 0 && !isSeparator(path->chars[path->length - 1]))
{
pathAppendChar(path, '/');
}
@@ -106,37 +175,6 @@ void pathJoin(Path* path, const char* string)
pathAppendString(path, string);
}
#ifdef _WIN32
static bool isDriveLetter(char c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
#endif
bool pathIsAbsolute(Path* path)
{
#ifdef _WIN32
// Absolute path in the current drive.
if (path->length >= 1 && path->chars[0] == '\\') return true;
// Drive letter.
if (path->length >= 3 &&
isDriveLetter(path->chars[0]) &&
path->chars[1] == ':' &&
path->chars[2] == '\\')
{
return true;
}
// UNC path.
return path->length >= 2 && path->chars[0] == '\\' && path->chars[1] == '\\';
#else
// Otherwise, assume POSIX-style paths.
return path->length >= 1 && path->chars[0] == '/';
#endif
}
void pathAppendChar(Path* path, char c)
{
ensureCapacity(path, path->length + 1);
@@ -152,10 +190,8 @@ void pathAppendString(Path* path, const char* string)
appendSlice(path, slice);
}
Path* pathNormalize(Path* path)
void pathNormalize(Path* path)
{
Path* result = pathNew("");
// Split the path into components.
Slice components[MAX_COMPONENTS];
int numComponents = 0;
@@ -167,7 +203,7 @@ Path* pathNormalize(Path* path)
int leadingDoubles = 0;
for (;;)
{
if (*end == '\0' || *end == '/')
if (*end == '\0' || isSeparator(*end))
{
// Add the current component.
if (start != end)
@@ -207,7 +243,7 @@ Path* pathNormalize(Path* path)
}
// Skip over separators.
while (*end != '\0' && *end == '/') end++;
while (*end != '\0' && isSeparator(*end)) end++;
start = end;
if (*end == '\0') break;
@@ -216,13 +252,22 @@ Path* pathNormalize(Path* path)
end++;
}
// Preserve the absolute prefix, if any.
// Preserve the path type. We don't want to turn, say, "./foo" into "foo"
// because that changes the semantics of how that path is handled when used
// as an import string.
bool needsSeparator = false;
if (path->length > 0 && path->chars[0] == '/')
Path* result = pathNew("");
size_t prefixLength = absolutePrefixLength(path->chars);
if (prefixLength > 0)
{
pathAppendChar(result, '/');
// It's an absolute path, so preserve the absolute prefix.
Slice slice;
slice.start = path->chars;
slice.end = path->chars + prefixLength;
appendSlice(result, slice);
}
else
else if (leadingDoubles > 0)
{
// Add any leading "..".
for (int i = 0; i < leadingDoubles; i++)
@@ -232,6 +277,13 @@ Path* pathNormalize(Path* path)
needsSeparator = true;
}
}
else if (path->chars[0] == '.' && isSeparator(path->chars[1]))
{
// Preserve a leading "./", since we use that to distinguish relative from
// logical imports.
pathAppendChar(result, '.');
needsSeparator = true;
}
for (int i = 0; i < numComponents; i++)
{
@@ -242,7 +294,11 @@ Path* pathNormalize(Path* path)
if (result->length == 0) pathAppendChar(result, '.');
return result;
// Copy back into the original path.
free(path->chars);
path->capacity = result->capacity;
path->chars = result->chars;
path->length = result->length;
}
char* pathToString(Path* path)
+18 -5
View File
@@ -16,6 +16,22 @@ typedef struct
size_t capacity;
} Path;
// Categorizes what form a path is.
typedef enum
{
// An absolute path, starting with "/" on POSIX systems, a drive letter on
// Windows, etc.
PATH_TYPE_ABSOLUTE,
// An explicitly relative path, starting with "./" or "../".
PATH_TYPE_RELATIVE,
// A path that has no leading prefix, like "foo/bar".
PATH_TYPE_SIMPLE,
} PathType;
PathType pathType(const char* path);
// Creates a new empty path.
Path* pathNew(const char* string);
@@ -31,9 +47,6 @@ void pathRemoveExtension(Path* path);
// Appends [string] to [path].
void pathJoin(Path* path, const char* string);
// Return true if [path] is an absolute path for the host operating system.
bool pathIsAbsolute(Path* path);
// Appends [c] to the path, growing the buffer if needed.
void pathAppendChar(Path* path, char c);
@@ -43,8 +56,8 @@ void pathAppendString(Path* path, const char* string);
// Simplifies the path string as much as possible.
//
// Applies and removes any "." or ".." components, collapses redundant "/"
// characters, etc.
Path* pathNormalize(Path* path);
// characters, and normalizes all path separators to "/".
void pathNormalize(Path* path);
// Allocates a new string exactly the right length and copies this path to it.
char* pathToString(Path* path);
+22
View File
@@ -0,0 +1,22 @@
#ifndef stat_h
#define stat_h
// Utilities to smooth over working with stat() in a cross-platform way.
// Windows doesn't define all of the Unix permission and mode flags by default,
// so map them ourselves.
#if defined(WIN32) || defined(WIN64)
#include <sys\stat.h>
// Map to Windows permission flags.
#define S_IRUSR _S_IREAD
#define S_IWUSR _S_IWRITE
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
// Not supported on Windows.
#define O_SYNC 0
#endif
#endif
+159 -63
View File
@@ -5,6 +5,7 @@
#include "modules.h"
#include "path.h"
#include "scheduler.h"
#include "stat.h"
#include "vm.h"
// The single VM instance that the CLI uses.
@@ -19,6 +20,7 @@ static uv_loop_t* loop;
// TODO: This isn't currently used, but probably will be when package imports
// are supported. If not then, then delete this.
static char* rootDirectory = NULL;
static Path* wrenModulesDirectory = NULL;
// The exit code to use unless some other error overrides it.
int defaultExitCode = 0;
@@ -61,46 +63,99 @@ static char* readFile(const char* path)
return buffer;
}
static bool isDirectory(Path* path)
{
uv_fs_t request;
uv_fs_stat(loop, &request, path->chars, NULL);
// TODO: Check request.result value?
bool result = request.result == 0 && S_ISDIR(request.statbuf.st_mode);
uv_fs_req_cleanup(&request);
return result;
}
static Path* realPath(Path* path)
{
uv_fs_t request;
uv_fs_realpath(loop, &request, path->chars, NULL);
Path* result = pathNew((char*)request.ptr);
uv_fs_req_cleanup(&request);
return result;
}
// Starting at [rootDirectory], walks up containing directories looking for a
// nearby "wren_modules" directory. If found, stores it in
// [wrenModulesDirectory].
//
// If [wrenModulesDirectory] has already been found, does nothing.
static void findModulesDirectory()
{
if (wrenModulesDirectory != NULL) return;
Path* searchDirectory = pathNew(rootDirectory);
Path* lastPath = realPath(searchDirectory);
// Keep walking up directories as long as we find them.
for (;;)
{
Path* modulesDirectory = pathNew(searchDirectory->chars);
pathJoin(modulesDirectory, "wren_modules");
if (isDirectory(modulesDirectory))
{
pathNormalize(modulesDirectory);
wrenModulesDirectory = modulesDirectory;
pathFree(lastPath);
break;
}
pathFree(modulesDirectory);
// Walk up directories until we hit the root. We can tell that because
// adding ".." yields the same real path.
pathJoin(searchDirectory, "..");
Path* thisPath = realPath(searchDirectory);
if (strcmp(lastPath->chars, thisPath->chars) == 0)
{
pathFree(thisPath);
break;
}
pathFree(lastPath);
lastPath = thisPath;
}
pathFree(searchDirectory);
}
// Applies the CLI's import resolution policy. The rules are:
//
// * If [name] starts with "./" or "../", it is a relative import, relative to
// [importer]. The resolved path is [name] concatenated onto the directory
// * If [module] starts with "./" or "../", it is a relative import, relative
// to [importer]. The resolved path is [name] concatenated onto the directory
// containing [importer] and then normalized.
//
// For example, importing "./a/./b/../c" from "d/e/f" gives you "d/e/a/c".
//
// * Otherwise, it is a "package" import. This isn't implemented yet.
//
// For example, importing "./a/./b/../c" from "./d/e/f" gives you "./d/e/a/c".
static const char* resolveModule(WrenVM* vm, const char* importer,
const char* name)
const char* module)
{
size_t nameLength = strlen(name);
// Logical import strings are used as-is and need no resolution.
if (pathType(module) == PATH_TYPE_SIMPLE) return module;
// See if it's a relative import.
if (nameLength > 2 &&
((name[0] == '.' && name[1] == '/') ||
(name[0] == '.' && name[1] == '.' && name[2] == '/')))
{
// Get the directory containing the importing module.
Path* relative = pathNew(importer);
pathDirName(relative);
// Add the relative import path.
pathJoin(relative, name);
Path* normal = pathNormalize(relative);
pathFree(relative);
char* resolved = pathToString(normal);
pathFree(normal);
return resolved;
}
else
{
// TODO: Implement package imports. For now, treat any non-relative import
// as an import relative to the current working directory.
}
// Get the directory containing the importing module.
Path* path = pathNew(importer);
pathDirName(path);
return name;
// Add the relative import path.
pathJoin(path, module);
pathNormalize(path);
char* resolved = pathToString(path);
pathFree(path);
return resolved;
}
// Attempts to read the source for [module] relative to the current root
@@ -110,23 +165,40 @@ static const char* resolveModule(WrenVM* vm, const char* importer,
// module was found but could not be read.
static char* readModule(WrenVM* vm, const char* module)
{
// Since the module has already been resolved, it should now be either a
// valid relative path, or a package-style name.
// TODO: Implement package imports.
Path* filePath;
if (pathType(module) == PATH_TYPE_SIMPLE)
{
// If there is no "wren_modules" directory, then the only logical imports
// we can handle are built-in ones. Let the VM try to handle it.
findModulesDirectory();
if (wrenModulesDirectory == NULL) return readBuiltInModule(module);
// TODO: Should we explicitly check for the existence of the module's base
// directory inside "wren_modules" here?
// Look up the module in "wren_modules".
filePath = pathNew(wrenModulesDirectory->chars);
pathJoin(filePath, module);
// If the module is a single bare name, treat it as a module with the same
// name inside the package. So "foo" means "foo/foo".
if (strchr(module, '/') == NULL) pathJoin(filePath, module);
}
else
{
// The module path is already a file path.
filePath = pathNew(module);
}
// Add a ".wren" file extension.
Path* modulePath = pathNew(module);
pathAppendString(modulePath, ".wren");
char* source = readFile(modulePath->chars);
pathFree(modulePath);
if (source != NULL) return source;
pathAppendString(filePath, ".wren");
// TODO: This used to look for a file named "<path>/module.wren" if
// "<path>.wren" could not be found. Do we still want to support that with
// the new relative import and package stuff?
char* source = readFile(filePath->chars);
pathFree(filePath);
// If we didn't find it, it may be a module built into the CLI or VM, so keep
// going.
if (source != NULL) return source;
// Otherwise, see if it's a built-in module.
return readBuiltInModule(module);
@@ -222,9 +294,11 @@ static void freeVM()
wrenFreeVM(vm);
uv_tty_reset_mode();
if (wrenModulesDirectory != NULL) pathFree(wrenModulesDirectory);
}
void runFile(const char* path)
WrenInterpretResult runFile(const char* path)
{
char* source = readFile(path);
if (source == NULL)
@@ -233,19 +307,36 @@ void runFile(const char* path)
exit(66);
}
// If it looks like a relative path, make it explicitly relative so that we
// can distinguish it from logical paths.
// TODO: It might be nice to be able to run scripts from within a surrounding
// "wren_modules" directory by passing in a simple path like "foo/bar". In
// that case, here, we could check to see whether the give path exists inside
// "wren_modules" or as a relative path and choose to add "./" or not based
// on that.
Path* module = pathNew(path);
if (pathType(module->chars) == PATH_TYPE_SIMPLE)
{
Path* relative = pathNew(".");
pathJoin(relative, path);
pathFree(module);
module = relative;
}
pathRemoveExtension(module);
// Use the directory where the file is as the root to resolve imports
// relative to.
Path* directory = pathNew(path);
Path* directory = pathNew(module->chars);
pathDirName(directory);
rootDirectory = pathToString(directory);
pathFree(directory);
Path* moduleName = pathNew(path);
pathRemoveExtension(moduleName);
initVM();
WrenInterpretResult result = wrenInterpret(vm, moduleName->chars, source);
WrenInterpretResult result = wrenInterpret(vm, module->chars, source);
if (afterLoadFn != NULL) afterLoadFn(vm);
@@ -258,29 +349,29 @@ void runFile(const char* path)
free(source);
free(rootDirectory);
pathFree(moduleName);
pathFree(module);
// Exit with an error code if the script failed.
if (result == WREN_RESULT_COMPILE_ERROR) exit(65); // EX_DATAERR.
if (result == WREN_RESULT_RUNTIME_ERROR) exit(70); // EX_SOFTWARE.
if (defaultExitCode != 0) exit(defaultExitCode);
return result;
}
int runRepl()
WrenInterpretResult runRepl()
{
rootDirectory = ".";
initVM();
printf("\\\\/\"-\n");
printf(" \\_/ wren v%s\n", WREN_VERSION_STRING);
wrenInterpret(vm, "repl", "import \"repl\"\n");
WrenInterpretResult result = wrenInterpret(vm, "<repl>", "import \"repl\"\n");
uv_run(loop, UV_RUN_DEFAULT);
if (result == WREN_RESULT_SUCCESS)
{
uv_run(loop, UV_RUN_DEFAULT);
}
freeVM();
return 0;
return result;
}
WrenVM* getVM()
@@ -293,6 +384,11 @@ uv_loop_t* getLoop()
return loop;
}
int getExitCode()
{
return defaultExitCode;
}
void setExitCode(int exitCode)
{
defaultExitCode = exitCode;
+5 -4
View File
@@ -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);
+1 -16
View File
@@ -4,28 +4,13 @@
#include "uv.h"
#include "scheduler.h"
#include "stat.h"
#include "vm.h"
#include "wren.h"
#include <stdio.h>
#include <fcntl.h>
// Windows doesn't define all of the Unix permission and mode flags by default,
// so map them ourselves.
#if defined(WIN32) || defined(WIN64)
#include <sys\stat.h>
// Map to Windows permission flags.
#define S_IRUSR _S_IREAD
#define S_IWUSR _S_IWRITE
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
// Not supported on Windows.
#define O_SYNC 0
#endif
typedef struct sFileRequestData
{
WrenHandle* fiber;
+2 -2
View File
@@ -723,7 +723,7 @@ static Value resolveModule(WrenVM* vm, Value name)
return name;
}
Value wrenImportModule(WrenVM* vm, Value name)
static Value importModule(WrenVM* vm, Value name)
{
name = resolveModule(vm, name);
@@ -1308,7 +1308,7 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber)
{
Value name = fn->constants.data[READ_SHORT()];
Value result = wrenImportModule(vm, name);
Value result = importModule(vm, name);
if (!IS_NULL(fiber->error)) RUNTIME_ERROR();
// Make a slot on the stack for the module's closure to place the return