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 classes #8

Merged
merged 12 commits into from Mar 7, 2024
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -18,6 +18,8 @@ So far, the following have been implemented in `slox`:
- Variable declaration and assignment
- Function declaration and invocation
- Lambda expressions
- Class declaration and instantiation
- Instance properties and methods

# Design

Expand Down Expand Up @@ -56,6 +58,10 @@ I also decided to create specialized error enums, one for each phase of processi

Instead of maintaining set of native functions in `Interpreter`'s constructor, they reside inside the `NativeFunction` enum. When the interpreter is constructed, it defines each of the native functions enumerated in `NativeFunction`. That keeps the responsibility of `Interpreter` clean and focused.

# 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.

# Relevant links

- The online version of "Crafting Interpreters"
Expand Down
18 changes: 12 additions & 6 deletions slox.xcodeproj/project.pbxproj
Expand Up @@ -9,19 +9,18 @@
/* Begin PBXBuildFile section */
870230752B9571490056FE57 /* MutableCollection+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870230742B9571490056FE57 /* MutableCollection+Extension.swift */; };
870230772B9575250056FE57 /* ResolverError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283972B9523AD00E49035 /* ResolverError.swift */; };
870230782B95752C0056FE57 /* FunctionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283992B956A2F00E49035 /* FunctionType.swift */; };
870230792B9575420056FE57 /* MutableCollection+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870230742B9571490056FE57 /* MutableCollection+Extension.swift */; };
8702307A2B9575460056FE57 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283952B95127100E49035 /* Resolver.swift */; };
8702307B2B95755E0056FE57 /* ResolvedExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283912B95118A00E49035 /* ResolvedExpression.swift */; };
8702307C2B9575610056FE57 /* ResolvedStatement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283932B95122800E49035 /* ResolvedStatement.swift */; };
8702307E2B95AA2A0056FE57 /* ResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8702307D2B95AA2A0056FE57 /* ResolverTests.swift */; };
870230802B96E9580056FE57 /* LoxClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8702307F2B96E9580056FE57 /* LoxClass.swift */; };
8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732838E2B93F89300E49035 /* NativeFunction.swift */; };
873283902B93FC0900E49035 /* NativeFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732838E2B93F89300E49035 /* NativeFunction.swift */; };
873283922B95118A00E49035 /* ResolvedExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283912B95118A00E49035 /* ResolvedExpression.swift */; };
873283942B95122800E49035 /* ResolvedStatement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283932B95122800E49035 /* ResolvedStatement.swift */; };
873283962B95127100E49035 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283952B95127100E49035 /* Resolver.swift */; };
873283982B9523AD00E49035 /* ResolverError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283972B9523AD00E49035 /* ResolverError.swift */; };
8732839A2B956A2F00E49035 /* FunctionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283992B956A2F00E49035 /* FunctionType.swift */; };
873CCB212B8D5FAE00FC249A /* Interpreter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB202B8D5FAE00FC249A /* Interpreter.swift */; };
873CCB232B8D617C00FC249A /* RuntimeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB222B8D617C00FC249A /* RuntimeError.swift */; };
873CCB252B8D765D00FC249A /* Lox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB242B8D765D00FC249A /* Lox.swift */; };
Expand Down Expand Up @@ -56,6 +55,9 @@
87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; };
87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */; };
87C2F3742B91C2BA00126707 /* Return.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* Return.swift */; };
87EEDFBE2B96F52D00C7FE6D /* LoxInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */; };
87EEDFBF2B9920F900C7FE6D /* LoxClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8702307F2B96E9580056FE57 /* LoxClass.swift */; };
87EEDFC02B9920FD00C7FE6D /* LoxInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand All @@ -74,12 +76,12 @@
870230742B9571490056FE57 /* MutableCollection+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MutableCollection+Extension.swift"; sourceTree = "<group>"; };
870230762B9574A90056FE57 /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = "<group>"; };
8702307D2B95AA2A0056FE57 /* ResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolverTests.swift; sourceTree = "<group>"; };
8702307F2B96E9580056FE57 /* LoxClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxClass.swift; sourceTree = "<group>"; };
8732838E2B93F89300E49035 /* NativeFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeFunction.swift; sourceTree = "<group>"; };
873283912B95118A00E49035 /* ResolvedExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolvedExpression.swift; sourceTree = "<group>"; };
873283932B95122800E49035 /* ResolvedStatement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolvedStatement.swift; sourceTree = "<group>"; };
873283952B95127100E49035 /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
873283972B9523AD00E49035 /* ResolverError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolverError.swift; sourceTree = "<group>"; };
873283992B956A2F00E49035 /* FunctionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionType.swift; sourceTree = "<group>"; };
873CCB202B8D5FAE00FC249A /* Interpreter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interpreter.swift; sourceTree = "<group>"; };
873CCB222B8D617C00FC249A /* RuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeError.swift; sourceTree = "<group>"; };
873CCB242B8D765D00FC249A /* Lox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lox.swift; sourceTree = "<group>"; };
Expand All @@ -101,6 +103,7 @@
877168C12B91A9BD00723543 /* Return.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Return.swift; sourceTree = "<group>"; };
87BAFC482B9179CB0013E5FE /* LoxCallable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxCallable.swift; sourceTree = "<group>"; };
87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefinedFunction.swift; sourceTree = "<group>"; };
87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxInstance.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -145,10 +148,11 @@
children = (
873CCB322B8ED8B900FC249A /* Environment.swift */,
876A31642B8C04990085A350 /* Expression.swift */,
873283992B956A2F00E49035 /* FunctionType.swift */,
873CCB202B8D5FAE00FC249A /* Interpreter.swift */,
873CCB242B8D765D00FC249A /* Lox.swift */,
87BAFC482B9179CB0013E5FE /* LoxCallable.swift */,
8702307F2B96E9580056FE57 /* LoxClass.swift */,
87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */,
876A315E2B897EEB0085A350 /* LoxValue.swift */,
870230742B9571490056FE57 /* MutableCollection+Extension.swift */,
8732838E2B93F89300E49035 /* NativeFunction.swift */,
Expand Down Expand Up @@ -280,13 +284,14 @@
87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */,
873CCB212B8D5FAE00FC249A /* Interpreter.swift in Sources */,
877168C22B91A9BD00723543 /* Return.swift in Sources */,
870230802B96E9580056FE57 /* LoxClass.swift in Sources */,
876A31672B8C11810085A350 /* Parser.swift in Sources */,
8732839A2B956A2F00E49035 /* FunctionType.swift in Sources */,
876560072B8827F9002BDE42 /* Scanner.swift in Sources */,
87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */,
873CCB332B8ED8B900FC249A /* Environment.swift in Sources */,
876A31652B8C04990085A350 /* Expression.swift in Sources */,
876560032B882259002BDE42 /* TokenType.swift in Sources */,
87EEDFBE2B96F52D00C7FE6D /* LoxInstance.swift in Sources */,
873283962B95127100E49035 /* Resolver.swift in Sources */,
8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */,
873283922B95118A00E49035 /* ResolvedExpression.swift in Sources */,
Expand All @@ -313,7 +318,6 @@
876A315C2B897C000085A350 /* Token.swift in Sources */,
876A31572B8979BD0085A350 /* ScannerTests.swift in Sources */,
876A31602B89827B0085A350 /* LoxValue.swift in Sources */,
870230782B95752C0056FE57 /* FunctionType.swift in Sources */,
873CCB312B8EBB7800FC249A /* Statement.swift in Sources */,
8755B8B52B91984C00530DC4 /* LoxCallable.swift in Sources */,
873283902B93FC0900E49035 /* NativeFunction.swift in Sources */,
Expand All @@ -322,7 +326,9 @@
873CCB282B8E7B6300FC249A /* Parser.swift in Sources */,
873CCB272B8E7AD100FC249A /* ParserTests.swift in Sources */,
876A315D2B897C020085A350 /* Scanner.swift in Sources */,
87EEDFBF2B9920F900C7FE6D /* LoxClass.swift in Sources */,
876A315B2B897BFD0085A350 /* TokenType.swift in Sources */,
87EEDFC02B9920FD00C7FE6D /* LoxInstance.swift in Sources */,
873CCB292B8E7B6900FC249A /* ParseError.swift in Sources */,
870230792B9575420056FE57 /* MutableCollection+Extension.swift in Sources */,
873CCB2A2B8E7BB600FC249A /* Expression.swift in Sources */,
Expand Down
3 changes: 3 additions & 0 deletions slox/Expression.swift
Expand Up @@ -15,4 +15,7 @@ indirect enum Expression: Equatable {
case logical(Expression, Token, Expression)
case call(Expression, Token, [Expression])
case lambda([Token], [Statement])
case get(Expression, Token)
case set(Expression, Token, Expression)
case this(Token)
}
12 changes: 0 additions & 12 deletions slox/FunctionType.swift

