Skip to content

Commit

Permalink
Merge pull request #10 from quephird/implement_inheritance_post_stati…
Browse files Browse the repository at this point in the history
…c_methods

Implement inheritance
  • Loading branch information
quephird committed Mar 12, 2024
2 parents 57ef297 + 043ceee commit 0485f0e
Show file tree
Hide file tree
Showing 17 changed files with 461 additions and 28 deletions.
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])
}

0 comments on commit 0485f0e

Please sign in to comment.