Skip to content
This repository has been archived by the owner on Jul 3, 2022. It is now read-only.

Commit

Permalink
Merge pull request #54 from RoelAdriaans/feature/add-chapter-13-inher…
Browse files Browse the repository at this point in the history
…itance
  • Loading branch information
github-actions[bot] authored Nov 1, 2020
2 parents 59510c5 + 10173f1 commit 5c72a19
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 8 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.0.10] - 2020-11-01

### Added

- Completing chapter 13
- Inheritance works, circular inheritance is catched, calling super
- Added some warnings for super outside a class

## [0.0.9] - 2020-10-25

### Added
Expand Down
4 changes: 4 additions & 0 deletions src/yaplox/ast_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Literal,
Logical,
Set,
Super,
This,
Unary,
Variable,
Expand Down Expand Up @@ -42,6 +43,9 @@ def visit_logical_expr(self, expr: Logical):
def visit_set_expr(self, expr: Set):
pass

def visit_super_expr(self, expr: Super):
pass

def visit_this_expr(self, expr: This):
pass

Expand Down
1 change: 1 addition & 0 deletions src/yaplox/class_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
class ClassType(enum.Enum):
NONE = enum.auto()
CLASS = enum.auto()
SUBCLASS = enum.auto()
14 changes: 14 additions & 0 deletions src/yaplox/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def visit_logical_expr(self, expr: Logical):
def visit_set_expr(self, expr: Set):
raise NotImplementedError

@abstractmethod
def visit_super_expr(self, expr: Super):
raise NotImplementedError

@abstractmethod
def visit_this_expr(self, expr: This):
raise NotImplementedError
Expand Down Expand Up @@ -145,6 +149,16 @@ def accept(self, visitor: ExprVisitor):
return visitor.visit_set_expr(self)


class Super(Expr):
def __init__(self, keyword: Token, method: Token):
self.keyword = keyword
self.method = method

def accept(self, visitor: ExprVisitor):
""" Create a accept method that calls the visitor. """
return visitor.visit_super_expr(self)


class This(Expr):
def __init__(self, keyword: Token):
self.keyword = keyword
Expand Down
36 changes: 35 additions & 1 deletion src/yaplox/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Literal,
Logical,
Set,
Super,
This,
Unary,
Variable,
Expand Down Expand Up @@ -199,6 +200,21 @@ def visit_set_expr(self, expr: Set):
obj.set(expr.name, value)
return value

def visit_super_expr(self, expr: Super):
distance = self.locals[expr]
superclass: YaploxClass = self.environment.get_at(
distance=distance, name="super"
)
obj = self.environment.get_at(distance=distance - 1, name="this")
method = superclass.find_method(expr.method.lexeme)

# Check that we have a super method
if method is None:
raise YaploxRuntimeError(
expr.method, f"Undefined property '{expr.method.lexeme}'."
)
return method.bind(obj)

def visit_this_expr(self, expr: This):
return self._look_up_variable(expr.keyword, expr)

Expand Down Expand Up @@ -259,8 +275,20 @@ def visit_assign_expr(self, expr: "Assign") -> Any:

# statement stuff
def visit_class_stmt(self, stmt: Class):
superclass = None
if stmt.superclass is not None:
superclass = self._evaluate(stmt.superclass)
if not isinstance(superclass, YaploxClass):
raise YaploxRuntimeError(
stmt.superclass.name, "Superclass must be a class."
)

self.environment.define(stmt.name.lexeme, None)

if stmt.superclass is not None:
self.environment = Environment(self.environment)
self.environment.define("super", superclass)

methods: Dict[str, YaploxFunction] = {}

for method in stmt.methods:
Expand All @@ -269,7 +297,13 @@ def visit_class_stmt(self, stmt: Class):
)
methods[method.name.lexeme] = function

klass = YaploxClass(stmt.name.lexeme, methods)
klass = YaploxClass(
name=stmt.name.lexeme, superclass=superclass, methods=methods
)

if stmt.superclass is not None:
self.environment = self.environment.enclosing # type: ignore

self.environment.assign(stmt.name, klass)

def visit_expression_stmt(self, stmt: Expression) -> None:
Expand Down
18 changes: 17 additions & 1 deletion src/yaplox/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Literal,
Logical,
Set,
Super,
This,
Unary,
Variable,
Expand Down Expand Up @@ -71,14 +72,20 @@ def _declaration(self) -> Optional[Stmt]:

def _class_declaration(self) -> Stmt:
name = self._consume(TokenType.IDENTIFIER, "Expect class name.")

superclass = None
if self._match(TokenType.LESS):
self._consume(TokenType.IDENTIFIER, "Expect superclass name.")
superclass = Variable(self._previous())

self._consume(TokenType.LEFT_BRACE, "Expect '{' before class body.")

methods = []
while not self._check(TokenType.RIGHT_BRACE) and not self._is_at_end():
methods.append(self._function("method"))

self._consume(TokenType.RIGHT_BRACE, "Expect '}' after class body.")
return Class(name=name, methods=methods)
return Class(name=name, superclass=superclass, methods=methods)

def _function(self, kind: str) -> Function:
name = self._consume(TokenType.IDENTIFIER, f"Expect {kind} name.")
Expand Down Expand Up @@ -365,6 +372,15 @@ def _primary(self) -> Expr:
if self._match(TokenType.NUMBER, TokenType.STRING):
return Literal(self._previous().literal)

if self._match(TokenType.SUPER):
keyword = self._previous()
self._consume(TokenType.DOT, "Expect '.' after 'super'.")
method = self._consume(
TokenType.IDENTIFIER, "Expect superclass method name."
)

