Skip to content

Commit

Permalink
Merge pull request #9 from quephird/implement_class_level_methods
Browse files Browse the repository at this point in the history
Implement class level methods
  • Loading branch information
quephird committed Mar 10, 2024
2 parents a27d135 + 2e8ce09 commit 57ef297
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 44 deletions.
60 changes: 42 additions & 18 deletions slox/Interpreter.swift
Expand Up @@ -56,8 +56,8 @@ class Interpreter {
environment: Environment(enclosingEnvironment: environment))
case .while(let expr, let stmt):
try handleWhileStatement(expr: expr, stmt: stmt)
case .class(let nameToken, let body):
try handleClassDeclaration(nameToken: nameToken, body: body)
case .class(let nameToken, let methods, let staticMethods):
try handleClassDeclaration(nameToken: nameToken, methods: methods, staticMethods: staticMethods)
case .function(let name, let lambda):
try handleFunctionDeclaration(name: name, lambda: lambda)
case .return(let returnToken, let expr):
Expand All @@ -80,14 +80,16 @@ class Interpreter {
print(literal)
}

private func handleClassDeclaration(nameToken: Token, body: [ResolvedStatement]) throws {
private func handleClassDeclaration(nameToken: Token,
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)

var methods: [String: UserDefinedFunction] = [:]
for method in body {
var methodImpls: [String: UserDefinedFunction] = [:]
for method in methods {
guard case .function(let nameToken, let lambdaExpr) = method else {
throw RuntimeError.notAFunctionDeclaration
}
Expand All @@ -97,16 +99,40 @@ class Interpreter {
}

let isInitializer = nameToken.lexeme == "init"
let method = UserDefinedFunction(name: nameToken.lexeme,
params: paramTokens,
enclosingEnvironment: environment,
body: methodBody,
isInitializer: isInitializer)
methods[nameToken.lexeme] = method
let methodImpl = UserDefinedFunction(name: nameToken.lexeme,
params: paramTokens,
enclosingEnvironment: environment,
body: methodBody,
isInitializer: isInitializer)
methodImpls[nameToken.lexeme] = methodImpl
}

let newClass = LoxClass(name: nameToken.lexeme, methods: methods)
try environment.assignAtDepth(name: nameToken.lexeme, value: .class(newClass), depth: 0)
var staticMethodImpls: [String: UserDefinedFunction] = [:]
for staticMethod in staticMethods {
guard case .function(let nameToken, let lambdaExpr) = staticMethod else {
throw RuntimeError.notAFunctionDeclaration
}

guard case .lambda(let paramTokens, let methodBody) = lambdaExpr else {
throw RuntimeError.notALambda
}

let staticMethodImpl = UserDefinedFunction(name: nameToken.lexeme,
params: paramTokens,
enclosingEnvironment: environment,
body: methodBody,
isInitializer: false)
staticMethodImpls[nameToken.lexeme] = staticMethodImpl
}

let newClass = LoxClass(name: nameToken.lexeme, 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
}

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

private func handleFunctionDeclaration(name: Token, lambda: ResolvedExpression) throws {
Expand Down Expand Up @@ -303,7 +329,7 @@ class Interpreter {
userDefinedFunction
case .nativeFunction(let nativeFunction):
nativeFunction
case .class(let klass):
case .instance(let klass as LoxClass):
klass
default:
throw RuntimeError.notACallableObject
Expand All @@ -324,8 +350,7 @@ class Interpreter {

private func handleGetExpression(instanceExpr: ResolvedExpression,
propertyNameToken: Token) throws -> LoxValue {
let instanceValue = try evaluate(expr: instanceExpr)
guard case .instance(let instance) = instanceValue else {
guard case .instance(let instance) = try evaluate(expr: instanceExpr) else {
throw RuntimeError.onlyInstancesHaveProperties
}

Expand All @@ -335,8 +360,7 @@ class Interpreter {
private func handleSetExpression(instanceExpr: ResolvedExpression,
propertyNameToken: Token,
valueExpr: ResolvedExpression) throws -> LoxValue {
let instanceValue = try evaluate(expr: instanceExpr)
guard case .instance(let instance) = instanceValue else {
guard case .instance(let instance) = try evaluate(expr: instanceExpr) else {
throw RuntimeError.onlyInstancesHaveProperties
}

Expand Down
4 changes: 3 additions & 1 deletion slox/LoxClass.swift
Expand Up @@ -5,7 +5,7 @@
// Created by Danielle Kefford on 3/4/24.
//

class LoxClass: LoxCallable, Equatable {
class LoxClass: LoxInstance, LoxCallable {
var name: String
var arity: Int {
if let initializer = methods["init"] {
Expand All @@ -19,6 +19,8 @@ class LoxClass: LoxCallable, Equatable {
init(name: String, methods: [String: UserDefinedFunction]) {
self.name = name
self.methods = methods

super.init(klass: nil)
}

static func == (lhs: LoxClass, rhs: LoxClass) -> Bool {
Expand Down
25 changes: 22 additions & 3 deletions slox/LoxInstance.swift
Expand Up @@ -6,11 +6,30 @@
//

class LoxInstance: Equatable {
var klass: LoxClass
// `klass` is what is used in the interpreter when we need
// to know the class of a particular instance. Every Lox
// instance, including Lox classes, need to have a non-nil
// parent class, and so we lazily construct one if the
// instance/class was initialized with a nil one, which will
// only ever happen for the class of a so-called metaclass.
// We do it lazily so we can avoid instantiating an infinite
// tower of parent instances of LoxClass.
private var _klass: LoxClass?
var klass: LoxClass {
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: [:])
}
return _klass!
}
var properties: [String: LoxValue] = [:]

init(klass: LoxClass) {
self.klass = klass
/// - Parameter klass: The class this instance belongs to.
/// Use `nil` if this instance *is* a class; the `klass` property
/// will then instantiate a metaclass for it on demand.
init(klass: LoxClass?) {
self._klass = klass
}

func get(propertyName: String) throws -> LoxValue {
Expand Down
3 changes: 1 addition & 2 deletions slox/LoxValue.swift
Expand Up @@ -12,7 +12,6 @@ enum LoxValue: CustomStringConvertible, Equatable {
case `nil`
case userDefinedFunction(UserDefinedFunction)
case nativeFunction(NativeFunction)
case `class`(LoxClass)
case instance(LoxInstance)

var description: String {
Expand All @@ -29,7 +28,7 @@ enum LoxValue: CustomStringConvertible, Equatable {
return "<function: \(function.name)>"
case .nativeFunction(let function):
return "<function: \(function)>"
case .class(let klass):
case .instance(let klass as LoxClass):
return "<class: \(klass.name)>"
case .instance(let instance):
return "<instance: \(instance.klass.name)>"
Expand Down
12 changes: 9 additions & 3 deletions slox/Parser.swift
Expand Up @@ -90,16 +90,22 @@ struct Parser {
}

var methodStatements: [Statement] = []
var staticMethodStatements: [Statement] = []
while currentToken.type != .rightBrace && currentToken.type != .eof {
// Note that we don't look for/consume a `fun` token before
// calling `parseFunctionDeclaration()`. That's a deliberate
// design decision by the original author.
let methodStatement = try parseFunctionDeclaration()
methodStatements.append(methodStatement)
if currentTokenMatchesAny(types: [.class]) {
let staticMethodStatement = try parseFunctionDeclaration()
staticMethodStatements.append(staticMethodStatement)
} else {
let methodStatement = try parseFunctionDeclaration()
methodStatements.append(methodStatement)
}
}

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

throw ParseError.missingClosingBrace(previousToken)
Expand Down
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])
case `class`(Token, [ResolvedStatement], [ResolvedStatement])
}
27 changes: 22 additions & 5 deletions slox/Resolver.swift
Expand Up @@ -38,8 +38,8 @@ struct Resolver {
return try handleBlock(statements: statements)
case .variableDeclaration(let nameToken, let initializeExpr):
return try handleVariableDeclaration(nameToken: nameToken, initializeExpr: initializeExpr)
case .class(let nameToken, let body):
return try handleClassDeclaration(nameToken: nameToken, body: body)
case .class(let nameToken, let methods, let staticMethods):
return try handleClassDeclaration(nameToken: nameToken, methods: methods, staticMethods: staticMethods)
case .function(let nameToken, let lambdaExpr):
return try handleFunctionDeclaration(nameToken: nameToken,
lambdaExpr: lambdaExpr,
Expand Down Expand Up @@ -80,7 +80,9 @@ struct Resolver {
return .variableDeclaration(nameToken, resolvedInitializerExpr)
}

mutating private func handleClassDeclaration(nameToken: Token, body: [Statement]) throws -> ResolvedStatement {
mutating private func handleClassDeclaration(nameToken: Token,
methods: [Statement],
staticMethods: [Statement]) throws -> ResolvedStatement {
let previousClassType = currentClassType
currentClassType = .class

Expand All @@ -95,7 +97,7 @@ struct Resolver {
currentClassType = previousClassType
}

let resolvedBody = try body.map { method in
let resolvedMethods = try methods.map { method in
guard case .function(let nameToken, let lambdaExpr) = method else {
throw ResolverError.notAFunction
}
Expand All @@ -111,7 +113,22 @@ struct Resolver {
functionType: functionType)
}

return .class(nameToken, resolvedBody)
let resolvedStaticMethods = try staticMethods.map { method in
guard case .function(let nameToken, let lambdaExpr) = method else {
throw ResolverError.notAFunction
}

if nameToken.lexeme == "init" {
throw ResolverError.staticInitsNotAllowed
}

return try handleFunctionDeclaration(
nameToken: nameToken,
lambdaExpr: lambdaExpr,
functionType: .method)
}

return .class(nameToken, resolvedMethods, resolvedStaticMethods)
}

mutating private func handleFunctionDeclaration(nameToken: Token,
Expand Down
3 changes: 3 additions & 0 deletions slox/ResolverError.swift
Expand Up @@ -14,6 +14,7 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError {
case cannotReturnOutsideFunction
case cannotReferenceThisOutsideClass
case cannotReturnValueFromInitializer
case staticInitsNotAllowed

var description: String {
switch self {
Expand All @@ -29,6 +30,8 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError {
return "Cannot use `this` from outside a class"
case .cannotReturnValueFromInitializer:
return "Cannot return value from an initializer"
case .staticInitsNotAllowed:
return "Cannot have class-level init function"
}
}
}
2 changes: 1 addition & 1 deletion slox/Statement.swift
Expand Up @@ -14,5 +14,5 @@ indirect enum Statement: Equatable {
case `while`(Expression, Statement)
case function(Token, Expression)
case `return`(Token, Expression?)
case `class`(Token, [Statement])
case `class`(Token, [Statement], [Statement])
}

0 comments on commit 57ef297

Please sign in to comment.