This file was deleted.

103 changes: 87 additions & 16 deletions slox/Interpreter.swift
Expand Up @@ -56,6 +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 .function(let name, let lambda):
try handleFunctionDeclaration(name: name, lambda: lambda)
case .return(let returnToken, let expr):
Expand All @@ -78,16 +80,46 @@ class Interpreter {
print(literal)
}

private func handleClassDeclaration(nameToken: Token, body: [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 {
guard case .function(let nameToken, let lambdaExpr) = method else {
throw RuntimeError.notAFunctionDeclaration
}

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

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

let newClass = LoxClass(name: nameToken.lexeme, methods: methods)
try environment.assignAtDepth(name: nameToken.lexeme, value: .class(newClass), depth: 0)
}

private func handleFunctionDeclaration(name: Token, lambda: ResolvedExpression) throws {
guard case .lambda(let params, let body) = lambda else {
throw RuntimeError.notALambda
}

let environmentWhenDeclared = self.environment
let function = UserDefinedFunction(name: name.lexeme,
params: params,
enclosingEnvironment: environmentWhenDeclared,
body: body)
params: params,
enclosingEnvironment: environmentWhenDeclared,
body: body,
isInitializer: false)
environment.define(name: name.lexeme, value: .userDefinedFunction(function))
}

Expand Down Expand Up @@ -148,7 +180,15 @@ class Interpreter {
case .logical(let leftExpr, let oper, let rightExpr):
return try handleLogicalExpression(leftExpr: leftExpr, oper: oper, rightExpr: rightExpr)
case .call(let calleeExpr, let rightParen, let args):
return try handleFunctionCallExpression(calleeExpr: calleeExpr, rightParen: rightParen, args: args)
return try handleCallExpression(calleeExpr: calleeExpr, rightParen: rightParen, args: args)
case .get(let instanceExpr, let propertyNameToken):
return try handleGetExpression(instanceExpr: instanceExpr, propertyNameToken: propertyNameToken)
case .set(let instanceExpr, let propertyNameToken, let valueExpr):
return try handleSetExpression(instanceExpr: instanceExpr,
propertyNameToken: propertyNameToken,
valueExpr: valueExpr)
case .this(let thisToken, let depth):
return try handleThis(thisToken: thisToken, depth: depth)
case .lambda(let params, let statements):
return try handleLambdaExpression(params: params, statements: statements)
}
Expand Down Expand Up @@ -253,22 +293,24 @@ class Interpreter {
}
}