return Super(keyword, method)

if self._match(TokenType.THIS):
return This(self._previous())

Expand Down
25 changes: 25 additions & 0 deletions src/yaplox/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Literal,
Logical,
Set,
Super,
This,
Unary,
Variable,
Expand Down Expand Up @@ -154,6 +155,16 @@ def visit_set_expr(self, expr: Set):
self._resolve_expression(expr.value)
self._resolve_expression(expr.obj)

def visit_super_expr(self, expr: Super):
if self.current_class == ClassType.NONE:
self.on_error(expr.keyword, "Can't use 'super' outside of a class.")
elif self.current_class != ClassType.SUBCLASS:
self.on_error(
expr.keyword, "Can't use 'super' in a class with no superclass."
)

self._resolve_local(expr, expr.keyword)

def visit_unary_expr(self, expr: Unary):
self._resolve_expression(expr.right)

Expand All @@ -176,6 +187,17 @@ def visit_class_stmt(self, stmt: Class):
self._declare(stmt.name)
self._define(stmt.name)

if stmt.superclass and stmt.name.lexeme == stmt.superclass.name.lexeme:
self.on_error(stmt.superclass.name, "A class can't inherit from itself.")

if stmt.superclass is not None:
self.current_class = ClassType.SUBCLASS
self._resolve_expression(stmt.superclass)

if stmt.superclass is not None:
self._begin_scope()
self.scopes[-1]["super"] = True

self._begin_scope()
self.scopes[-1]["this"] = True

Expand All @@ -188,6 +210,9 @@ def visit_class_stmt(self, stmt: Class):

self._end_scope()

if stmt.superclass is not None:
self._end_scope()

self.current_class = enclosing_class

def visit_expression_stmt(self, stmt: Expression):
Expand Down
7 changes: 5 additions & 2 deletions src/yaplox/stmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from abc import ABC, abstractmethod
from typing import List, Optional

from yaplox.expr import Expr
from yaplox.expr import Expr, Variable
from yaplox.token import Token


Expand Down Expand Up @@ -66,8 +66,11 @@ def accept(self, visitor: StmtVisitor):


class Class(Stmt):
def __init__(self, name: Token, methods: List[Function]):
def __init__(
self, name: Token, superclass: Optional[Variable], methods: List[Function]
):
self.name = name
self.superclass = superclass
self.methods = methods

def accept(self, visitor: StmtVisitor):
Expand Down
15 changes: 13 additions & 2 deletions src/yaplox/yaplox_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ def arity(self) -> int:
else:
return 0

def __init__(self, name: str, methods: Dict[str, YaploxFunction]):
def __init__(
self,
name: str,
superclass: Optional[YaploxClass],
methods: Dict[str, YaploxFunction],
):
self.name = name
self.superclass = superclass
self.methods = methods

def __repr__(self):
Expand All @@ -38,4 +44,9 @@ def find_method(self, name: str) -> Optional[YaploxFunction]:
try:
return self.methods[name]
except KeyError:
return None
pass

if self.superclass:
return self.superclass.find_method(name)

return None
118 changes: 118 additions & 0 deletions tests/test_class_inheritance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
class TestClassesInheretance:
def test_class_circular(self, run_code_block):
line = "class Oops < Oops {}"

assert (
run_code_block(line).err
== "[line 1] Error at 'Oops' : A class can't inherit from itself.\n"
)
assert run_code_block(line).out == ""

def test_class_not_a_class(self, run_code_block):
lines = """
var NotAClass = "I am totally not a class";
class Subclass < NotAClass {} // ?!
"""
assert (
run_code_block(lines).err == "Superclass must be a class. in line [line3]\n"
)
assert run_code_block(lines).out == ""

def test_class_inheritance(self, run_code_block):
lines = """
class Doughnut {
cook() {
print "Fry until golden brown.";
}
}
class BostonCream < Doughnut {}
BostonCream().cook();
"""

assert run_code_block(lines).err == ""
assert run_code_block(lines).out == "Fry until golden brown.\n"

def test_class_call_super(self, run_code_block):
lines = """
class Doughnut {
cook() {
print "Fry until golden brown.";
}
}
class BostonCream < Doughnut {
cook() {
super.cook();
print "Pipe full of custard and coat with chocolate.";
}
}
BostonCream().cook();
// Prints:
// Fry until golden brown.
// Pipe full of custard and coat with chocolate."""

captured = run_code_block(lines)

assert captured.err == ""
assert (
captured.out == "Fry until golden brown.\n"
"Pipe full of custard and coat with chocolate.\n"
)

def test_multiple_inherit(self, run_code_block):
lines = """
class A {
method() {
print "A method";
}
}
class B < A {
method() {
print "B method";
}
test() {
super.method();
}
}
class C < B {}
C().test();
"""
captured = run_code_block(lines)

assert captured.err == ""
assert captured.out == "A method\n"

def test_invalid_super(self, run_code_block):
lines = """
class Eclair {
cook() {
super.cook();
print "Pipe full of crème pâtissière.";
}
}
"""
captured = run_code_block(lines)

assert (
captured.err == "[line 4] Error at 'super' : "
"Can't use 'super' in a class with no superclass.\n"
)

assert captured.out == ""

def test_invalid_super_not_even_a_class(self, run_code_block):
lines = "super.notEvenInAClass();"
captured = run_code_block(lines)

assert (
captured.err
== "[line 1] Error at 'super' : Can't use 'super' outside of a class.\n"
)
assert captured.out == ""
Loading

0 comments on commit 5c72a19

Please sign in to comment.