From 507b2ad68cecf9aee2dd734ed99687208c84ebf3 Mon Sep 17 00:00:00 2001 From: Michel Hermier Date: Thu, 1 Dec 2022 09:09:26 +0100 Subject: [PATCH 1/4] Reserve `implements` keyword --- src/vm/wren_compiler.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vm/wren_compiler.c b/src/vm/wren_compiler.c index 92b16cac0..cf469b42f 100644 --- a/src/vm/wren_compiler.c +++ b/src/vm/wren_compiler.c @@ -96,6 +96,7 @@ typedef enum TOKEN_FOR, TOKEN_FOREIGN, TOKEN_IF, + TOKEN_IMPLEMENTS, TOKEN_IMPORT, TOKEN_AS, TOKEN_IN, @@ -608,6 +609,7 @@ static Keyword keywords[] = {"for", 3, TOKEN_FOR}, {"foreign", 7, TOKEN_FOREIGN}, {"if", 2, TOKEN_IF}, + {"implements", 10, TOKEN_IMPLEMENTS}, {"import", 6, TOKEN_IMPORT}, {"as", 2, TOKEN_AS}, {"in", 2, TOKEN_IN}, @@ -2791,6 +2793,7 @@ GrammarRule rules[] = /* TOKEN_FOR */ UNUSED, /* TOKEN_FOREIGN */ UNUSED, /* TOKEN_IF */ UNUSED, + /* TOKEN_IMPLEMENTS */ UNUSED, /* TOKEN_IMPORT */ UNUSED, /* TOKEN_AS */ UNUSED, /* TOKEN_IN */ UNUSED, From 9b4ebe1a4811b11837c85c651ba2a2c585b98ba1 Mon Sep 17 00:00:00 2001 From: Michel Hermier Date: Sun, 4 Dec 2022 18:32:00 +0100 Subject: [PATCH 2/4] Add `wrenClassImplements` --- src/vm/wren_value.c | 27 +++++++++++++++++++++++++++ src/vm/wren_value.h | 3 +++ 2 files changed, 30 insertions(+) diff --git a/src/vm/wren_value.c b/src/vm/wren_value.c index c49a3b6be..3fc84d4fe 100644 --- a/src/vm/wren_value.c +++ b/src/vm/wren_value.c @@ -131,6 +131,33 @@ void wrenBindMethod(WrenVM* vm, ObjClass* classObj, int symbol, Method method) classObj->methods.data[symbol] = method; } +int wrenClassImplements(WrenVM* vm, const ObjClass* classObj, + Value interface) +{ + if (IS_CLASS(interface)) + { + const ObjClass *interfaceObj = AS_CLASS(interface); + + // The following shortpath can't be added because, constructors and static + // method should be ignored. + // if (classObj->methods.count < interfaceObj->methods.count) return false; + + for (int i = 0; i < interfaceObj->methods.count; i++) + { + if (interfaceObj->methods.data[i].type == METHOD_NONE) continue; + + // Check if it is a constructor initializer + if (strncmp(vm->methodNames.data[i]->value, "init ", 5) == 0) continue; + + if (i >= classObj->methods.count || + classObj->methods.data[i].type == METHOD_NONE) return i; + } + return -2; + } + + return -1; +} + ObjClosure* wrenNewClosure(WrenVM* vm, ObjFn* fn) { ObjClosure* closure = ALLOCATE_FLEX(vm, ObjClosure, diff --git a/src/vm/wren_value.h b/src/vm/wren_value.h index 2ca0cfdcd..1912db616 100644 --- a/src/vm/wren_value.h +++ b/src/vm/wren_value.h @@ -633,6 +633,9 @@ ObjClass* wrenNewClass(WrenVM* vm, ObjClass* superclass, int numFields, void wrenBindMethod(WrenVM* vm, ObjClass* classObj, int symbol, Method method); +int wrenClassImplements(WrenVM* vm, const ObjClass* classObj, + Value interface); + // Creates a new closure object that invokes [fn]. Allocates room for its // upvalues, but assumes outside code will populate it. ObjClosure* wrenNewClosure(WrenVM* vm, ObjFn* fn); From bed763ab72650733b1b0f8b41bfba7da27e19aea Mon Sep 17 00:00:00 2001 From: Michel Hermier Date: Thu, 1 Dec 2022 10:01:49 +0100 Subject: [PATCH 3/4] Add `implements` operator --- doc/site/modules/core/object.markdown | 20 +++++++++++ src/vm/wren_compiler.c | 4 +-- src/vm/wren_core.c | 12 +++++++ test/core/object/implements.wren | 50 +++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 test/core/object/implements.wren diff --git a/doc/site/modules/core/object.markdown b/doc/site/modules/core/object.markdown index 58d69cb5c..b22a8fc04 100644 --- a/doc/site/modules/core/object.markdown +++ b/doc/site/modules/core/object.markdown @@ -29,6 +29,26 @@ Compares two objects using built-in equality. This compares [value types](../../values.html) by value, and all other objects are compared by identity—two objects are equal only if they are the exact same object. +### **implements**(class) operator + +Returns `true` if this object's class implements the interface of `class`. + +It is a runtime error if `class` is not a [Class][]. + +
+System.print(123 implements Num)              //> true
+System.print("s" implements Num)              //> false
+System.print(null implements String)          //> false
+System.print([] implements List)              //> true
+
+class IterableInterface {
+  iterate()       { subclassResponsibility }
+  iteratorValue() { subclassResponsibility }
+}
+
+System.print([] implements IterableInterface) //> true
+
+ ### **is**(class) operator Returns `true` if this object's class or one of its superclasses is `class`. diff --git a/src/vm/wren_compiler.c b/src/vm/wren_compiler.c index cf469b42f..81d167485 100644 --- a/src/vm/wren_compiler.c +++ b/src/vm/wren_compiler.c @@ -1719,7 +1719,7 @@ typedef enum PREC_LOGICAL_OR, // || PREC_LOGICAL_AND, // && PREC_EQUALITY, // == != - PREC_IS, // is + PREC_IS, // implements is PREC_COMPARISON, // < > <= >= PREC_BITWISE_OR, // | PREC_BITWISE_XOR, // ^ @@ -2793,7 +2793,7 @@ GrammarRule rules[] = /* TOKEN_FOR */ UNUSED, /* TOKEN_FOREIGN */ UNUSED, /* TOKEN_IF */ UNUSED, - /* TOKEN_IMPLEMENTS */ UNUSED, + /* TOKEN_IMPLEMENTS */ INFIX_OPERATOR(PREC_IS, "implements"), /* TOKEN_IMPORT */ UNUSED, /* TOKEN_AS */ UNUSED, /* TOKEN_IN */ UNUSED, diff --git a/src/vm/wren_core.c b/src/vm/wren_core.c index d0a121f8c..6e39ab93c 100644 --- a/src/vm/wren_core.c +++ b/src/vm/wren_core.c @@ -863,6 +863,17 @@ DEF_PRIMITIVE(object_bangeq) RETURN_BOOL(!wrenValuesEqual(args[0], args[1])); } +DEF_PRIMITIVE(object_implements) +{ + ObjClass* classObj = wrenGetClassInline(vm, args[0]); + if (!IS_CLASS(args[1])) + { + RETURN_ERROR("Right operand must be a class."); + } + + RETURN_BOOL(wrenClassImplements(vm, classObj, args[1]) == -2); +} + DEF_PRIMITIVE(object_is) { if (!IS_CLASS(args[1])) @@ -1247,6 +1258,7 @@ void wrenInitializeCore(WrenVM* vm) PRIMITIVE(vm->objectClass, "!", object_not); PRIMITIVE(vm->objectClass, "==(_)", object_eqeq); PRIMITIVE(vm->objectClass, "!=(_)", object_bangeq); + PRIMITIVE(vm->objectClass, "implements(_)", object_implements); PRIMITIVE(vm->objectClass, "is(_)", object_is); PRIMITIVE(vm->objectClass, "toString", object_toString); PRIMITIVE(vm->objectClass, "type", object_type); diff --git a/test/core/object/implements.wren b/test/core/object/implements.wren new file mode 100644 index 000000000..62547cb3d --- /dev/null +++ b/test/core/object/implements.wren @@ -0,0 +1,50 @@ +class Foo { + construct new() {} +} + +class EmptyInterface { +} + +class ConstructorInterface { + construct constructor_should_be_ingored_by_implements() { subclassResponsibility() } +} + +class IterableInterface { + iterate(iterator) { subclassResponsibility() } + iteratorValue(iterator) { subclassResponsibility() } +} + +class StaticMethodInterface { + static static_methods_should_be_ingored_by_implements() { subclassResponsibility() } +} + +// Everything implements the empty interface. +System.print(Object implements EmptyInterface) // expect: true +System.print(Foo.new() implements EmptyInterface) // expect: true +System.print([] implements EmptyInterface) // expect: true +System.print({} implements EmptyInterface) // expect: true +System.print(0..1 implements EmptyInterface) // expect: true + +// Everything should ignore constructors +System.print(Object implements ConstructorInterface) // expect: true +System.print(Foo.new() implements ConstructorInterface) // expect: true +System.print([] implements ConstructorInterface) // expect: true +System.print({} implements ConstructorInterface) // expect: true +System.print(0..1 implements ConstructorInterface) // expect: true + +// Test some plausible interfaces +System.print(Foo.new() implements IterableInterface) // expect: false +System.print([] implements IterableInterface) // expect: true +System.print({} implements IterableInterface) // expect: true +System.print(0..1 implements IterableInterface) // expect: true + +// Everything should ignore static methods +System.print(Object implements StaticMethodInterface) // expect: true +System.print(Foo.new() implements StaticMethodInterface) // expect: true +System.print([] implements StaticMethodInterface) // expect: true +System.print({} implements StaticMethodInterface) // expect: true +System.print(0..1 implements StaticMethodInterface) // expect: true + +// Ignore newline after "implements". +System.print(123 implements + Num) // expect: true From 47f2742cd403998f3609409dbc57c0369c8284b6 Mon Sep 17 00:00:00 2001 From: Michel Hermier Date: Sun, 4 Dec 2022 17:59:33 +0100 Subject: [PATCH 4/4] Add `implements` class definitions --- doc/site/classes.markdown | 26 ++++++++++++++ src/vm/wren_compiler.c | 22 ++++++++++++ src/vm/wren_opcodes.h | 3 ++ src/vm/wren_vm.c | 31 +++++++++++++++++ .../class/implements/invalid_type.wren | 3 ++ .../class/implements/missing_method.wren | 7 ++++ test/language/class/implements/syntax.wren | 34 +++++++++++++++++++ 7 files changed, 126 insertions(+) create mode 100644 test/language/class/implements/invalid_type.wren create mode 100644 test/language/class/implements/missing_method.wren create mode 100644 test/language/class/implements/syntax.wren diff --git a/doc/site/classes.markdown b/doc/site/classes.markdown index 2c06104a3..fdc799ca4 100644 --- a/doc/site/classes.markdown +++ b/doc/site/classes.markdown @@ -591,6 +591,32 @@ class Pegasus is Unicorn { Pegasus.new("Fred") //> My name is Fred +## Interfaces + +A class can implements the methods of other interface classes. + +By default it does not check for any interface class. A different list of +interfaces classes can be passed using `implements` when declaring a class: + +
+class Countable {
+  count               { subclassResponsibility }
+}
+
+class Iterable {
+  iterate(iter)       { subclassResponsibility }
+  iteratorValue(iter) { subclassResponsibility }
+}
+
+class EmptyList implements Countable, Iterable {
+  construct new()     {}
+
+  count               { 0 }
+  iterate(iter)       { false }
+  iteratorValue(iter) { Fiber.abort("This should be unreachable") }
+}
+
+ ## Super **TODO: Integrate better into page. Should explain this before mentioning diff --git a/src/vm/wren_compiler.c b/src/vm/wren_compiler.c index 81d167485..e0fa26a5c 100644 --- a/src/vm/wren_compiler.c +++ b/src/vm/wren_compiler.c @@ -2889,6 +2889,7 @@ static int getByteCountForArguments(const uint8_t* bytecode, case CODE_FOREIGN_CLASS: case CODE_END_MODULE: case CODE_END_CLASS: + case CODE_ASSERT_IMPLEMENTED_BY: return 0; case CODE_LOAD_LOCAL: @@ -3557,6 +3558,21 @@ static void classDefinition(Compiler* compiler, bool isForeign) // Store it in its name. defineVariable(compiler, classVariable.index); + size_t implementsCount = 0; + if (match(compiler, TOKEN_IMPLEMENTS)) + { + // Push the interface type list onto the stack. + do + { + ignoreNewlines(compiler); + + implementsCount++; + + // The interface type. + expression(compiler); + } while (match(compiler, TOKEN_COMMA)); + } + // Push a local variable scope. Static fields in a class body are hoisted out // into local variables declared in this scope. Methods that use them will // have upvalues referencing them. @@ -3621,6 +3637,12 @@ static void classDefinition(Compiler* compiler, bool isForeign) (uint8_t)classInfo.fields.count; } + for (size_t i = 0; i < implementsCount; i++) + { + loadVariable(compiler, classVariable); + emitOp(compiler, CODE_ASSERT_IMPLEMENTED_BY); + } + // Clear symbol tables for tracking field and method names. wrenSymbolTableClear(compiler->parser->vm, &classInfo.fields); wrenIntBufferClear(compiler->parser->vm, &classInfo.methods); diff --git a/src/vm/wren_opcodes.h b/src/vm/wren_opcodes.h index 46ba8b47a..89dcf4ca3 100644 --- a/src/vm/wren_opcodes.h +++ b/src/vm/wren_opcodes.h @@ -169,6 +169,9 @@ OPCODE(FOREIGN_CONSTRUCT, 0) // the name of the class. Byte [arg] is the number of fields in the class. OPCODE(CLASS, -1) +// +OPCODE(ASSERT_IMPLEMENTED_BY, -2) + // Ends a class. // Atm the stack contains the class and the ClassAttributes (or null). OPCODE(END_CLASS, -2) diff --git a/src/vm/wren_vm.c b/src/vm/wren_vm.c index 254d0b037..a4702d9a9 100644 --- a/src/vm/wren_vm.c +++ b/src/vm/wren_vm.c @@ -607,6 +607,30 @@ static void bindForeignClass(WrenVM* vm, ObjClass* classObj, ObjModule* module) } } +static void assertImplementedBy(WrenVM* vm) +{ + // Pull the attributes and class off the stack + Value interfaceValue = vm->fiber->stackTop[-2]; + Value classValue = vm->fiber->stackTop[-1]; + + if (IS_CLASS(classValue)) + { + ObjClass* classObj = AS_CLASS(classValue); + int ret = wrenClassImplements(vm, classObj, interfaceValue); + + if (ret == -1) + { + vm->fiber->error = wrenStringFormat(vm, + "$ is not a valid interface object.", + wrenGetClassInline(vm, interfaceValue)->name->value ); + } + if (ret >= 0) methodNotFound(vm, classObj, ret); + } + + // Remove the stack items + vm->fiber->stackTop -= 2; +} + // Completes the process for creating a new class. // // The class attributes instance and the class itself should be on the @@ -1295,6 +1319,13 @@ static WrenInterpretResult runInterpreter(WrenVM* vm, register ObjFiber* fiber) DISPATCH(); } + CASE_CODE(ASSERT_IMPLEMENTED_BY): + { + assertImplementedBy(vm); + if (wrenHasError(fiber)) RUNTIME_ERROR(); + DISPATCH(); + } + CASE_CODE(END_CLASS): { endClass(vm); diff --git a/test/language/class/implements/invalid_type.wren b/test/language/class/implements/invalid_type.wren new file mode 100644 index 000000000..06411d8ce --- /dev/null +++ b/test/language/class/implements/invalid_type.wren @@ -0,0 +1,3 @@ + +class Foo implements 42 { +} // expect runtime error: Num is not a valid interface object. diff --git a/test/language/class/implements/missing_method.wren b/test/language/class/implements/missing_method.wren new file mode 100644 index 000000000..7c296429b --- /dev/null +++ b/test/language/class/implements/missing_method.wren @@ -0,0 +1,7 @@ + +class Foo implements Object { + foo { "Bug" } +} + +class Bar implements Foo { +} // expect runtime error: Bar does not implement 'foo'. diff --git a/test/language/class/implements/syntax.wren b/test/language/class/implements/syntax.wren new file mode 100644 index 000000000..47e17b8e9 --- /dev/null +++ b/test/language/class/implements/syntax.wren @@ -0,0 +1,34 @@ + +class Foo implements Object { + foo { "Bug" } +} + +class Bar implements Foo { + construct new() {} + + foo { "Hello world!" } +} + +System.print(Bar.new().foo) // expect: Hello world! + +// Check for multiple inheritance +class Countable { + count { subclassResponsibility } +} + +class Iterable { + iterate(iter) { subclassResponsibility } + iteratorValue(iter) { subclassResponsibility } +} + +class EmptyList implements Countable, Iterable { + construct new() {} + + count { 0 } + iterate(iter) { false } + iteratorValue(iter) { Fiber.abort("This should be unreachable") } +} + +var emptyList = EmptyList.new() +System.print(emptyList.count) // expect: 0 +System.print(emptyList.iterate(null)) // expect: false