private func handleFunctionCallExpression(calleeExpr: ResolvedExpression,
rightParen: Token,
args: [ResolvedExpression]) throws -> LoxValue {
private func handleCallExpression(calleeExpr: ResolvedExpression,
rightParen: Token,
args: [ResolvedExpression]) throws -> LoxValue {
let callee = try evaluate(expr: calleeExpr)

let actualFunction: LoxCallable = switch callee {
let actualCallable: LoxCallable = switch callee {
case .userDefinedFunction(let userDefinedFunction):
userDefinedFunction
case .nativeFunction(let nativeFunction):
nativeFunction
case .class(let klass):
klass
default:
throw RuntimeError.notAFunction
throw RuntimeError.notACallableObject
}

guard args.count == actualFunction.arity else {
throw RuntimeError.wrongArity(actualFunction.arity, args.count)
guard args.count == actualCallable.arity else {
throw RuntimeError.wrongArity(actualCallable.arity, args.count)
}

var argValues: [LoxValue] = []
Expand All @@ -277,16 +319,45 @@ class Interpreter {
argValues.append(argValue)
}

return try actualFunction.call(interpreter: self, args: argValues)
return try actualCallable.call(interpreter: self, args: argValues)
}

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

return try instance.get(propertyName: propertyNameToken.lexeme)
}

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

let propertyValue = try evaluate(expr: valueExpr)

instance.set(propertyName: propertyNameToken.lexeme, propertyValue: propertyValue)
return propertyValue
}

private func handleThis(thisToken: Token, depth: Int) throws -> LoxValue {
return try environment.getValueAtDepth(name: thisToken.lexeme, depth: depth)
}

private func handleLambdaExpression(params: [Token], statements: [ResolvedStatement]) throws -> LoxValue {
let environmentWhenDeclared = self.environment

let function = UserDefinedFunction(name: "<lambda>",
params: params,
enclosingEnvironment: environmentWhenDeclared,
body: statements)
let function = UserDefinedFunction(name: "lambda",
params: params,
enclosingEnvironment: environmentWhenDeclared,
body: statements,
isInitializer: false)

return .userDefinedFunction(function)
}
Expand Down
38 changes: 38 additions & 0 deletions slox/LoxClass.swift
@@ -0,0 +1,38 @@
//
// LoxClass.swift
// slox
//
// Created by Danielle Kefford on 3/4/24.
//

class LoxClass: LoxCallable, Equatable {
var name: String
var arity: Int {
if let initializer = methods["init"] {
return initializer.params.count
}

return 0
}
var methods: [String: UserDefinedFunction]

init(name: String, methods: [String: UserDefinedFunction]) {
self.name = name
self.methods = methods
}

static func == (lhs: LoxClass, rhs: LoxClass) -> Bool {
return lhs === rhs
}

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

if let initializer = methods["init"] {
let boundInit = initializer.bind(instance: newInstance)
let _ = try boundInit.call(interpreter: interpreter, args: args)
}

return .instance(newInstance)
}
}