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

Commit 5c72a19

Browse files
Merge pull request #54 from RoelAdriaans/feature/add-chapter-13-inheritance
2 parents 59510c5 + 10173f1 commit 5c72a19

11 files changed

+244
-8
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.0.10] - 2020-11-01
11+
12+
### Added
13+
14+
- Completing chapter 13
15+
- Inheritance works, circular inheritance is catched, calling super
16+
- Added some warnings for super outside a class
17+
1018
## [0.0.9] - 2020-10-25
1119

1220
### Added

src/yaplox/ast_printer.py

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
Literal,
1010
Logical,
1111
Set,
12+
Super,
1213
This,
1314
Unary,
1415
Variable,
@@ -42,6 +43,9 @@ def visit_logical_expr(self, expr: Logical):
4243
def visit_set_expr(self, expr: Set):
4344
pass
4445

46+
def visit_super_expr(self, expr: Super):
47+
pass
48+
4549
def visit_this_expr(self, expr: This):
4650
pass
4751

src/yaplox/class_type.py

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
class ClassType(enum.Enum):
55
NONE = enum.auto()
66
CLASS = enum.auto()
7+
SUBCLASS = enum.auto()

src/yaplox/expr.py

+14
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def visit_logical_expr(self, expr: Logical):
4444
def visit_set_expr(self, expr: Set):
4545
raise NotImplementedError
4646

47+
@abstractmethod
48+
def visit_super_expr(self, expr: Super):
49+
raise NotImplementedError
50+
4751
@abstractmethod
4852
def visit_this_expr(self, expr: This):
4953
raise NotImplementedError
@@ -145,6 +149,16 @@ def accept(self, visitor: ExprVisitor):
145149
return visitor.visit_set_expr(self)
146150

147151

152+
class Super(Expr):
153+
def __init__(self, keyword: Token, method: Token):
154+
self.keyword = keyword
155+
self.method = method
156+
157+
def accept(self, visitor: ExprVisitor):
158+
""" Create a accept method that calls the visitor. """
159+
return visitor.visit_super_expr(self)
160+
161+
148162
class This(Expr):
149163
def __init__(self, keyword: Token):
150164
self.keyword = keyword

src/yaplox/interpreter.py

+35-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Literal,
1616
Logical,
1717
Set,
18+
Super,
1819
This,
1920
Unary,
2021
Variable,
@@ -199,6 +200,21 @@ def visit_set_expr(self, expr: Set):
199200
obj.set(expr.name, value)
200201
return value
201202

203+
def visit_super_expr(self, expr: Super):
204+
distance = self.locals[expr]
205+
superclass: YaploxClass = self.environment.get_at(
206+
distance=distance, name="super"
207+
)
208+
obj = self.environment.get_at(distance=distance - 1, name="this")
209+
method = superclass.find_method(expr.method.lexeme)
210+
211+
# Check that we have a super method
212+
if method is None:
213+
raise YaploxRuntimeError(
214+
expr.method, f"Undefined property '{expr.method.lexeme}'."
215+
)
216+
return method.bind(obj)
217+
202218
def visit_this_expr(self, expr: This):
203219
return self._look_up_variable(expr.keyword, expr)
204220

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

260276
# statement stuff
261277
def visit_class_stmt(self, stmt: Class):
278+
superclass = None
279+
if stmt.superclass is not None:
280+
superclass = self._evaluate(stmt.superclass)
281+
if not isinstance(superclass, YaploxClass):
282+
raise YaploxRuntimeError(
283+
stmt.superclass.name, "Superclass must be a class."
284+
)
285+
262286
self.environment.define(stmt.name.lexeme, None)
263287

288+
if stmt.superclass is not None:
289+
self.environment = Environment(self.environment)
290+
self.environment.define("super", superclass)
291+
264292
methods: Dict[str, YaploxFunction] = {}
265293

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

272-
klass = YaploxClass(stmt.name.lexeme, methods)
300+
klass = YaploxClass(
301+
name=stmt.name.lexeme, superclass=superclass, methods=methods
302+
)
303+
304+
if stmt.superclass is not None:
305+
self.environment = self.environment.enclosing # type: ignore
306+
273307
self.environment.assign(stmt.name, klass)
274308

