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 class level methods #9

Merged
merged 7 commits into from Mar 10, 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
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])
}