Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic interface capabilities. #1126

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions doc/site/classes.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,32 @@ class Pegasus is Unicorn {
Pegasus.new("Fred") //> My name is Fred
</pre>

## 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:

<pre class="snippet">
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") }
}
</pre>

## Super

**TODO: Integrate better into page. Should explain this before mentioning
Expand Down
20 changes: 20 additions & 0 deletions doc/site/modules/core/object.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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&mdash;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][].

<pre class="snippet">
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
</pre>

### **is**(class) operator

Returns `true` if this object's class or one of its superclasses is `class`.
Expand Down
27 changes: 26 additions & 1 deletion src/vm/wren_compiler.c
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ typedef enum
TOKEN_FOR,
TOKEN_FOREIGN,
TOKEN_IF,
TOKEN_IMPLEMENTS,
TOKEN_IMPORT,
TOKEN_AS,
TOKEN_IN,
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -1717,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, // ^
Expand Down Expand Up @@ -2791,6 +2793,7 @@ GrammarRule rules[] =
/* TOKEN_FOR */ UNUSED,
/* TOKEN_FOREIGN */ UNUSED,
/* TOKEN_IF */ UNUSED,
/* TOKEN_IMPLEMENTS */ INFIX_OPERATOR(PREC_IS, "implements"),
/* TOKEN_IMPORT */ UNUSED,
/* TOKEN_AS */ UNUSED,
/* TOKEN_IN */ UNUSED,
Expand Down Expand Up @@ -2886,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:
Expand Down Expand Up @@ -3554,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.
Expand Down Expand Up @@ -3618,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);
Expand Down
12 changes: 12 additions & 0 deletions src/vm/wren_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/vm/wren_opcodes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions src/vm/wren_value.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/vm/wren_value.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 31 additions & 0 deletions src/vm/wren_vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
50 changes: 50 additions & 0 deletions test/core/object/implements.wren
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions test/language/class/implements/invalid_type.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

class Foo implements 42 {
} // expect runtime error: Num is not a valid interface object.
7 changes: 7 additions & 0 deletions test/language/class/implements/missing_method.wren
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

class Foo implements Object {
foo { "Bug" }
}

class Bar implements Foo {
} // expect runtime error: Bar does not implement 'foo'.
34 changes: 34 additions & 0 deletions test/language/class/implements/syntax.wren
Original file line number Diff line number Diff line change
@@ -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