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

Implement inheritance #10

Merged
merged 6 commits into from Mar 12, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Expand Up @@ -20,6 +20,10 @@ So far, the following have been implemented in `slox`:
- Lambda expressions
- Class declaration and instantiation
- Instance properties and methods
- Referencing the scoped instance via `this`
- Class-level properties and methods
- Single inheritance
- Invoking superclass methods via `super`

# Design

Expand Down Expand Up @@ -60,7 +64,7 @@ Instead of maintaining set of native functions in `Interpreter`'s constructor, t

# Unit testing

This repository contains a fairly comprehensive suite of unit tests that exercise the scanner, parser, resolver, and interpreter; to run them, hit ⌘-U.
This repository contains a fairly comprehensive suite of unit tests that exercise the scanner, parser, resolver, and interpreter; to run them, hit ⌘-U from within Xcode.

# Relevant links

Expand Down
2 changes: 1 addition & 1 deletion slox/Environment.swift
Expand Up @@ -6,7 +6,7 @@
//

class Environment: Equatable {
private var enclosingEnvironment: Environment?
var enclosingEnvironment: Environment?
private var values: [String: LoxValue] = [:]

init(enclosingEnvironment: Environment? = nil) {
Expand Down
1 change: 1 addition & 0 deletions slox/Expression.swift
Expand Up @@ -18,4 +18,5 @@ indirect enum Expression: Equatable {
case get(Expression, Token)
case set(Expression, Token, Expression)
case this(Token)
case `super`(Token, Token)
}
48 changes: 45 additions & 3 deletions slox/Interpreter.swift
Expand Up @@ -56,8 +56,11 @@ class Interpreter {
environment: Environment(enclosingEnvironment: environment))
case .while(let expr, let stmt):
try handleWhileStatement(expr: expr, stmt: stmt)
case .class(let nameToken, let methods, let staticMethods):
try handleClassDeclaration(nameToken: nameToken, methods: methods, staticMethods: staticMethods)
case .class(let nameToken, let superclassExpr, let methods, let staticMethods):
try handleClassDeclaration(nameToken: nameToken,
superclassExpr: superclassExpr,
methods: methods,
staticMethods: staticMethods)
case .function(let name, let lambda):
try handleFunctionDeclaration(name: name, lambda: lambda)
case .return(let returnToken, let expr):
Expand All @@ -81,13 +84,25 @@ class Interpreter {
}

private func handleClassDeclaration(nameToken: Token,
superclassExpr: ResolvedExpression?,
methods: [ResolvedStatement],
staticMethods: [ResolvedStatement]) throws {
// NOTA BENE: We temporarily set the initial value associated with
// the class name to `.nil` so that, according to the book,
// "allows references to the class inside its own methods".
environment.define(name: nameToken.lexeme, value: .nil)

let superclass = try superclassExpr.map { superclassExpr in
guard case .instance(let superclass as LoxClass) = try evaluate(expr: superclassExpr) else {
throw RuntimeError.superclassMustBeAClass
}

environment = Environment(enclosingEnvironment: environment);
environment.define(name: "super", value: .instance(superclass));

return superclass
}

var methodImpls: [String: UserDefinedFunction] = [:]
for method in methods {
guard case .function(let nameToken, let lambdaExpr) = method else {
Expand Down Expand Up @@ -125,13 +140,21 @@ class Interpreter {
staticMethodImpls[nameToken.lexeme] = staticMethodImpl
}

let newClass = LoxClass(name: nameToken.lexeme, methods: methodImpls)
let newClass = LoxClass(name: nameToken.lexeme,
superclass: superclass,
methods: methodImpls)
if !staticMethodImpls.isEmpty {
// NOTA BENE: This assigns the static methods to the metaclass,
// which is lazily created in `LoxInstance`
newClass.klass.methods = staticMethodImpls
}

// Note that we can't accomplish this via a defer block because we need
// to assign the class to the _outermost_ environment, not the enclosing one.
if superclassExpr != nil {
environment = environment.enclosingEnvironment!
}

try environment.assignAtDepth(name: nameToken.lexeme, value: .instance(newClass), depth: 0)
}

Expand Down Expand Up @@ -217,6 +240,8 @@ class Interpreter {
return try handleThis(thisToken: thisToken, depth: depth)
case .lambda(let params, let statements):
return try handleLambdaExpression(params: params, statements: statements)
case .super(let superToken, let methodToken, let depth):
return try handleSuperExpression(superToken: superToken, methodToken: methodToken, depth: depth)
}
}

Expand Down Expand Up @@ -386,6 +411,23 @@ class Interpreter {
return .userDefinedFunction(function)
}

private func handleSuperExpression(superToken: Token, methodToken: Token, depth: Int) throws -> LoxValue {
guard case .instance(let superclass as LoxClass) = try environment.getValueAtDepth(name: "super", depth: depth) else {
throw RuntimeError.superclassMustBeAClass
}

guard case .instance(let thisInstance) = try environment.getValueAtDepth(name: "this", depth: depth - 1) else {
throw RuntimeError.notAnInstance
}

if let method = superclass.findMethod(name: methodToken.lexeme) {
return .userDefinedFunction(method.bind(instance: thisInstance))
}

throw RuntimeError.undefinedProperty(methodToken.lexeme)
}

// Utility functions below
private func isEqual(leftValue: LoxValue, rightValue: LoxValue) -> Bool {
switch (leftValue, rightValue) {
case (.nil, .nil):
Expand Down
12 changes: 11 additions & 1 deletion slox/LoxClass.swift
Expand Up @@ -7,6 +7,7 @@

class LoxClass: LoxInstance, LoxCallable {
var name: String
var superclass: LoxClass?
var arity: Int {
if let initializer = methods["init"] {
return initializer.params.count
Expand All @@ -16,8 +17,9 @@ class LoxClass: LoxInstance, LoxCallable {
}
var methods: [String: UserDefinedFunction]

init(name: String, methods: [String: UserDefinedFunction]) {
init(name: String, superclass: LoxClass?, methods: [String: UserDefinedFunction]) {
self.name = name
self.superclass = superclass
self.methods = methods

super.init(klass: nil)
Expand All @@ -27,6 +29,14 @@ class LoxClass: LoxInstance, LoxCallable {
return lhs === rhs
}

func findMethod(name: String) -> UserDefinedFunction? {
if let method = methods[name] {
return method
}

return superclass?.findMethod(name: name)
}

func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue {
let newInstance = LoxInstance(klass: self)

Expand Down
4 changes: 2 additions & 2 deletions slox/LoxInstance.swift
Expand Up @@ -19,7 +19,7 @@ class LoxInstance: Equatable {
if _klass == nil {
// Only metaclasses should ever have a `nil` value for `_klass`
let selfClass = self as! LoxClass
_klass = LoxClass(name: "\(selfClass.name) metaclass", methods: [:])
_klass = LoxClass(name: "\(selfClass.name) metaclass", superclass: nil, methods: [:])
}
return _klass!
}
Expand All @@ -37,7 +37,7 @@ class LoxInstance: Equatable {
return propertyValue
}

if let method = klass.methods[propertyName] {
if let method = klass.findMethod(name: propertyName) {
let boundMethod = method.bind(instance: self)
return .userDefinedFunction(boundMethod)
}
Expand Down
9 changes: 9 additions & 0 deletions slox/ParseError.swift
Expand Up @@ -29,6 +29,9 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError {
case missingOpenBraceBeforeFunctionBody(Token)
case missingCloseParenAfterArguments(Token)
case missingIdentifierAfterDot(Token)
case missingSuperclassName(Token)
case missingDotAfterSuper(Token)
case expectedSuperclassMethodName(Token)

var description: String {
switch self {
Expand Down Expand Up @@ -74,6 +77,12 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError {
return "[Line \(token.line)] Error: expected right parenthesis after arguments"
case .missingIdentifierAfterDot(let token):
return "[Line \(token.line)] Error: expected identifer after dot"
case .missingSuperclassName(let token):
return "[Line \(token.line)] Error: expected superclass name"
case .missingDotAfterSuper(let token):
return "[Line \(token.line)] Error: expected dot after super"
case .expectedSuperclassMethodName(let token):
return "[Line \(token.line)] Error: expected superclass method name"
}
}
}
33 changes: 30 additions & 3 deletions slox/Parser.swift
Expand Up @@ -36,7 +36,8 @@ struct Parser {
// | funDecl
// | varDecl
// | statement ;
// classDecl → "class" IDENTIFIER "{" function* "}" ;
// classDecl → "class" IDENTIFIER ( "<" IDENTIFIER )?
// "{" function* "}" ;
// funDecl → "fun" function ;
// function → IDENTIFIER "(" parameters? ")" block ;
// varDecl → "var" IDENTIFIER ( "=" expression )? ";" ;
Expand Down Expand Up @@ -85,6 +86,16 @@ struct Parser {
let className = currentToken
advanceCursor()

var superclassExpr: Expression? = nil
if currentTokenMatchesAny(types: [.less]) {
guard case .identifier = currentToken.type else {
throw ParseError.missingSuperclassName(currentToken)
}

superclassExpr = .variable(currentToken)
advanceCursor()
}

if !currentTokenMatchesAny(types: [.leftBrace]) {
throw ParseError.missingOpenBraceBeforeClassBody(currentToken)
}
Expand All @@ -105,7 +116,7 @@ struct Parser {
}

if currentTokenMatchesAny(types: [.rightBrace]) {
return .class(className, methodStatements, staticMethodStatements)
return .class(className, superclassExpr, methodStatements, staticMethodStatements)
}

throw ParseError.missingClosingBrace(previousToken)
Expand Down Expand Up @@ -340,7 +351,8 @@ struct Parser {
// | "(" expression ")"
// | "this"
// | IDENTIFIER
// | lambda ;
// | lambda
// | "super" "." IDENTIFIER ;
// lambda → "fun" "(" parameters? ")" block ;
//
mutating private func parseExpression() throws -> Expression {
Expand Down Expand Up @@ -508,6 +520,21 @@ struct Parser {
throw ParseError.missingClosingParenthesis(currentToken)
}

if currentTokenMatchesAny(types: [.super]) {
let superToken = previousToken
if !currentTokenMatchesAny(types: [.dot]) {
throw ParseError.missingDotAfterSuper(currentToken)
}

guard case .identifier = currentToken.type else {
throw ParseError.expectedSuperclassMethodName(currentToken)
}
let methodToken = currentToken
advanceCursor()

return .super(superToken, methodToken)
}

if currentTokenMatchesAny(types: [.this]) {
return .this(previousToken)
}
Expand Down
1 change: 1 addition & 0 deletions slox/ResolvedExpression.swift
Expand Up @@ -18,4 +18,5 @@ indirect enum ResolvedExpression: Equatable {
case get(ResolvedExpression, Token)
case set(ResolvedExpression, Token, ResolvedExpression)
case this(Token, Int)
case `super`(Token, Token, Int)
}
2 changes: 1 addition & 1 deletion slox/ResolvedStatement.swift
Expand Up @@ -14,5 +14,5 @@ indirect enum ResolvedStatement: Equatable {
case `while`(ResolvedExpression, ResolvedStatement)
case function(Token, ResolvedExpression)
case `return`(Token, ResolvedExpression?)
case `class`(Token, [ResolvedStatement], [ResolvedStatement])
case `class`(Token, ResolvedExpression?, [ResolvedStatement], [ResolvedStatement])
}