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

feat: Implement Chapter 13, Inheritance #39

Merged
merged 5 commits into from
Feb 2, 2024
Merged
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
2 changes: 2 additions & 0 deletions baselines/chapter13/boston-creme-super.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fry until golden brown.
Pipe full of custard and coat with chocolate.
1 change: 1 addition & 0 deletions baselines/chapter13/boston-creme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fry until golden brown.
2 changes: 2 additions & 0 deletions baselines/chapter13/inheritance-loop.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Undefined variable 'B'.
[line 1]
1 change: 1 addition & 0 deletions baselines/chapter13/invalid-super-in-class.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[line 3] Error at 'super': Can't use 'super' in a class with no superclass.
1 change: 1 addition & 0 deletions baselines/chapter13/invalid-super-top-level.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[line 1] Error at 'super': Can't use 'super' outside of a class.
2 changes: 2 additions & 0 deletions baselines/chapter13/non-class-parent.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Superclass must be a class.
[line 3]
2 changes: 2 additions & 0 deletions baselines/chapter13/non-existent-method-on-super.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Undefined property bar.
[line 12]
1 change: 1 addition & 0 deletions baselines/chapter13/non-existent-method-on-super.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Parent.foo
1 change: 1 addition & 0 deletions baselines/chapter13/self-inheritance.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[line 1] Error at 'Oops': A class can't inherit from itself.
1 change: 1 addition & 0 deletions baselines/chapter13/super-without-parent.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[line 9] Error at 'super': Can't use 'super' in a class with no superclass.
1 change: 1 addition & 0 deletions baselines/chapter13/which-super.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A method
4 changes: 3 additions & 1 deletion cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"quickfix",
"Nystrom",
"unshadow",
"klass"
"klass",
"crème",
"pâtissière"
]
}
14 changes: 14 additions & 0 deletions examples/chapter13/boston-creme-super.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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();
9 changes: 9 additions & 0 deletions examples/chapter13/boston-creme.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Doughnut {
cook() {
print "Fry until golden brown.";
}
}

class BostonCream < Doughnut {}

BostonCream().cook();
13 changes: 13 additions & 0 deletions examples/chapter13/inheritance-loop.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class A < B {
foo() {
super.foo();
}
}
class B < A {
foo() {
super.foo();
}
}

var a = A();
a.foo();
6 changes: 6 additions & 0 deletions examples/chapter13/invalid-super-in-class.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Eclair {
cook() {
super.cook();
print "Pipe full of crème pâtissière.";
}
}
1 change: 1 addition & 0 deletions examples/chapter13/invalid-super-top-level.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
super.notEvenInAClass();
3 changes: 3 additions & 0 deletions examples/chapter13/non-class-parent.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fun Parent() {}

class Child < Parent {}
18 changes: 18 additions & 0 deletions examples/chapter13/non-existent-method-on-super.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Parent {
foo() {
print "Parent.foo";
}
}

class Child < Parent {
foo() {
super.foo();
}
bar() {
super.bar();
}
}

var c = Child();
c.foo();
c.bar();
1 change: 1 addition & 0 deletions examples/chapter13/self-inheritance.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Oops < Oops {}
14 changes: 14 additions & 0 deletions examples/chapter13/super-without-parent.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Parent {
foo() {
print "Parent.foo";
}
}

class Child {
foo() {
super.foo();
}
}

var c = Child();
c.foo();
19 changes: 19 additions & 0 deletions examples/chapter13/which-super.lox
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class A {
method() {
print "A method";
}
}

class B < A {
method() {
print "B method";
}

test() {
super.method();
}
}

class C < B {}