275309
def visit_expression_stmt(self, stmt: Expression) -> None:

src/yaplox/parser.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Literal,
1111
Logical,
1212
Set,
13+
Super,
1314
This,
1415
Unary,
1516
Variable,
@@ -71,14 +72,20 @@ def _declaration(self) -> Optional[Stmt]:
7172

7273
def _class_declaration(self) -> Stmt:
7374
name = self._consume(TokenType.IDENTIFIER, "Expect class name.")
75+
76+
superclass = None
77+
if self._match(TokenType.LESS):
78+
self._consume(TokenType.IDENTIFIER, "Expect superclass name.")
79+
superclass = Variable(self._previous())
80+
7481
self._consume(TokenType.LEFT_BRACE, "Expect '{' before class body.")
7582

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

8087
self._consume(TokenType.RIGHT_BRACE, "Expect '}' after class body.")
81-
return Class(name=name, methods=methods)
88+
return Class(name=name, superclass=superclass, methods=methods)
8289

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

375+
if self._match(TokenType.SUPER):
376+
keyword = self._previous()
377+
self._consume(TokenType.DOT, "Expect '.' after 'super'.")
378+
method = self._consume(
379+
TokenType.IDENTIFIER, "Expect superclass method name."
380+
)
381+
382+
return Super(keyword, method)
383+
368384
if self._match(TokenType.THIS):
369385
return This(self._previous())
370386

src/yaplox/resolver.py

+25
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Literal,
1616
Logical,
1717
Set,
18+
Super,
1819
This,
1920
Unary,
2021
Variable,
@@ -154,6 +155,16 @@ def visit_set_expr(self, expr: Set):
154155
self._resolve_expression(expr.value)
155156
self._resolve_expression(expr.obj)
156157

158+
def visit_super_expr(self, expr: Super):
159+
if self.current_class == ClassType.NONE:
160+
self.on_error(expr.keyword, "Can't use 'super' outside of a class.")
161+
elif self.current_class != ClassType.SUBCLASS:
162+
self.on_error(
163+
expr.keyword, "Can't use 'super' in a class with no superclass."
164+
)
165+
166+
self._resolve_local(expr, expr.keyword)
167+
157168
def visit_unary_expr(self, expr: Unary):
158169
self._resolve_expression(expr.right)
159170

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

190+
if stmt.superclass and stmt.name.lexeme == stmt.superclass.name.lexeme:
191+
self.on_error(stmt.superclass.name, "A class can't inherit from itself.")
192+
193+
if stmt.superclass is not None:
194+
self.current_class = ClassType.SUBCLASS
195+
self._resolve_expression(stmt.superclass)
196+
197+
if stmt.superclass is not None:
198+
self._begin_scope()
199+
self.scopes[-1]["super"] = True
200+
179201
self._begin_scope()
180202
self.scopes[-1]["this"] = True
181203

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

189211
self._end_scope()
190212

213+
if stmt.superclass is not None:
214+
self._end_scope()
215+
191216
self.current_class = enclosing_class
192217

193218
def visit_expression_stmt(self, stmt: Expression):

src/yaplox/stmt.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from abc import ABC, abstractmethod
77
from typing import List, Optional
88

9-
from yaplox.expr import Expr
9+
from yaplox.expr import Expr, Variable
1010
from yaplox.token import Token
1111

1212

@@ -66,8 +66,11 @@ def accept(self, visitor: StmtVisitor):
6666

6767

6868
class Class(Stmt):
69-
def __init__(self, name: Token, methods: List[Function]):
69+
def __init__(
70+
self, name: Token, superclass: Optional[Variable], methods: List[Function]
71+
):
7072
self.name = name
73+
self.superclass = superclass
7174
self.methods = methods
7275

7376
def accept(self, visitor: StmtVisitor):

src/yaplox/yaplox_class.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,14 @@ def arity(self) -> int:
2727
else:
2828
return 0
2929

