From 48bdbc77459d443d21ca9157088d65bce49143f1 Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Sat, 15 Aug 2015 12:07:53 -0700 Subject: [PATCH] First pass at implementing foreign classes. Most of the pieces are there: - You can declare a foreign class. - It will call your C function to provide an allocator function. - Whenever a foreign object is created, it calls the allocator. - Foreign methods can access the foreign bytes of an object. - Most of the runtime checking is in place for things like subclassing foreign classes. There is still some loose ends to tie up: - Finalizers are not called. - Some of the error-handling could be better. - The GC doesn't track how much memory a marked foreign object uses. --- project/xcode/wren.xcodeproj/project.pbxproj | 148 ++++++++++ src/cli/main.c | 4 +- src/cli/vm.c | 27 +- src/cli/vm.h | 11 +- src/include/wren.h | 48 ++++ src/vm/wren_compiler.c | 43 ++- src/vm/wren_debug.c | 10 +- src/vm/wren_opcodes.h | 11 + src/vm/wren_value.c | 34 ++- src/vm/wren_value.h | 11 + src/vm/wren_vm.c | 266 +++++++++++++----- src/vm/wren_vm.h | 5 +- test/api/foreign_class.c | 87 ++++++ test/api/foreign_class.h | 5 + test/api/foreign_class.wren | 48 ++++ test/api/main.c | 35 ++- test/api/returns.c | 2 +- test/api/returns.h | 2 +- test/api/value.c | 2 +- test/api/value.h | 2 +- .../class/field_in_foreign_class.wren | 9 + .../class/foreign_class_inherit_fields.wren | 7 + .../class/missing_class_after_foreign.wren | 2 + .../inheritance/inherit_from_class.wren | 2 +- .../inheritance/inherit_from_closure.wren | 2 +- .../inheritance/inherit_from_fiber.wren | 2 +- .../language/inheritance/inherit_from_fn.wren | 2 +- .../inheritance/inherit_from_list.wren | 2 +- .../inheritance/inherit_from_map.wren | 2 +- .../inheritance/inherit_from_nonclass.wren | 2 +- .../inheritance/inherit_from_range.wren | 2 +- .../inheritance/inherit_from_string.wren | 2 +- .../language/inheritance/inherit_methods.wren | 2 - 33 files changed, 720 insertions(+), 119 deletions(-) create mode 100644 test/api/foreign_class.c create mode 100644 test/api/foreign_class.h create mode 100644 test/api/foreign_class.wren create mode 100644 test/language/class/field_in_foreign_class.wren create mode 100644 test/language/class/foreign_class_inherit_fields.wren create mode 100644 test/language/class/missing_class_after_foreign.wren diff --git a/project/xcode/wren.xcodeproj/project.pbxproj b/project/xcode/wren.xcodeproj/project.pbxproj index 13d8ac80..b66b594c 100644 --- a/project/xcode/wren.xcodeproj/project.pbxproj +++ b/project/xcode/wren.xcodeproj/project.pbxproj @@ -18,6 +18,21 @@ 2986F6D71ACF93BA00BCE26C /* wren_primitive.c in Sources */ = {isa = PBXBuildFile; fileRef = 2986F6D51ACF93BA00BCE26C /* wren_primitive.c */; }; 29C8A92F1AB71C1C00DEC81D /* io.c in Sources */ = {isa = PBXBuildFile; fileRef = 29C8A92E1AB71C1C00DEC81D /* io.c */; }; 29C8A9331AB71FFF00DEC81D /* vm.c in Sources */ = {isa = PBXBuildFile; fileRef = 29C8A9311AB71FFF00DEC81D /* vm.c */; }; + 29D009A71B7E3993000CE58C /* main.c in Sources */ = {isa = PBXBuildFile; fileRef = 29D009A61B7E3993000CE58C /* main.c */; }; + 29D009AE1B7E39A8000CE58C /* foreign_class.c in Sources */ = {isa = PBXBuildFile; fileRef = 29D009A81B7E39A8000CE58C /* foreign_class.c */; }; + 29D009AF1B7E39A8000CE58C /* returns.c in Sources */ = {isa = PBXBuildFile; fileRef = 29D009AA1B7E39A8000CE58C /* returns.c */; }; + 29D009B01B7E39A8000CE58C /* value.c in Sources */ = {isa = PBXBuildFile; fileRef = 29D009AC1B7E39A8000CE58C /* value.c */; }; + 29D009B11B7E39FE000CE58C /* io.c in Sources */ = {isa = PBXBuildFile; fileRef = 29C8A92E1AB71C1C00DEC81D /* io.c */; }; + 29D009B21B7E39FE000CE58C /* vm.c in Sources */ = {isa = PBXBuildFile; fileRef = 29C8A9311AB71FFF00DEC81D /* vm.c */; }; + 29D009B31B7E39FE000CE58C /* wren_compiler.c in Sources */ = {isa = PBXBuildFile; fileRef = 29205C921AB4E6430073018D /* wren_compiler.c */; }; + 29D009B41B7E39FE000CE58C /* wren_core.c in Sources */ = {isa = PBXBuildFile; fileRef = 29205C931AB4E6430073018D /* wren_core.c */; }; + 29D009B51B7E39FE000CE58C /* wren_debug.c in Sources */ = {isa = PBXBuildFile; fileRef = 29205C941AB4E6430073018D /* wren_debug.c */; }; + 29D009B61B7E39FE000CE58C /* wren_io.c in Sources */ = {isa = PBXBuildFile; fileRef = 29205C951AB4E6430073018D /* wren_io.c */; }; + 29D009B71B7E39FE000CE58C /* wren_meta.c in Sources */ = {isa = PBXBuildFile; fileRef = 29DE39511AC3A50A00987D41 /* wren_meta.c */; }; + 29D009B81B7E39FE000CE58C /* wren_primitive.c in Sources */ = {isa = PBXBuildFile; fileRef = 2986F6D51ACF93BA00BCE26C /* wren_primitive.c */; }; + 29D009B91B7E39FE000CE58C /* wren_utils.c in Sources */ = {isa = PBXBuildFile; fileRef = 29205C961AB4E6430073018D /* wren_utils.c */; }; + 29D009BA1B7E39FE000CE58C /* wren_value.c in Sources */ = {isa = PBXBuildFile; fileRef = 29205C971AB4E6430073018D /* wren_value.c */; }; + 29D009BB1B7E39FE000CE58C /* wren_vm.c in Sources */ = {isa = PBXBuildFile; fileRef = 29205C981AB4E6430073018D /* wren_vm.c */; }; 29DE39531AC3A50A00987D41 /* wren_meta.c in Sources */ = {isa = PBXBuildFile; fileRef = 29DE39511AC3A50A00987D41 /* wren_meta.c */; }; /* End PBXBuildFile section */ @@ -31,6 +46,15 @@ ); runOnlyForDeploymentPostprocessing = 1; }; + 29D0099D1B7E397D000CE58C /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -59,6 +83,14 @@ 29C8A9301AB71C3300DEC81D /* io.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = io.h; path = ../../src/cli/io.h; sourceTree = ""; }; 29C8A9311AB71FFF00DEC81D /* vm.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = vm.c; path = ../../src/cli/vm.c; sourceTree = ""; }; 29C8A9321AB71FFF00DEC81D /* vm.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = vm.h; path = ../../src/cli/vm.h; sourceTree = ""; }; + 29D0099F1B7E397D000CE58C /* api_test */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = api_test; sourceTree = BUILT_PRODUCTS_DIR; }; + 29D009A61B7E3993000CE58C /* main.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = main.c; path = ../../test/api/main.c; sourceTree = ""; }; + 29D009A81B7E39A8000CE58C /* foreign_class.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = foreign_class.c; path = ../../test/api/foreign_class.c; sourceTree = ""; }; + 29D009A91B7E39A8000CE58C /* foreign_class.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = foreign_class.h; path = ../../test/api/foreign_class.h; sourceTree = ""; }; + 29D009AA1B7E39A8000CE58C /* returns.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = returns.c; path = ../../test/api/returns.c; sourceTree = ""; }; + 29D009AB1B7E39A8000CE58C /* returns.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = returns.h; path = ../../test/api/returns.h; sourceTree = ""; }; + 29D009AC1B7E39A8000CE58C /* value.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = value.c; path = ../../test/api/value.c; sourceTree = ""; }; + 29D009AD1B7E39A8000CE58C /* value.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = value.h; path = ../../test/api/value.h; sourceTree = ""; }; 29DE39511AC3A50A00987D41 /* wren_meta.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = wren_meta.c; path = ../../src/vm/wren_meta.c; sourceTree = ""; }; 29DE39521AC3A50A00987D41 /* wren_meta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = wren_meta.h; path = ../../src/vm/wren_meta.h; sourceTree = ""; }; /* End PBXFileReference section */ @@ -71,6 +103,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 29D0099C1B7E397D000CE58C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -127,6 +166,7 @@ 29205CA91AB4E67B0073018D /* cli */, 29205CAA1AB4E6840073018D /* include */, 29205CA01AB4E6470073018D /* vm */, + 29D0099A1B7E394F000CE58C /* api_test */, 29AB1F071816E3AD004B501E /* Products */, ); sourceTree = ""; @@ -135,10 +175,25 @@ isa = PBXGroup; children = ( 29AB1F061816E3AD004B501E /* wren */, + 29D0099F1B7E397D000CE58C /* api_test */, ); name = Products; sourceTree = ""; }; + 29D0099A1B7E394F000CE58C /* api_test */ = { + isa = PBXGroup; + children = ( + 29D009A61B7E3993000CE58C /* main.c */, + 29D009A81B7E39A8000CE58C /* foreign_class.c */, + 29D009A91B7E39A8000CE58C /* foreign_class.h */, + 29D009AA1B7E39A8000CE58C /* returns.c */, + 29D009AB1B7E39A8000CE58C /* returns.h */, + 29D009AC1B7E39A8000CE58C /* value.c */, + 29D009AD1B7E39A8000CE58C /* value.h */, + ); + name = api_test; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -159,6 +214,23 @@ productReference = 29AB1F061816E3AD004B501E /* wren */; productType = "com.apple.product-type.tool"; }; + 29D0099E1B7E397D000CE58C /* api_test */ = { + isa = PBXNativeTarget; + buildConfigurationList = 29D009A31B7E397D000CE58C /* Build configuration list for PBXNativeTarget "api_test" */; + buildPhases = ( + 29D0099B1B7E397D000CE58C /* Sources */, + 29D0099C1B7E397D000CE58C /* Frameworks */, + 29D0099D1B7E397D000CE58C /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = api_test; + productName = api_test; + productReference = 29D0099F1B7E397D000CE58C /* api_test */; + productType = "com.apple.product-type.tool"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -167,6 +239,11 @@ attributes = { LastUpgradeCheck = 0610; ORGANIZATIONNAME = "Bob Nystrom"; + TargetAttributes = { + 29D0099E1B7E397D000CE58C = { + CreatedOnToolsVersion = 6.3.2; + }; + }; }; buildConfigurationList = 29AB1F011816E3AD004B501E /* Build configuration list for PBXProject "wren" */; compatibilityVersion = "Xcode 3.2"; @@ -181,6 +258,7 @@ projectRoot = ""; targets = ( 29AB1F051816E3AD004B501E /* wren */, + 29D0099E1B7E397D000CE58C /* api_test */, ); }; /* End PBXProject section */ @@ -205,6 +283,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 29D0099B1B7E397D000CE58C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 29D009B11B7E39FE000CE58C /* io.c in Sources */, + 29D009B21B7E39FE000CE58C /* vm.c in Sources */, + 29D009B31B7E39FE000CE58C /* wren_compiler.c in Sources */, + 29D009B41B7E39FE000CE58C /* wren_core.c in Sources */, + 29D009B51B7E39FE000CE58C /* wren_debug.c in Sources */, + 29D009B61B7E39FE000CE58C /* wren_io.c in Sources */, + 29D009B71B7E39FE000CE58C /* wren_meta.c in Sources */, + 29D009B81B7E39FE000CE58C /* wren_primitive.c in Sources */, + 29D009B91B7E39FE000CE58C /* wren_utils.c in Sources */, + 29D009BA1B7E39FE000CE58C /* wren_value.c in Sources */, + 29D009BB1B7E39FE000CE58C /* wren_vm.c in Sources */, + 29D009A71B7E3993000CE58C /* main.c in Sources */, + 29D009B01B7E39A8000CE58C /* value.c in Sources */, + 29D009AF1B7E39A8000CE58C /* returns.c in Sources */, + 29D009AE1B7E39A8000CE58C /* foreign_class.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ @@ -302,6 +402,46 @@ }; name = Release; }; + 29D009A41B7E397D000CE58C /* 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 29D009A51B7E397D000CE58C /* 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; + MACOSX_DEPLOYMENT_TARGET = 10.10; + MTL_ENABLE_DEBUG_INFO = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -323,6 +463,14 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 29D009A31B7E397D000CE58C /* Build configuration list for PBXNativeTarget "api_test" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 29D009A41B7E397D000CE58C /* Debug */, + 29D009A51B7E397D000CE58C /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; /* End XCConfigurationList section */ }; rootObject = 29AB1EFE1816E3AD004B501E /* Project object */; diff --git a/src/cli/main.c b/src/cli/main.c index 2b8e5d61..b04e9f94 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -9,7 +9,7 @@ static int runRepl() { - WrenVM* vm = createVM(NULL); + WrenVM* vm = createVM(); printf("\\\\/\"-\n"); printf(" \\_/ wren v0.0.0\n"); @@ -50,7 +50,7 @@ int main(int argc, const char* argv[]) } else if (argc == 2) { - runFile(NULL, argv[1]); + runFile(argv[1]); } return 0; diff --git a/src/cli/vm.c b/src/cli/vm.c index 5aedc3c8..ad4bcbc0 100644 --- a/src/cli/vm.c +++ b/src/cli/vm.c @@ -4,25 +4,25 @@ #include "io.h" #include "vm.h" -WrenVM* createVM(WrenBindForeignMethodFn bindForeign) +static WrenBindForeignMethodFn bindMethodFn = NULL; +static WrenBindForeignClassFn bindClassFn = NULL; + +WrenVM* createVM() { WrenConfiguration config; - - config.bindForeignMethodFn = bindForeign; + wrenInitConfiguration(&config); + + config.bindForeignMethodFn = bindMethodFn; + config.bindForeignClassFn = bindClassFn; config.loadModuleFn = readModule; // Since we're running in a standalone process, be generous with memory. config.initialHeapSize = 1024 * 1024 * 100; - // Use defaults for these. - config.reallocateFn = NULL; - config.minHeapSize = 0; - config.heapGrowthPercent = 0; - return wrenNewVM(&config); } -void runFile(WrenBindForeignMethodFn bindForeign, const char* path) +void runFile(const char* path) { // Use the directory where the file is as the root to resolve imports // relative to. @@ -43,7 +43,7 @@ void runFile(WrenBindForeignMethodFn bindForeign, const char* path) exit(66); } - WrenVM* vm = createVM(bindForeign); + WrenVM* vm = createVM(); WrenInterpretResult result = wrenInterpret(vm, path, source); @@ -55,3 +55,10 @@ void runFile(WrenBindForeignMethodFn bindForeign, const char* path) if (result == WREN_RESULT_COMPILE_ERROR) exit(65); // EX_DATAERR. if (result == WREN_RESULT_RUNTIME_ERROR) exit(70); // EX_SOFTWARE. } + +void setForeignCallbacks( + WrenBindForeignMethodFn bindMethod, WrenBindForeignClassFn bindClass) +{ + bindMethodFn = bindMethod; + bindClassFn = bindClass; +} diff --git a/src/cli/vm.h b/src/cli/vm.h index 90fa491a..7e98c3a7 100644 --- a/src/cli/vm.h +++ b/src/cli/vm.h @@ -4,11 +4,18 @@ #include "wren.h" // Creates a new Wren VM with the CLI's module loader and other configuration. -WrenVM* createVM(WrenBindForeignMethodFn bindForeign); +WrenVM* createVM(); // Executes the Wren script at [path] in a new VM. // // Exits if the script failed or could not be loaded. -void runFile(WrenBindForeignMethodFn bindForeign, const char* path); +void runFile(const char* path); + +// Adds additional callbacks to use when binding foreign members from Wren. +// +// Used by the API test executable to let it wire up its own foreign functions. +// This must be called before calling [createVM()]. +void setForeignCallbacks(WrenBindForeignMethodFn bindMethod, + WrenBindForeignClassFn bindClass); #endif diff --git a/src/include/wren.h b/src/include/wren.h index d08d9d0a..e13291fb 100644 --- a/src/include/wren.h +++ b/src/include/wren.h @@ -53,6 +53,26 @@ typedef WrenForeignMethodFn (*WrenBindForeignMethodFn)(WrenVM* vm, bool isStatic, const char* signature); +typedef struct +{ + // The callback invoked when the foreign object is created. + // + // This must be provided. Inside the body of this, it must call + // [wrenAllocateForeign] exactly once. + WrenForeignMethodFn allocate; + + // The callback invoked when the garbage collector is about to collecto a + // foreign object's memory. + // + // This may be `NULL` if the foreign class does not need to finalize. + WrenForeignMethodFn finalize; +} WrenForeignClassMethods; + +// Returns a pair of pointers to the foreign methods used to allocate and +// finalize the data for instances of [className] in [module]. +typedef WrenForeignClassMethods (*WrenBindForeignClassFn)( + WrenVM* vm, const char* module, const char* className); + typedef struct { // The callback Wren will use to allocate, reallocate, and deallocate memory. @@ -88,6 +108,14 @@ typedef struct // Wren will report it as runtime error. WrenBindForeignMethodFn bindForeignMethodFn; + // The callback Wren uses to find a foreign class and get its foreign methods. + // + // When a foreign class is declared, this will be called with the class's + // module and name when the class body is executed. It should return the + // foreign functions uses to allocate and (optionally) finalize the bytes + // stored in the foreign object when an instance is created. + WrenBindForeignClassFn bindForeignClassFn; + // The number of bytes Wren will allocate before triggering the first garbage // collection. // @@ -129,6 +157,11 @@ typedef enum { WREN_RESULT_RUNTIME_ERROR } WrenInterpretResult; +// Initializes [configuration] with all of its default values. +// +// Call this before setting the particular fields you care about. +void wrenInitConfiguration(WrenConfiguration* configuration); + // Creates a new Wren virtual machine using the given [configuration]. Wren // will copy the configuration data, so the argument passed to this can be // freed after calling this. If [configuration] is `NULL`, uses a default @@ -186,6 +219,16 @@ void wrenReleaseMethod(WrenVM* vm, WrenMethod* method); // longer be used. void wrenReleaseValue(WrenVM* vm, WrenValue* value); +// This must be called once inside a foreign class's allocator function. +// +// It tells Wren how many bytes of raw data need to be stored in the foreign +// object and creates the new object with that size. It returns a pointer to +// the foreign object's data. +void* wrenAllocateForeign(WrenVM* vm, size_t size); + +// Returns the number of arguments available to the current foreign method. +int wrenGetArgumentCount(WrenVM* vm); + // The following functions read one of the arguments passed to a foreign call. // They may only be called while within a function provided to // [wrenDefineMethod] or [wrenDefineStaticMethod] that Wren has invoked. @@ -214,6 +257,11 @@ bool wrenGetArgumentBool(WrenVM* vm, int index); // a number. double wrenGetArgumentDouble(WrenVM* vm, int index); +// Reads a foreign object argument for a foreign call and returns a pointer to +// the foreign data stored with it. Returns NULL if the argument is not a +// foreign object. +void* wrenGetArgumentForeign(WrenVM* vm, int index); + // Reads an string argument for a foreign call. Returns NULL if the argument is // not a string. // diff --git a/src/vm/wren_compiler.c b/src/vm/wren_compiler.c index ba4e8e59..a53933b6 100644 --- a/src/vm/wren_compiler.c +++ b/src/vm/wren_compiler.c @@ -252,6 +252,9 @@ typedef struct // Symbol table for the fields of the class. SymbolTable* fields; + // True if the class being compiled is a foreign class. + bool isForeign; + // True if the current method being compiled is static. bool inStatic; @@ -1938,6 +1941,10 @@ static void field(Compiler* compiler, bool allowAssignment) { error(compiler, "Cannot reference a field outside of a class definition."); } + else if (enclosingClass->isForeign) + { + error(compiler, "Cannot define fields in a foreign class."); + } else if (enclosingClass->inStatic) { error(compiler, "Cannot use an instance field in a static method."); @@ -2591,6 +2598,8 @@ static int getNumArguments(const uint8_t* bytecode, const Value* constants, case CODE_LOAD_LOCAL_7: case CODE_LOAD_LOCAL_8: case CODE_CONSTRUCT: + case CODE_FOREIGN_CONSTRUCT: + case CODE_FOREIGN_CLASS: return 0; case CODE_LOAD_LOCAL: @@ -2945,7 +2954,8 @@ static void createConstructor(Compiler* compiler, Signature* signature, initCompiler(&methodCompiler, compiler->parser, compiler, false); // Allocate the instance. - emit(&methodCompiler, CODE_CONSTRUCT); + emit(&methodCompiler, compiler->enclosingClass->isForeign + ? CODE_FOREIGN_CONSTRUCT : CODE_CONSTRUCT); // Run its initializer. emitShortArg(&methodCompiler, (Code)(CODE_CALL_0 + signature->arity), @@ -3072,7 +3082,6 @@ static bool method(Compiler* compiler, ClassCompiler* classCompiler, static void createDefaultConstructor(Compiler* compiler, int classSlot) { Signature signature = { "new", 3, SIG_INITIALIZER, 0 }; - int initializerSymbol = signatureSymbol(compiler, &signature); signature.type = SIG_METHOD; @@ -3083,8 +3092,8 @@ static void createDefaultConstructor(Compiler* compiler, int classSlot) } // Compiles a class definition. Assumes the "class" token has already been -// consumed. -static void classDefinition(Compiler* compiler) +// consumed (along with a possibly preceding "foreign" token). +static void classDefinition(Compiler* compiler, bool isForeign) { // Create a variable to store the class in. int slot = declareNamedVariable(compiler); @@ -3109,7 +3118,15 @@ static void classDefinition(Compiler* compiler) // Store a placeholder for the number of fields argument. We don't know // the value until we've compiled all the methods to see which fields are // used. - int numFieldsInstruction = emitByteArg(compiler, CODE_CLASS, 255); + int numFieldsInstruction = -1; + if (isForeign) + { + emit(compiler, CODE_FOREIGN_CLASS); + } + else + { + numFieldsInstruction = emitByteArg(compiler, CODE_CLASS, 255); + } // Store it in its name. defineVariable(compiler, slot); @@ -3120,6 +3137,7 @@ static void classDefinition(Compiler* compiler) pushScope(compiler); ClassCompiler classCompiler; + classCompiler.isForeign = isForeign; // Set up a symbol table for the class's fields. We'll initially compile // them to slots starting at zero. When the method is bound to the class, the @@ -3155,7 +3173,11 @@ static void classDefinition(Compiler* compiler) } // Update the class with the number of fields. - compiler->bytecode.data[numFieldsInstruction] = (uint8_t)fields.count; + if (!isForeign) + { + compiler->bytecode.data[numFieldsInstruction] = (uint8_t)fields.count; + } + wrenSymbolTableClear(compiler->parser->vm, &fields); compiler->enclosingClass = NULL; @@ -3227,7 +3249,14 @@ void definition(Compiler* compiler) { if (match(compiler, TOKEN_CLASS)) { - classDefinition(compiler); + classDefinition(compiler, false); + return; + } + + if (match(compiler, TOKEN_FOREIGN)) + { + consume(compiler, TOKEN_CLASS, "Expect 'class' after 'foreign'."); + classDefinition(compiler, true); return; } diff --git a/src/vm/wren_debug.c b/src/vm/wren_debug.c index cb31f4b8..902a2f2f 100644 --- a/src/vm/wren_debug.c +++ b/src/vm/wren_debug.c @@ -32,10 +32,13 @@ static void dumpObject(Obj* obj) { switch (obj->type) { - case OBJ_CLASS: printf("[class %p]", obj); break; + case OBJ_CLASS: + printf("[class %s %p]", ((ObjClass*)obj)->name->value, obj); + break; case OBJ_CLOSURE: printf("[closure %p]", obj); break; case OBJ_FIBER: printf("[fiber %p]", obj); break; case OBJ_FN: printf("[fn %p]", obj); break; + case OBJ_FOREIGN: printf("[foreign %p]", obj); break; case OBJ_INSTANCE: printf("[instance %p]", obj); break; case OBJ_LIST: printf("[list %p]", obj); break; case OBJ_MAP: printf("[map %p]", obj); break; @@ -269,7 +272,8 @@ static int dumpInstruction(WrenVM* vm, ObjFn* fn, int i, int* lastLine) break; } - case CODE_CONSTRUCT: printf("CODE_CONSTRUCT\n"); break; + case CODE_CONSTRUCT: printf("CODE_CONSTRUCT\n"); break; + case CODE_FOREIGN_CONSTRUCT: printf("CODE_FOREIGN_CONSTRUCT\n"); break; case CODE_CLASS: { @@ -278,6 +282,8 @@ static int dumpInstruction(WrenVM* vm, ObjFn* fn, int i, int* lastLine) break; } + case CODE_FOREIGN_CLASS: printf("FOREIGN_CLASS\n"); break; + case CODE_METHOD_INSTANCE: { int symbol = READ_SHORT(); diff --git a/src/vm/wren_opcodes.h b/src/vm/wren_opcodes.h index 6a48ce45..08ccf83c 100644 --- a/src/vm/wren_opcodes.h +++ b/src/vm/wren_opcodes.h @@ -157,11 +157,22 @@ OPCODE(CLOSURE) // compiler-generated constructor metaclass methods. OPCODE(CONSTRUCT) +// Creates a new instance of a foreign class. +// +// Assumes the class object is in slot zero, and replaces it with the new +// uninitialized instance of that class. This opcode is only emitted by the +// compiler-generated constructor metaclass methods. +OPCODE(FOREIGN_CONSTRUCT) + // Creates a class. Top of stack is the superclass, or `null` if the class // inherits Object. Below that is a string for the name of the class. Byte // [arg] is the number of fields in the class. OPCODE(CLASS) +// Creates a foreign class. Top of stack is the superclass, or `null` if the +// class inherits Object. Below that is a string for the name of the class. +OPCODE(FOREIGN_CLASS) + // Define a method for symbol [arg]. The class receiving the method is popped // off the stack, then the function defining the body is popped. // diff --git a/src/vm/wren_value.c b/src/vm/wren_value.c index 5fab71c7..bbfc2bbe 100644 --- a/src/vm/wren_value.c +++ b/src/vm/wren_value.c @@ -65,7 +65,15 @@ void wrenBindSuperclass(WrenVM* vm, ObjClass* subclass, ObjClass* superclass) subclass->superclass = superclass; // Include the superclass in the total number of fields. - subclass->numFields += superclass->numFields; + if (subclass->numFields != -1) + { + subclass->numFields += superclass->numFields; + } + else + { + ASSERT(superclass->numFields == 0, + "A foreign class cannot inherit from a class with fields."); + } // Inherit methods from its superclass. for (int i = 0; i < superclass->methods.count; i++) @@ -166,6 +174,16 @@ void wrenResetFiber(WrenVM* vm, ObjFiber* fiber, Obj* fn) wrenAppendCallFrame(vm, fiber, fn, fiber->stack); } +ObjForeign* wrenNewForeign(WrenVM* vm, ObjClass* classObj, size_t size) +{ + ObjForeign* object = ALLOCATE_FLEX(vm, ObjForeign, uint8_t, size); + initObj(vm, &object->obj, OBJ_FOREIGN, classObj); + + // Zero out the bytes. + memset(object->data, 0, size); + return object; +} + ObjFn* wrenNewFunction(WrenVM* vm, ObjModule* module, const Value* constants, int numConstants, int numUpvalues, int arity, @@ -903,6 +921,15 @@ static void markFn(WrenVM* vm, ObjFn* fn) // TODO: What about the function name? } +static void markForeign(WrenVM* vm, ObjForeign* foreign) +{ + // TODO: Keep track of how much memory the foreign object uses. We can store + // this in each foreign object, but it will balloon the size. We may not want + // that much overhead. One option would be to let the foreign class register + // a C function that returns a size for the object. That way the VM doesn't + // always have to explicitly store it. +} + static void markInstance(WrenVM* vm, ObjInstance* instance) { wrenMarkObj(vm, (Obj*)instance->obj.classObj); @@ -1007,6 +1034,7 @@ void wrenMarkObj(WrenVM* vm, Obj* obj) case OBJ_CLOSURE: markClosure( vm, (ObjClosure*) obj); break; case OBJ_FIBER: markFiber( vm, (ObjFiber*) obj); break; case OBJ_FN: markFn( vm, (ObjFn*) obj); break; + case OBJ_FOREIGN: markForeign( vm, (ObjForeign*) obj); break; case OBJ_INSTANCE: markInstance(vm, (ObjInstance*)obj); break; case OBJ_LIST: markList( vm, (ObjList*) obj); break; case OBJ_MAP: markMap( vm, (ObjMap*) obj); break; @@ -1059,6 +1087,10 @@ void wrenFreeObj(WrenVM* vm, Obj* obj) DEALLOCATE(vm, fn->debug); break; } + + case OBJ_FOREIGN: + // TODO: Call finalizer. + break; case OBJ_LIST: wrenValueBufferClear(vm, &((ObjList*)obj)->elements); diff --git a/src/vm/wren_value.h b/src/vm/wren_value.h index 26469e05..f453bcd7 100644 --- a/src/vm/wren_value.h +++ b/src/vm/wren_value.h @@ -51,6 +51,7 @@ #define AS_CLOSURE(value) ((ObjClosure*)AS_OBJ(value)) // ObjClosure* #define AS_FIBER(v) ((ObjFiber*)AS_OBJ(v)) // ObjFiber* #define AS_FN(value) ((ObjFn*)AS_OBJ(value)) // ObjFn* +#define AS_FOREIGN(v) ((ObjForeign*)AS_OBJ(v)) // ObjForeign* #define AS_INSTANCE(value) ((ObjInstance*)AS_OBJ(value)) // ObjInstance* #define AS_LIST(value) ((ObjList*)AS_OBJ(value)) // ObjList* #define AS_MAP(value) ((ObjMap*)AS_OBJ(value)) // ObjMap* @@ -74,6 +75,7 @@ #define IS_CLOSURE(value) (wrenIsObjType(value, OBJ_CLOSURE)) // ObjClosure #define IS_FIBER(value) (wrenIsObjType(value, OBJ_FIBER)) // ObjFiber #define IS_FN(value) (wrenIsObjType(value, OBJ_FN)) // ObjFn +#define IS_FOREIGN(value) (wrenIsObjType(value, OBJ_FOREIGN)) // ObjForeign #define IS_INSTANCE(value) (wrenIsObjType(value, OBJ_INSTANCE)) // ObjInstance #define IS_RANGE(value) (wrenIsObjType(value, OBJ_RANGE)) // ObjRange #define IS_STRING(value) (wrenIsObjType(value, OBJ_STRING)) // ObjString @@ -89,6 +91,7 @@ typedef enum { OBJ_CLOSURE, OBJ_FIBER, OBJ_FN, + OBJ_FOREIGN, OBJ_INSTANCE, OBJ_LIST, OBJ_MAP, @@ -397,6 +400,12 @@ struct sObjClass ObjString* name; }; +typedef struct +{ + Obj obj; + uint8_t data[FLEXIBLE_ARRAY]; +} ObjForeign; + typedef struct { Obj obj; @@ -652,6 +661,8 @@ static inline void wrenAppendCallFrame(WrenVM* vm, ObjFiber* fiber, frame->ip = wrenGetFrameFunction(frame)->bytecode; } +ObjForeign* wrenNewForeign(WrenVM* vm, ObjClass* classObj, size_t size); + // TODO: The argument list here is getting a bit gratuitous. // Creates a new function object with the given code and constants. The new // function will take over ownership of [bytecode] and [sourceLines]. It will diff --git a/src/vm/wren_vm.c b/src/vm/wren_vm.c index eef3a515..89107058 100644 --- a/src/vm/wren_vm.c +++ b/src/vm/wren_vm.c @@ -37,6 +37,17 @@ static void* defaultReallocate(void* ptr, size_t newSize) return realloc(ptr, newSize); } +void wrenInitConfiguration(WrenConfiguration* configuration) +{ + configuration->reallocateFn = NULL; + configuration->loadModuleFn = NULL; + configuration->bindForeignMethodFn = NULL; + configuration->bindForeignClassFn = NULL; + configuration->initialHeapSize = 1024 * 1024 * 10; + configuration->minHeapSize = 1024 * 1024; + configuration->heapGrowthPercent = 50; +} + WrenVM* wrenNewVM(WrenConfiguration* configuration) { WrenReallocateFn reallocate = defaultReallocate; @@ -49,32 +60,19 @@ WrenVM* wrenNewVM(WrenConfiguration* configuration) memset(vm, 0, sizeof(WrenVM)); vm->reallocate = reallocate; - vm->bindForeign = configuration->bindForeignMethodFn; + vm->bindForeignMethod = configuration->bindForeignMethodFn; + vm->bindForeignClass = configuration->bindForeignClassFn; vm->loadModule = configuration->loadModuleFn; + vm->nextGC = configuration->initialHeapSize; + vm->minNextGC = configuration->minHeapSize; + // +100 here because the configuration gives us the *additional* size of + // the heap relative to the in-use memory, while heapScalePercent is the + // *total* size of the heap relative to in-use. + vm->heapScalePercent = 100 + configuration->heapGrowthPercent; + wrenSymbolTableInit(&vm->methodNames); - vm->nextGC = 1024 * 1024 * 10; - if (configuration->initialHeapSize != 0) - { - vm->nextGC = configuration->initialHeapSize; - } - - vm->minNextGC = 1024 * 1024; - if (configuration->minHeapSize != 0) - { - vm->minNextGC = configuration->minHeapSize; - } - - vm->heapScalePercent = 150; - if (configuration->heapGrowthPercent != 0) - { - // +100 here because the configuration gives us the *additional* size of - // the heap relative to the in-use memory, while heapScalePercent is the - // *total* size of the heap relative to in-use. - vm->heapScalePercent = 100 + configuration->heapGrowthPercent; - } - ObjString* name = AS_STRING(CONST_STRING(vm, "core")); wrenPushRoot(vm, (Obj*)name); @@ -311,24 +309,21 @@ static WrenForeignMethodFn findForeignMethod(WrenVM* vm, { WrenForeignMethodFn fn; - // Let the host try to find it first. - if (vm->bindForeign != NULL) - { - fn = vm->bindForeign(vm, moduleName, className, isStatic, signature); - if (fn != NULL) return fn; - } - - // Otherwise, try the built-in libraries. + // Bind foreign methods in the core module. if (strcmp(moduleName, "core") == 0) { #if WREN_USE_LIB_IO fn = wrenBindIOForeignMethod(vm, className, signature); - if (fn != NULL) return fn; #endif + + ASSERT(fn != NULL, "Failed to bind core module foreign method."); + return fn; } - // TODO: Report a runtime error on failure to find it. - return NULL; + // For other modules, let the host bind it. + if (vm->bindForeignMethod == NULL) return NULL; + + return vm->bindForeignMethod(vm, moduleName, className, isStatic, signature); } // Defines [methodValue] as a method on [classObj]. @@ -546,18 +541,23 @@ static bool importVariable(WrenVM* vm, Value moduleName, Value variableName, return false; } -// Verifies that [superclass] is a valid object to inherit from. That means it -// must be a class and cannot be the class of any built-in type. +// Verifies that [superclassValue] is a valid object to inherit from. That +// means it must be a class and cannot be the class of any built-in type. // -// If successful, returns null. Otherwise, returns a string for the runtime +// Also validates that it doesn't result in a class with too many fields and +// the other limitations foreign classes have. +// +// If successful, returns `null`. Otherwise, returns a string for the runtime // error message. -static Value validateSuperclass(WrenVM* vm, Value name, - Value superclassValue) +static Value validateSuperclass(WrenVM* vm, Value name, Value superclassValue, + int numFields) { // Make sure the superclass is a class. if (!IS_CLASS(superclassValue)) { - return CONST_STRING(vm, "Must inherit from a class."); + return wrenStringFormat(vm, + "Class '@' cannot inherit from a non-class object.", + name); } // Make sure it doesn't inherit from a sealed built-in type. Primitive methods @@ -572,13 +572,125 @@ static Value validateSuperclass(WrenVM* vm, Value name, superclass == vm->rangeClass || superclass == vm->stringClass) { - return wrenStringFormat(vm, "@ cannot inherit from @.", + return wrenStringFormat(vm, + "Class '@' cannot inherit from built-in class '@'.", + name, OBJ_VAL(superclass->name)); + } + + if (superclass->numFields == -1) + { + return wrenStringFormat(vm, + "Class '@' cannot inherit from foreign class '@'.", name, OBJ_VAL(superclass->name)); } + if (numFields == -1 && superclass->numFields > 0) + { + return wrenStringFormat(vm, + "Foreign class '@' may not inherit from a class with fields.", + name); + } + + if (superclass->numFields + numFields > MAX_FIELDS) + { + return wrenStringFormat(vm, + "Class '@' may not have more than 255 fields, including inherited " + "ones.", name); + } + return NULL_VAL; } +static void bindForeignClass(WrenVM* vm, ObjClass* classObj, ObjModule* module) +{ + // TODO: Make this a runtime error? + ASSERT(vm->bindForeignClass != NULL, + "Cannot declare foreign classes without a bindForeignClassFn."); + + WrenForeignClassMethods methods = vm->bindForeignClass( + vm, module->name->value, classObj->name->value); + + Method method; + method.type = METHOD_FOREIGN; + method.fn.foreign = methods.allocate; + + ASSERT(method.fn.foreign != NULL, + "A foreign class must provide an allocate function."); + + int symbol = wrenSymbolTableEnsure(vm, &vm->methodNames, "", 10); + wrenBindMethod(vm, classObj, symbol, method); + + if (methods.finalize != NULL) + { + method.fn.foreign = methods.finalize; + symbol = wrenSymbolTableEnsure(vm, &vm->methodNames, "", 10); + wrenBindMethod(vm, classObj, symbol, method); + } +} + +// Creates a new class. +// +// If [numFields] is -1, the class is a foreign class. The name and superclass +// should be on top of the fiber's stack. After calling this, the top of the +// stack will contain either the new class or a string if a runtime error +// occurred. +// +// Returns false if the result is an error. +static bool defineClass(WrenVM* vm, ObjFiber* fiber, int numFields, + ObjModule* module) +{ + // Pull the name and superclass off the stack. + Value name = fiber->stackTop[-2]; + Value superclassValue = fiber->stackTop[-1]; + + // We have two values on the stack and we are going to leave one, so discard + // the other slot. + fiber->stackTop--; + + // Use implicit Object superclass if none given. + ObjClass* superclass = vm->objectClass; + + if (!IS_NULL(superclassValue)) + { + Value error = validateSuperclass(vm, name, superclassValue, numFields); + if (!IS_NULL(error)) + { + fiber->stackTop[-1] = error; + return false; + } + superclass = AS_CLASS(superclassValue); + } + + ObjClass* classObj = wrenNewClass(vm, superclass, numFields, AS_STRING(name)); + fiber->stackTop[-1] = OBJ_VAL(classObj); + + if (numFields == -1) bindForeignClass(vm, classObj, module); + + return true; +} + +static void createForeign(WrenVM* vm, ObjFiber* fiber, Value* stack) +{ + ObjClass* classObj = AS_CLASS(stack[0]); + ASSERT(classObj->numFields == -1, "Class must be a foreign class."); + + // TODO: Don't look up every time. + int symbol = wrenSymbolTableFind(&vm->methodNames, "", 10); + ASSERT(symbol != -1, "Should have defined symbol."); + + ASSERT(classObj->methods.count > symbol, "Class should have allocator."); + Method* method = &classObj->methods.data[symbol]; + ASSERT(method->type == METHOD_FOREIGN, "Allocator should be foreign."); + + // Pass the constructor arguments to the allocator as well. + vm->foreignCallSlot = stack; + vm->foreignCallNumArgs = (int)(fiber->stackTop - stack); + + method->fn.foreign(vm); + + // TODO: Check that allocateForeign was called. +} + // 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. @@ -1022,6 +1134,11 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) stackStart[0] = wrenNewInstance(vm, AS_CLASS(stackStart[0])); DISPATCH(); + CASE_CODE(FOREIGN_CONSTRUCT): + ASSERT(IS_CLASS(stackStart[0]), "'this' should be a class."); + createForeign(vm, fiber, stackStart); + DISPATCH(); + CASE_CODE(CLOSURE): { ObjFn* prototype = AS_FN(fn->constants[READ_SHORT()]); @@ -1057,40 +1174,16 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) CASE_CODE(CLASS): { - Value name = PEEK2(); - ObjClass* superclass = vm->objectClass; - - // Use implicit Object superclass if none given. - if (!IS_NULL(PEEK())) - { - Value error = validateSuperclass(vm, name, PEEK()); - if (!IS_NULL(error)) RUNTIME_ERROR(error); - superclass = AS_CLASS(PEEK()); - } - - int numFields = READ_BYTE(); - - Value classObj = OBJ_VAL(wrenNewClass(vm, superclass, numFields, - AS_STRING(name))); - - // Don't pop the superclass and name off the stack until the subclass is - // done being created, to make sure it doesn't get collected. - DROP(); - DROP(); - - // Now that we know the total number of fields, make sure we don't - // overflow. - if (superclass->numFields + numFields > MAX_FIELDS) - { - RUNTIME_ERROR(wrenStringFormat(vm, - "Class '@' may not have more than 255 fields, including inherited " - "ones.", name)); - } - - PUSH(classObj); + if (!defineClass(vm, fiber, READ_BYTE(), NULL)) RUNTIME_ERROR(PEEK()); DISPATCH(); } - + + CASE_CODE(FOREIGN_CLASS): + { + if (!defineClass(vm, fiber, -1, fn->module)) RUNTIME_ERROR(PEEK()); + DISPATCH(); + } + CASE_CODE(METHOD_INSTANCE): CASE_CODE(METHOD_STATIC): { @@ -1323,6 +1416,20 @@ void wrenReleaseValue(WrenVM* vm, WrenValue* value) DEALLOCATE(vm, value); } +void* wrenAllocateForeign(WrenVM* vm, size_t size) +{ + ASSERT(vm->foreignCallSlot != NULL, "Must be in foreign call."); + + // TODO: Validate this. It can fail if the user calls this inside another + // foreign method, or calls one of the return functions. + ObjClass* classObj = AS_CLASS(vm->foreignCallSlot[0]); + + ObjForeign* foreign = wrenNewForeign(vm, classObj, size); + vm->foreignCallSlot[0] = OBJ_VAL(foreign); + + return (void*)foreign->data; +} + // Execute [source] in the context of the core module. static WrenInterpretResult loadIntoCore(WrenVM* vm, const char* source) { @@ -1460,6 +1567,12 @@ void wrenPopRoot(WrenVM* vm) vm->numTempRoots--; } +int wrenGetArgumentCount(WrenVM* vm) +{ + ASSERT(vm->foreignCallSlot != NULL, "Must be in foreign call."); + return vm->foreignCallNumArgs; +} + static void validateForeignArgument(WrenVM* vm, int index) { ASSERT(vm->foreignCallSlot != NULL, "Must be in foreign call."); @@ -1485,6 +1598,15 @@ double wrenGetArgumentDouble(WrenVM* vm, int index) return AS_NUM(*(vm->foreignCallSlot + index)); } +void* wrenGetArgumentForeign(WrenVM* vm, int index) +{ + validateForeignArgument(vm, index); + + if (!IS_FOREIGN(*(vm->foreignCallSlot + index))) return NULL; + + return AS_FOREIGN(*(vm->foreignCallSlot + index))->data; +} + const char* wrenGetArgumentString(WrenVM* vm, int index) { validateForeignArgument(vm, index); diff --git a/src/vm/wren_vm.h b/src/vm/wren_vm.h index 1dfd3427..7e605537 100644 --- a/src/vm/wren_vm.h +++ b/src/vm/wren_vm.h @@ -125,7 +125,10 @@ struct WrenVM int foreignCallNumArgs; // The function used to locate foreign functions. - WrenBindForeignMethodFn bindForeign; + WrenBindForeignMethodFn bindForeignMethod; + + // The function used to locate foreign classes. + WrenBindForeignClassFn bindForeignClass; // The function used to load modules. WrenLoadModuleFn loadModule; diff --git a/test/api/foreign_class.c b/test/api/foreign_class.c new file mode 100644 index 00000000..4e12fc31 --- /dev/null +++ b/test/api/foreign_class.c @@ -0,0 +1,87 @@ +#include +#include + +#include "foreign_class.h" + +static void counterAllocate(WrenVM* vm) +{ + double* value = (double*)wrenAllocateForeign(vm, sizeof(double)); + *value = 0; +} + +static void counterIncrement(WrenVM* vm) +{ + double* value = (double*)wrenGetArgumentForeign(vm, 0); + double increment = wrenGetArgumentDouble(vm, 1); + + *value += increment; +} + +static void counterValue(WrenVM* vm) +{ + double value = *(double*)wrenGetArgumentForeign(vm, 0); + wrenReturnDouble(vm, value); +} + +static void pointAllocate(WrenVM* vm) +{ + double* coordinates = (double*)wrenAllocateForeign(vm, sizeof(double[3])); + + // This gets called by both constructors, so sniff the argument count to see + // which one was invoked. + if (wrenGetArgumentCount(vm) == 1) + { + coordinates[0] = 0.0; + coordinates[1] = 0.0; + coordinates[2] = 0.0; + } + else + { + coordinates[0] = wrenGetArgumentDouble(vm, 1); + coordinates[1] = wrenGetArgumentDouble(vm, 2); + coordinates[2] = wrenGetArgumentDouble(vm, 3); + } +} + +static void pointTranslate(WrenVM* vm) +{ + double* coordinates = (double*)wrenGetArgumentForeign(vm, 0); + coordinates[0] += wrenGetArgumentDouble(vm, 1); + coordinates[1] += wrenGetArgumentDouble(vm, 2); + coordinates[2] += wrenGetArgumentDouble(vm, 3); +} + +static void pointToString(WrenVM* vm) +{ + double* coordinates = (double*)wrenGetArgumentForeign(vm, 0); + char result[100]; + sprintf(result, "(%g, %g, %g)", + coordinates[0], coordinates[1], coordinates[2]); + wrenReturnString(vm, result, (int)strlen(result)); +} + +WrenForeignMethodFn foreignClassBindMethod(const char* signature) +{ + if (strcmp(signature, "Counter.increment(_)") == 0) return counterIncrement; + if (strcmp(signature, "Counter.value") == 0) return counterValue; + if (strcmp(signature, "Point.translate(_,_,_)") == 0) return pointTranslate; + if (strcmp(signature, "Point.toString") == 0) return pointToString; + + return NULL; +} + +void foreignClassBindClass( + const char* className, WrenForeignClassMethods* methods) +{ + if (strcmp(className, "Counter") == 0) + { + methods->allocate = counterAllocate; + return; + } + + if (strcmp(className, "Point") == 0) + { + methods->allocate = pointAllocate; + return; + } +} diff --git a/test/api/foreign_class.h b/test/api/foreign_class.h new file mode 100644 index 00000000..6c075a3f --- /dev/null +++ b/test/api/foreign_class.h @@ -0,0 +1,5 @@ +#include "wren.h" + +WrenForeignMethodFn foreignClassBindMethod(const char* signature); +void foreignClassBindClass( + const char* className, WrenForeignClassMethods* methods); diff --git a/test/api/foreign_class.wren b/test/api/foreign_class.wren new file mode 100644 index 00000000..781988bd --- /dev/null +++ b/test/api/foreign_class.wren @@ -0,0 +1,48 @@ +// Class with a default constructor. +foreign class Counter { + foreign increment(amount) + foreign value +} + +var counter = Counter.new() +IO.print(counter.value) // expect: 0 +counter.increment(3.1) +IO.print(counter.value) // expect: 3.1 +counter.increment(1.2) +IO.print(counter.value) // expect: 4.3 + +// Foreign classes can inherit a class as long as it has no fields. +class PointBase { + inherited() { + IO.print("inherited method") + } +} + +// Class with non-default constructor. +foreign class Point is PointBase { + construct new() { + IO.print("default") + } + + construct new(x, y, z) { + IO.print(x, ", ", y, ", ", z) + } + + foreign translate(x, y, z) + foreign toString +} + +var p = Point.new(1, 2, 3) // expect: 1, 2, 3 +IO.print(p) // expect: (1, 2, 3) +p.translate(3, 4, 5) +IO.print(p) // expect: (4, 6, 8) + +p = Point.new() // expect: default +IO.print(p) // expect: (0, 0, 0) + +p.inherited() // expect: inherited method + +var error = Fiber.new { + class Subclass is Point {} +}.try() +IO.print(error) // expect: Class 'Subclass' cannot inherit from foreign class 'Point'. diff --git a/test/api/main.c b/test/api/main.c index 5d8d732b..117b53c7 100644 --- a/test/api/main.c +++ b/test/api/main.c @@ -5,16 +5,23 @@ #include "vm.h" #include "wren.h" -#include "value.h" +#include "foreign_class.h" #include "returns.h" +#include "value.h" -#define REGISTER_TEST(name, camelCase) \ - if (strcmp(testName, #name) == 0) return camelCase##BindForeign(fullName) +#define REGISTER_METHOD(name, camelCase) \ + if (strcmp(testName, #name) == 0) return camelCase##BindMethod(fullName) + +#define REGISTER_CLASS(name, camelCase) \ + if (strcmp(testName, #name) == 0) \ + { \ + camelCase##BindClass(className, &methods); \ + } // The name of the currently executing API test. const char* testName; -static WrenForeignMethodFn bindForeign( +static WrenForeignMethodFn bindForeignMethod( WrenVM* vm, const char* module, const char* className, bool isStatic, const char* signature) { @@ -29,8 +36,9 @@ static WrenForeignMethodFn bindForeign( strcat(fullName, "."); strcat(fullName, signature); - REGISTER_TEST(returns, returns); - REGISTER_TEST(value, value); + REGISTER_METHOD(foreign_class, foreignClass); + REGISTER_METHOD(returns, returns); + REGISTER_METHOD(value, value); fprintf(stderr, "Unknown foreign method '%s' for test '%s'\n", fullName, testName); @@ -38,6 +46,18 @@ static WrenForeignMethodFn bindForeign( return NULL; } +static WrenForeignClassMethods bindForeignClass( + WrenVM* vm, const char* module, const char* className) +{ + WrenForeignClassMethods methods = { NULL, NULL }; + if (strcmp(module, "main") != 0) return methods; + + REGISTER_CLASS(foreign_class, foreignClass); + + return methods; +} + + int main(int argc, const char* argv[]) { if (argc != 2) @@ -54,6 +74,7 @@ int main(int argc, const char* argv[]) strcat(testPath, testName); strcat(testPath, ".wren"); - runFile(bindForeign, testPath); + setForeignCallbacks(bindForeignMethod, bindForeignClass); + runFile(testPath); return 0; } diff --git a/test/api/returns.c b/test/api/returns.c index 83ca0bcf..0e7d0700 100644 --- a/test/api/returns.c +++ b/test/api/returns.c @@ -27,7 +27,7 @@ static void returnFalse(WrenVM* vm) wrenReturnBool(vm, false); } -WrenForeignMethodFn returnsBindForeign(const char* signature) +WrenForeignMethodFn returnsBindMethod(const char* signature) { if (strcmp(signature, "static Api.implicitNull") == 0) return implicitNull; if (strcmp(signature, "static Api.returnInt") == 0) return returnInt; diff --git a/test/api/returns.h b/test/api/returns.h index 7b1c2a1b..21fa57cd 100644 --- a/test/api/returns.h +++ b/test/api/returns.h @@ -1,3 +1,3 @@ #include "wren.h" -WrenForeignMethodFn returnsBindForeign(const char* signature); +WrenForeignMethodFn returnsBindMethod(const char* signature); diff --git a/test/api/value.c b/test/api/value.c index d8fd3c3b..53022054 100644 --- a/test/api/value.c +++ b/test/api/value.c @@ -15,7 +15,7 @@ static void getValue(WrenVM* vm) wrenReleaseValue(vm, value); } -WrenForeignMethodFn valueBindForeign(const char* signature) +WrenForeignMethodFn valueBindMethod(const char* signature) { if (strcmp(signature, "static Api.value=(_)") == 0) return setValue; if (strcmp(signature, "static Api.value") == 0) return getValue; diff --git a/test/api/value.h b/test/api/value.h index 11229246..dcdea52a 100644 --- a/test/api/value.h +++ b/test/api/value.h @@ -1,3 +1,3 @@ #include "wren.h" -WrenForeignMethodFn valueBindForeign(const char* signature); +WrenForeignMethodFn valueBindMethod(const char* signature); diff --git a/test/language/class/field_in_foreign_class.wren b/test/language/class/field_in_foreign_class.wren new file mode 100644 index 00000000..5b7c2c26 --- /dev/null +++ b/test/language/class/field_in_foreign_class.wren @@ -0,0 +1,9 @@ +foreign class Foo { + bar { + // Can't read a field. + IO.print(_bar) // expect error + + // Or write one. + _bar = "value" // expect error + } +} diff --git a/test/language/class/foreign_class_inherit_fields.wren b/test/language/class/foreign_class_inherit_fields.wren new file mode 100644 index 00000000..1be9bbb6 --- /dev/null +++ b/test/language/class/foreign_class_inherit_fields.wren @@ -0,0 +1,7 @@ +class Foo { + method() { + _field = "value" + } +} + +foreign class Bar is Foo {} // expect runtime error: Foreign class 'Bar' may not inherit from a class with fields. diff --git a/test/language/class/missing_class_after_foreign.wren b/test/language/class/missing_class_after_foreign.wren new file mode 100644 index 00000000..b8884850 --- /dev/null +++ b/test/language/class/missing_class_after_foreign.wren @@ -0,0 +1,2 @@ +// Missing "class". +foreign blah A {} // expect error diff --git a/test/language/inheritance/inherit_from_class.wren b/test/language/inheritance/inherit_from_class.wren index d4f8b095..3a256692 100644 --- a/test/language/inheritance/inherit_from_class.wren +++ b/test/language/inheritance/inherit_from_class.wren @@ -1 +1 @@ -class Subclass is Class {} // expect runtime error: Subclass cannot inherit from Class. +class Subclass is Class {} // expect runtime error: Class 'Subclass' cannot inherit from built-in class 'Class'. diff --git a/test/language/inheritance/inherit_from_closure.wren b/test/language/inheritance/inherit_from_closure.wren index 85d79f74..c79e8d01 100644 --- a/test/language/inheritance/inherit_from_closure.wren +++ b/test/language/inheritance/inherit_from_closure.wren @@ -5,4 +5,4 @@ var ClosureType ClosureType = Fn.new { IO.print(a) }.type } -class Subclass is ClosureType {} // expect runtime error: Subclass cannot inherit from Fn. +class Subclass is ClosureType {} // expect runtime error: Class 'Subclass' cannot inherit from built-in class 'Fn'. diff --git a/test/language/inheritance/inherit_from_fiber.wren b/test/language/inheritance/inherit_from_fiber.wren index d620fc17..ab2f59d7 100644 --- a/test/language/inheritance/inherit_from_fiber.wren +++ b/test/language/inheritance/inherit_from_fiber.wren @@ -1 +1 @@ -class Subclass is Fiber {} // expect runtime error: Subclass cannot inherit from Fiber. +class Subclass is Fiber {} // expect runtime error: Class 'Subclass' cannot inherit from built-in class 'Fiber'. diff --git a/test/language/inheritance/inherit_from_fn.wren b/test/language/inheritance/inherit_from_fn.wren index 97a03fda..830bf730 100644 --- a/test/language/inheritance/inherit_from_fn.wren +++ b/test/language/inheritance/inherit_from_fn.wren @@ -1 +1 @@ -class Subclass is Fn {} // expect runtime error: Subclass cannot inherit from Fn. +class Subclass is Fn {} // expect runtime error: Class 'Subclass' cannot inherit from built-in class 'Fn'. diff --git a/test/language/inheritance/inherit_from_list.wren b/test/language/inheritance/inherit_from_list.wren index b4237e05..cba3ca47 100644 --- a/test/language/inheritance/inherit_from_list.wren +++ b/test/language/inheritance/inherit_from_list.wren @@ -1 +1 @@ -class Subclass is List {} // expect runtime error: Subclass cannot inherit from List. +class Subclass is List {} // expect runtime error: Class 'Subclass' cannot inherit from built-in class 'List'. diff --git a/test/language/inheritance/inherit_from_map.wren b/test/language/inheritance/inherit_from_map.wren index e1c97ba3..d1fe2823 100644 --- a/test/language/inheritance/inherit_from_map.wren +++ b/test/language/inheritance/inherit_from_map.wren @@ -1 +1 @@ -class Subclass is Map {} // expect runtime error: Subclass cannot inherit from Map. +class Subclass is Map {} // expect runtime error: Class 'Subclass' cannot inherit from built-in class 'Map'. diff --git a/test/language/inheritance/inherit_from_nonclass.wren b/test/language/inheritance/inherit_from_nonclass.wren index e849fc89..31fb047f 100644 --- a/test/language/inheritance/inherit_from_nonclass.wren +++ b/test/language/inheritance/inherit_from_nonclass.wren @@ -1 +1 @@ -class Foo is 123 {} // expect runtime error: Must inherit from a class. +class Foo is 123 {} // expect runtime error: Class 'Foo' cannot inherit from a non-class object. diff --git a/test/language/inheritance/inherit_from_range.wren b/test/language/inheritance/inherit_from_range.wren index 88c61a2a..1550df01 100644 --- a/test/language/inheritance/inherit_from_range.wren +++ b/test/language/inheritance/inherit_from_range.wren @@ -1 +1 @@ -class Subclass is Range {} // expect runtime error: Subclass cannot inherit from Range. +class Subclass is Range {} // expect runtime error: Class 'Subclass' cannot inherit from built-in class 'Range'. diff --git a/test/language/inheritance/inherit_from_string.wren b/test/language/inheritance/inherit_from_string.wren index ca9544eb..42285bdf 100644 --- a/test/language/inheritance/inherit_from_string.wren +++ b/test/language/inheritance/inherit_from_string.wren @@ -1 +1 @@ -class Subclass is String {} // expect runtime error: Subclass cannot inherit from String. +class Subclass is String {} // expect runtime error: Class 'Subclass' cannot inherit from built-in class 'String'. diff --git a/test/language/inheritance/inherit_methods.wren b/test/language/inheritance/inherit_methods.wren index 1361b581..164c8668 100644 --- a/test/language/inheritance/inherit_methods.wren +++ b/test/language/inheritance/inherit_methods.wren @@ -22,5 +22,3 @@ bar.method(1, 2) // expect: bar bar.method(1, 2, 3) // expect: foo bar.method(1, 2, 3, 4) // expect: bar bar.override // expect: bar - -// TODO: Prevent extending built-in types.