From 0da5e49d80ddbfcf8b4303cb4f62d0dec3e36f7b Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 2 Feb 2024 17:21:15 -0500 Subject: [PATCH 1/5] inherit methods --- examples/chapter13/boston-creme.lox | 9 +++++++++ src/ast.ts | 1 + src/interpreter.ts | 13 ++++++++++++- src/lox-class.ts | 13 ++++++++++--- src/parser.ts | 21 ++++++++++++++++++--- src/resolver.ts | 10 ++++++++++ 6 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 examples/chapter13/boston-creme.lox diff --git a/examples/chapter13/boston-creme.lox b/examples/chapter13/boston-creme.lox new file mode 100644 index 0000000..916dbfb --- /dev/null +++ b/examples/chapter13/boston-creme.lox @@ -0,0 +1,9 @@ +class Doughnut { + cook() { + print "Fry until golden brown."; + } +} + +class BostonCream < Doughnut {} + +BostonCream().cook(); \ No newline at end of file diff --git a/src/ast.ts b/src/ast.ts index 1fec362..e28daa9 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -113,6 +113,7 @@ export interface Return { export interface Class { kind: "class"; name: Token; + superclass: VarExpr | null; methods: Func[]; } diff --git a/src/interpreter.ts b/src/interpreter.ts index 5415e6f..d1b11d8 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -161,6 +161,17 @@ 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); const methods = new Map(); for (const method of stmt.methods) { @@ -171,7 +182,7 @@ 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); this.#environment.assign(stmt.name, klass); } diff --git a/src/lox-class.ts b/src/lox-class.ts index 908a58c..b32a905 100644 --- a/src/lox-class.ts +++ b/src/lox-class.ts @@ -10,11 +10,17 @@ import { LoxValue } from "./lox-value.js"; export class LoxClass extends LoxCallable { #methods: Map; name: string; + superclass: LoxClass | null; - constructor(name: string, methods: Map) { + constructor( + name: string, + superclass: LoxClass | null, + methods: Map, + ) { super(); this.name = name; this.#methods = methods; + this.superclass = superclass; } arity(): number { @@ -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() { diff --git a/src/parser.ts b/src/parser.ts index bf42e15..abf1c6e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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 )? ";" ; @@ -28,7 +28,15 @@ // | "(" expression ")" ; // | IDENTIFIER ; -import { Expr, Expression, Func, Print, Stmt, VarStmt } from "./ast.js"; +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"; @@ -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 ; diff --git a/src/resolver.ts b/src/resolver.ts index c2b0530..7ebf9a7 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -92,6 +92,16 @@ export function makeResolver(interpreter: Interpreter) { currentClass = "class"; declare(stmt.name); define(stmt.name); + if (stmt.superclass && stmt.name.lexeme === stmt.superclass.name.lexeme) { + // What about circular inheritance? + errorOnToken( + stmt.superclass.name, + "A class can't inherit from itself.", + ); + } + if (stmt.superclass) { + resolveExpr(stmt.superclass); + } beginScope(); // TODO: write a peek() to enforce that -1 works. scopes.at(-1)?.set("this", true); From aa68670646882f76b89555ecde762d9bbda4d3eb Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 2 Feb 2024 17:42:16 -0500 Subject: [PATCH 2/5] implement super --- examples/chapter13/boston-creme-super.lox | 14 +++++++++ examples/chapter13/invalid-super-in-class.lox | 6 ++++ .../chapter13/invalid-super-top-level.lox | 1 + examples/chapter13/which-super.lox | 19 ++++++++++++ src/ast-printer.ts | 1 + src/ast.ts | 7 +++++ src/environment.ts | 6 ++-- src/interpreter.ts | 30 +++++++++++++++++++ src/parser.ts | 9 ++++-- src/resolver.ts | 23 +++++++++++++- 10 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 examples/chapter13/boston-creme-super.lox create mode 100644 examples/chapter13/invalid-super-in-class.lox create mode 100644 examples/chapter13/invalid-super-top-level.lox create mode 100644 examples/chapter13/which-super.lox diff --git a/examples/chapter13/boston-creme-super.lox b/examples/chapter13/boston-creme-super.lox new file mode 100644 index 0000000..79ad64b --- /dev/null +++ b/examples/chapter13/boston-creme-super.lox @@ -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(); diff --git a/examples/chapter13/invalid-super-in-class.lox b/examples/chapter13/invalid-super-in-class.lox new file mode 100644 index 0000000..76f7e5d --- /dev/null +++ b/examples/chapter13/invalid-super-in-class.lox @@ -0,0 +1,6 @@ +class Eclair { + cook() { + super.cook(); + print "Pipe full of crème pâtissière."; + } +} \ No newline at end of file diff --git a/examples/chapter13/invalid-super-top-level.lox b/examples/chapter13/invalid-super-top-level.lox new file mode 100644 index 0000000..284bedf --- /dev/null +++ b/examples/chapter13/invalid-super-top-level.lox @@ -0,0 +1 @@ +super.notEvenInAClass(); diff --git a/examples/chapter13/which-super.lox b/examples/chapter13/which-super.lox new file mode 100644 index 0000000..6dd7f1c --- /dev/null +++ b/examples/chapter13/which-super.lox @@ -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(); \ No newline at end of file diff --git a/src/ast-printer.ts b/src/ast-printer.ts index a55dd87..317a2bc 100644 --- a/src/ast-printer.ts +++ b/src/ast-printer.ts @@ -38,6 +38,7 @@ export const astPrinter: ExpressionVisitor & StmtVisitor = { 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), diff --git a/src/ast.ts b/src/ast.ts index e28daa9..610650e 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -124,6 +124,12 @@ export interface SetExpr { value: Expr; } +export interface Super { + kind: "super"; + keyword: Token; + method: Token; +} + export type Expr = | Assign | Binary @@ -133,6 +139,7 @@ export type Expr = | Literal | Logical | SetExpr + | Super | This | Unary | VarExpr; diff --git a/src/environment.ts b/src/environment.ts index a01b765..1872640 100644 --- a/src/environment.ts +++ b/src/environment.ts @@ -3,19 +3,19 @@ import { LoxValue } from "./lox-value.js"; import { Token } from "./token.js"; export class Environment { - #enclosing?: Environment; #values: Map; + 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!"); } diff --git a/src/interpreter.ts b/src/interpreter.ts index d1b11d8..535495d 100644 --- a/src/interpreter.ts +++ b/src/interpreter.ts @@ -173,6 +173,11 @@ export class Interpreter { } this.#environment.define(stmt.name.lexeme, null); + if (superclass) { + this.#environment = new Environment(this.#environment); + this.#environment.define("super", superclass); + } + const methods = new Map(); for (const method of stmt.methods) { const func = new LoxFunction( @@ -183,6 +188,10 @@ export class Interpreter { methods.set(method.name.lexeme, func); } 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); } @@ -242,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) { diff --git a/src/parser.ts b/src/parser.ts index abf1c6e..a3923c3 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -24,9 +24,9 @@ // 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 ; +// | IDENTIFIER | "super" "." IDENTIFIER; import { Expr, @@ -383,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."); }; diff --git a/src/resolver.ts b/src/resolver.ts index 7ebf9a7..6ffaef0 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -12,7 +12,7 @@ import { errorOnToken } from "./main.js"; import { Token } from "./token.js"; type FunctionType = "function" | "initializer" | "method" | "none"; -type ClassType = "class" | "none"; +type ClassType = "class" | "none" | "subclass"; export function makeResolver(interpreter: Interpreter) { const scopes: Map[] = []; @@ -100,8 +100,15 @@ export function makeResolver(interpreter: Interpreter) { ); } if (stmt.superclass) { + currentClass = "subclass"; resolveExpr(stmt.superclass); } + if (stmt.superclass) { + beginScope(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + scopes.at(-1)!.set("super", true); + } + beginScope(); // TODO: write a peek() to enforce that -1 works. scopes.at(-1)?.set("this", true); @@ -112,6 +119,9 @@ export function makeResolver(interpreter: Interpreter) { ); } endScope(); + if (stmt.superclass) { + endScope(); + } currentClass = encClass; }, expr(stmt) { @@ -163,6 +173,17 @@ export function makeResolver(interpreter: Interpreter) { resolveExpr(expr.value); resolveExpr(expr.object); }, + super(expr) { + if (currentClass === "none") { + errorOnToken(expr.keyword, "Can't use 'super' outside of a class."); + } else if (currentClass === "class") { + errorOnToken( + expr.keyword, + "Can't use 'super' in a class with no superclass.", + ); + } + resolveLocal(expr, expr.keyword); + }, this(expr) { if (currentClass === "none") { errorOnToken(expr.keyword, "Can't use 'this' outside of a class."); From 82935fd39d178ac4fed907840333c92ce7e48f9b Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 2 Feb 2024 17:43:12 -0500 Subject: [PATCH 3/5] new baselines --- baselines/chapter13/boston-creme-super.txt | 2 ++ baselines/chapter13/boston-creme.txt | 1 + baselines/chapter13/invalid-super-in-class.errors.txt | 1 + baselines/chapter13/invalid-super-top-level.errors.txt | 1 + baselines/chapter13/which-super.txt | 1 + 5 files changed, 6 insertions(+) create mode 100644 baselines/chapter13/boston-creme-super.txt create mode 100644 baselines/chapter13/boston-creme.txt create mode 100644 baselines/chapter13/invalid-super-in-class.errors.txt create mode 100644 baselines/chapter13/invalid-super-top-level.errors.txt create mode 100644 baselines/chapter13/which-super.txt diff --git a/baselines/chapter13/boston-creme-super.txt b/baselines/chapter13/boston-creme-super.txt new file mode 100644 index 0000000..1769cde --- /dev/null +++ b/baselines/chapter13/boston-creme-super.txt @@ -0,0 +1,2 @@ +Fry until golden brown. +Pipe full of custard and coat with chocolate. diff --git a/baselines/chapter13/boston-creme.txt b/baselines/chapter13/boston-creme.txt new file mode 100644 index 0000000..b2ad912 --- /dev/null +++ b/baselines/chapter13/boston-creme.txt @@ -0,0 +1 @@ +Fry until golden brown. diff --git a/baselines/chapter13/invalid-super-in-class.errors.txt b/baselines/chapter13/invalid-super-in-class.errors.txt new file mode 100644 index 0000000..5bc8cb7 --- /dev/null +++ b/baselines/chapter13/invalid-super-in-class.errors.txt @@ -0,0 +1 @@ +[line 3] Error at 'super': Can't use 'super' in a class with no superclass. diff --git a/baselines/chapter13/invalid-super-top-level.errors.txt b/baselines/chapter13/invalid-super-top-level.errors.txt new file mode 100644 index 0000000..1a00167 --- /dev/null +++ b/baselines/chapter13/invalid-super-top-level.errors.txt @@ -0,0 +1 @@ +[line 1] Error at 'super': Can't use 'super' outside of a class. diff --git a/baselines/chapter13/which-super.txt b/baselines/chapter13/which-super.txt new file mode 100644 index 0000000..5fbcdc5 --- /dev/null +++ b/baselines/chapter13/which-super.txt @@ -0,0 +1 @@ +A method From 276a0a7ba234d9065fc498db5571d51d675ac9df Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 2 Feb 2024 17:43:51 -0500 Subject: [PATCH 4/5] =?UTF-8?q?cr=C3=A8me=20p=C3=A2tissi=C3=A8re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cspell.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index 9cea18d..7e1b407 100644 --- a/cspell.json +++ b/cspell.json @@ -30,6 +30,8 @@ "quickfix", "Nystrom", "unshadow", - "klass" + "klass", + "crème", + "pâtissière" ] } From cafe719c1f3f19abf744634c16dd0e5a8323581d Mon Sep 17 00:00:00 2001 From: Dan Vanderkam Date: Fri, 2 Feb 2024 17:52:05 -0500 Subject: [PATCH 5/5] Test a few more error cases --- .../chapter13/inheritance-loop.errors.txt | 2 ++ .../chapter13/non-class-parent.errors.txt | 2 ++ .../non-existent-method-on-super.errors.txt | 2 ++ .../chapter13/non-existent-method-on-super.txt | 1 + .../chapter13/self-inheritance.errors.txt | 1 + .../chapter13/super-without-parent.errors.txt | 1 + examples/chapter13/inheritance-loop.lox | 13 +++++++++++++ examples/chapter13/non-class-parent.lox | 3 +++ .../chapter13/non-existent-method-on-super.lox | 18 ++++++++++++++++++ examples/chapter13/self-inheritance.lox | 1 + examples/chapter13/super-without-parent.lox | 14 ++++++++++++++ 11 files changed, 58 insertions(+) create mode 100644 baselines/chapter13/inheritance-loop.errors.txt create mode 100644 baselines/chapter13/non-class-parent.errors.txt create mode 100644 baselines/chapter13/non-existent-method-on-super.errors.txt create mode 100644 baselines/chapter13/non-existent-method-on-super.txt create mode 100644 baselines/chapter13/self-inheritance.errors.txt create mode 100644 baselines/chapter13/super-without-parent.errors.txt create mode 100644 examples/chapter13/inheritance-loop.lox create mode 100644 examples/chapter13/non-class-parent.lox create mode 100644 examples/chapter13/non-existent-method-on-super.lox create mode 100644 examples/chapter13/self-inheritance.lox create mode 100644 examples/chapter13/super-without-parent.lox diff --git a/baselines/chapter13/inheritance-loop.errors.txt b/baselines/chapter13/inheritance-loop.errors.txt new file mode 100644 index 0000000..be15b6a --- /dev/null +++ b/baselines/chapter13/inheritance-loop.errors.txt @@ -0,0 +1,2 @@ +Undefined variable 'B'. +[line 1] diff --git a/baselines/chapter13/non-class-parent.errors.txt b/baselines/chapter13/non-class-parent.errors.txt new file mode 100644 index 0000000..8fb60e1 --- /dev/null +++ b/baselines/chapter13/non-class-parent.errors.txt @@ -0,0 +1,2 @@ +Superclass must be a class. +[line 3] diff --git a/baselines/chapter13/non-existent-method-on-super.errors.txt b/baselines/chapter13/non-existent-method-on-super.errors.txt new file mode 100644 index 0000000..5c69ec2 --- /dev/null +++ b/baselines/chapter13/non-existent-method-on-super.errors.txt @@ -0,0 +1,2 @@ +Undefined property bar. +[line 12] diff --git a/baselines/chapter13/non-existent-method-on-super.txt b/baselines/chapter13/non-existent-method-on-super.txt new file mode 100644 index 0000000..28153a2 --- /dev/null +++ b/baselines/chapter13/non-existent-method-on-super.txt @@ -0,0 +1 @@ +Parent.foo diff --git a/baselines/chapter13/self-inheritance.errors.txt b/baselines/chapter13/self-inheritance.errors.txt new file mode 100644 index 0000000..24b5b0e --- /dev/null +++ b/baselines/chapter13/self-inheritance.errors.txt @@ -0,0 +1 @@ +[line 1] Error at 'Oops': A class can't inherit from itself. diff --git a/baselines/chapter13/super-without-parent.errors.txt b/baselines/chapter13/super-without-parent.errors.txt new file mode 100644 index 0000000..99c6b35 --- /dev/null +++ b/baselines/chapter13/super-without-parent.errors.txt @@ -0,0 +1 @@ +[line 9] Error at 'super': Can't use 'super' in a class with no superclass. diff --git a/examples/chapter13/inheritance-loop.lox b/examples/chapter13/inheritance-loop.lox new file mode 100644 index 0000000..9ad2ada --- /dev/null +++ b/examples/chapter13/inheritance-loop.lox @@ -0,0 +1,13 @@ +class A < B { + foo() { + super.foo(); + } +} +class B < A { + foo() { + super.foo(); + } +} + +var a = A(); +a.foo(); diff --git a/examples/chapter13/non-class-parent.lox b/examples/chapter13/non-class-parent.lox new file mode 100644 index 0000000..7baae92 --- /dev/null +++ b/examples/chapter13/non-class-parent.lox @@ -0,0 +1,3 @@ +fun Parent() {} + +class Child < Parent {} diff --git a/examples/chapter13/non-existent-method-on-super.lox b/examples/chapter13/non-existent-method-on-super.lox new file mode 100644 index 0000000..7c102bd --- /dev/null +++ b/examples/chapter13/non-existent-method-on-super.lox @@ -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(); diff --git a/examples/chapter13/self-inheritance.lox b/examples/chapter13/self-inheritance.lox new file mode 100644 index 0000000..d144a83 --- /dev/null +++ b/examples/chapter13/self-inheritance.lox @@ -0,0 +1 @@ +class Oops < Oops {} diff --git a/examples/chapter13/super-without-parent.lox b/examples/chapter13/super-without-parent.lox new file mode 100644 index 0000000..d6ec11b --- /dev/null +++ b/examples/chapter13/super-without-parent.lox @@ -0,0 +1,14 @@ +class Parent { + foo() { + print "Parent.foo"; + } +} + +class Child { + foo() { + super.foo(); + } +} + +var c = Child(); +c.foo();