30-
def __init__(self, name: str, methods: Dict[str, YaploxFunction]):
30+
def __init__(
31+
self,
32+
name: str,
33+
superclass: Optional[YaploxClass],
34+
methods: Dict[str, YaploxFunction],
35+
):
3136
self.name = name
37+
self.superclass = superclass
3238
self.methods = methods
3339

3440
def __repr__(self):
@@ -38,4 +44,9 @@ def find_method(self, name: str) -> Optional[YaploxFunction]:
3844
try:
3945
return self.methods[name]
4046
except KeyError:
41-
return None
47+
pass
48+
49+
if self.superclass:
50+
return self.superclass.find_method(name)
51+
52+
return None

tests/test_class_inheritance.py

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
class TestClassesInheretance:
2+
def test_class_circular(self, run_code_block):
3+
line = "class Oops < Oops {}"
4+
5+
assert (
6+
run_code_block(line).err
7+
== "[line 1] Error at 'Oops' : A class can't inherit from itself.\n"
8+
)
9+
assert run_code_block(line).out == ""
10+
11+
def test_class_not_a_class(self, run_code_block):
12+
lines = """
13+
var NotAClass = "I am totally not a class";
14+
class Subclass < NotAClass {} // ?!
15+
"""
16+
assert (
17+
run_code_block(lines).err == "Superclass must be a class. in line [line3]\n"
18+
)
19+
assert run_code_block(lines).out == ""
20+
21+
def test_class_inheritance(self, run_code_block):
22+
lines = """
23+
class Doughnut {
24+
cook() {
25+
print "Fry until golden brown.";
26+
}
27+
}
28+
29+
class BostonCream < Doughnut {}
30+
31+
BostonCream().cook();
32+
"""
33+
34+
assert run_code_block(lines).err == ""
35+
assert run_code_block(lines).out == "Fry until golden brown.\n"
36+
37+
def test_class_call_super(self, run_code_block):
38+
lines = """
39+
class Doughnut {
40+
cook() {
41+
print "Fry until golden brown.";
42+
}
43+
}
44+
45+
class BostonCream < Doughnut {
46+
cook() {
47+
super.cook();
48+
print "Pipe full of custard and coat with chocolate.";
49+
}
50+
}
51+
52+
BostonCream().cook();
53+
// Prints:
54+
// Fry until golden brown.
55+
// Pipe full of custard and coat with chocolate."""
56+
57+
captured = run_code_block(lines)
58+
59+
assert captured.err == ""
60+
assert (
61+
captured.out == "Fry until golden brown.\n"
62+
"Pipe full of custard and coat with chocolate.\n"
63+
)
64+
65+
def test_multiple_inherit(self, run_code_block):
66+
lines = """
67+
class A {
68+
method() {
69+
print "A method";
70+
}
71+
}
72+
73+
class B < A {
74+
method() {
75+
print "B method";
76+
}
77+
78+
test() {
79+
super.method();
80+
}
81+
}
82+
83+
class C < B {}
84+
85+
C().test();
86+
"""
87+
captured = run_code_block(lines)
88+
89+
assert captured.err == ""
90+
assert captured.out == "A method\n"
91+
92+
def test_invalid_super(self, run_code_block):
93+
lines = """
94+
class Eclair {
95+
cook() {
96+
super.cook();
97+
print "Pipe full of crème pâtissière.";
98+
}
99+
}
100+
"""
101+
captured = run_code_block(lines)
102+
103+
assert (
104+
captured.err == "[line 4] Error at 'super' : "
105+
"Can't use 'super' in a class with no superclass.\n"
106+
)
107+
108+
assert captured.out == ""
109+
110+
def test_invalid_super_not_even_a_class(self, run_code_block):
111+
lines = "super.notEvenInAClass();"
112+
captured = run_code_block(lines)
113+
114+
assert (
115+
captured.err
116+
== "[line 1] Error at 'super' : Can't use 'super' outside of a class.\n"
117+
)
118+
assert captured.out == ""

0 commit comments

Comments
 (0)