C().test();
1 change: 1 addition & 0 deletions src/ast-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const astPrinter: ExpressionVisitor<string> & StmtVisitor<string> = {
print: (stmt) => parenthesize("print", stmt.expression),
return: (stmt) => parenthesize("return", ...(stmt.value ? [stmt.value] : [])),
set: (expr) => parenthesize("set", expr.object, expr.name.lexeme, expr.value),
super: () => "super",
this: () => parenthesize("this"),
unary: (expr) => parenthesize(expr.operator.lexeme, expr.right),
"var-expr": (expr) => String(expr.name.literal),
Expand Down
8 changes: 8 additions & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface Return {
export interface Class {
kind: "class";
name: Token;
superclass: VarExpr | null;
methods: Func[];
}

Expand All @@ -123,6 +124,12 @@ export interface SetExpr {
value: Expr;
}

export interface Super {
kind: "super";
keyword: Token;
method: Token;
}

export type Expr =
| Assign
| Binary
Expand All @@ -132,6 +139,7 @@ export type Expr =
| Literal
| Logical
| SetExpr
| Super
| This
| Unary
| VarExpr;
Expand Down
6 changes: 3 additions & 3 deletions src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import { LoxValue } from "./lox-value.js";
import { Token } from "./token.js";

export class Environment {
#enclosing?: Environment;
#values: Map<string, LoxValue>;
enclosing?: Environment;

constructor(enclosing?: Environment) {
this.#values = new Map();
this.#enclosing = enclosing;
this.enclosing = enclosing;
}

ancestor(distance: number): Environment {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let env: Environment = this;
for (let i = 0; i < distance; i++) {
const enclosing = env.#enclosing;
const enclosing = env.enclosing;
if (!enclosing) {
throw new Error("Tried to go past last ancestor!");
}
Expand Down
43 changes: 42 additions & 1 deletion src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,23 @@ export class Interpreter {
}

class(stmt: Class): void {
let superclass = null;
if (stmt.superclass) {
superclass = this.evaluate(stmt.superclass);
if (!(superclass instanceof LoxClass)) {
throw new RuntimeError(
stmt.superclass.name,
"Superclass must be a class.",
);
}
}

this.#environment.define(stmt.name.lexeme, null);
if (superclass) {
this.#environment = new Environment(this.#environment);
this.#environment.define("super", superclass);
}

const methods = new Map<string, LoxFunction>();
for (const method of stmt.methods) {
const func = new LoxFunction(
Expand All @@ -171,7 +187,11 @@ export class Interpreter {
);
methods.set(method.name.lexeme, func);
}
const klass = new LoxClass(stmt.name.lexeme, methods);
const klass = new LoxClass(stmt.name.lexeme, superclass, methods);
if (superclass) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.#environment = this.#environment.enclosing!;
}
this.#environment.assign(stmt.name, klass);
}

Expand Down Expand Up @@ -231,6 +251,27 @@ export class Interpreter {
return value;
}

case "super": {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const distance = this.#locals.get(expr)!;
const superclass = this.#environment.getAt(
distance,
"super",
) as LoxClass;
const object = this.#environment.getAt(
distance - 1,
"this",
) as LoxInstance;
const method = superclass.findMethod(expr.method.lexeme);
if (!method) {
throw new RuntimeError(
expr.method,
`Undefined property ${expr.method.lexeme}.`,
);
}
return method.bindThis(object);
}

case "unary":
const right = this.evaluate(expr.right);
switch (expr.operator.type) {
Expand Down
13 changes: 10 additions & 3 deletions src/lox-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ import { LoxValue } from "./lox-value.js";
export class LoxClass extends LoxCallable {
#methods: Map<string, LoxFunction>;
name: string;
superclass: LoxClass | null;

constructor(name: string, methods: Map<string, LoxFunction>) {
constructor(
name: string,
superclass: LoxClass | null,
methods: Map<string, LoxFunction>,
) {
super();
this.name = name;
this.#methods = methods;
this.superclass = superclass;
}

arity(): number {
Expand All @@ -31,8 +37,9 @@ export class LoxClass extends LoxCallable {
return instance;
}

findMethod(name: string) {
return this.#methods.get(name);
findMethod(name: string): LoxFunction | undefined {
const meth = this.#methods.get(name);
return meth ?? this.superclass?.findMethod(name);
}

toString() {
Expand Down
32 changes: 26 additions & 6 deletions src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Grammar:
// program → declaration* EOF ;
// declaration → classDecl | funDecl | varDecl | statement ;
// classDecl → "class" IDENTIFIER "{" function* "}" ;
// classDecl → "class" IDENTIFIER ( "<" IDENTIFIER ) "{" function* "}" ;
// funDecl → "fun" function ;
// function → IDENTIFIER "(" parameters? ")" block ;
// varDecl → "var" IDENTIFIER ( "=" expression )? ";" ;
Expand All @@ -24,11 +24,19 @@
// unary → ( "!" | "-" ) unary | call ;
// call → primary ( "(" arguments? ")" | "." IDENTIFIER )* ;
// arguments → expression ( "," expression )* ;
// primary → NUMBER | STRING | "true" | "false" | "nil"
// primary → NUMBER | STRING | "true" | "false" | "nil" | "this"
// | "(" expression ")" ;
// | IDENTIFIER ;

import { Expr, Expression, Func, Print, Stmt, VarStmt } from "./ast.js";
// | IDENTIFIER | "super" "." IDENTIFIER;

import {
Expr,
Expression,
Func,
Print,
Stmt,
VarExpr,
VarStmt,
} from "./ast.js";
import { errorOnToken } from "./main.js";
import { Token } from "./token.js";
import { TokenType } from "./token-type.js";
Expand Down Expand Up @@ -121,13 +129,20 @@ export function parse(tokens: Token[]) {
// classDecl → "class" IDENTIFIER "{" function* "}" ;
const classDecl = (): Stmt => {
const name = consume("identifier", "Expect class name.");

let superclass: VarExpr | null = null;
if (match("<")) {
consume("identifier", "Expect superclass name.");
superclass = { kind: "var-expr", name: previous() };
}

consume("{", "Expect '{' before class body.");
const methods = [];
while (!check("}") && !isAtEnd()) {
methods.push(func("method"));
}
consume("}", "Expect '}' after class body.");
return { kind: "class", methods, name };
return { kind: "class", methods, name, superclass };
};

// funDecl → "fun" function ;
Expand Down Expand Up @@ -368,6 +383,11 @@ export function parse(tokens: Token[]) {
return { keyword: previous(), kind: "this" };
} else if (match("identifier")) {
return { kind: "var-expr", name: previous() };
} else if (match("super")) {
const keyword = previous();
consume(".", "Expect '.' after 'super'.");
const method = consume("identifier", "Expect superclass method name.");
return { keyword, kind: "super", method };
}
throw error(peek(), "Expect expression.");
};
Expand Down
Loading
Loading