diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index c7fc6dc..230397c 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -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): @@ -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 } @@ -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 { @@ -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 @@ -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 } @@ -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 } diff --git a/slox/LoxClass.swift b/slox/LoxClass.swift index 4fc8410..7beb58f 100644 --- a/slox/LoxClass.swift +++ b/slox/LoxClass.swift @@ -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"] { @@ -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 { diff --git a/slox/LoxInstance.swift b/slox/LoxInstance.swift index fc7e6a0..d5d9f0e 100644 --- a/slox/LoxInstance.swift +++ b/slox/LoxInstance.swift @@ -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 { diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index cb59167..c36f484 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -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 { @@ -29,7 +28,7 @@ enum LoxValue: CustomStringConvertible, Equatable { return "" case .nativeFunction(let function): return "" - case .class(let klass): + case .instance(let klass as LoxClass): return "" case .instance(let instance): return "" diff --git a/slox/Parser.swift b/slox/Parser.swift index f85c492..8326a86 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -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) diff --git a/slox/ResolvedStatement.swift b/slox/ResolvedStatement.swift index 7fd729e..98b8831 100644 --- a/slox/ResolvedStatement.swift +++ b/slox/ResolvedStatement.swift @@ -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]) } diff --git a/slox/Resolver.swift b/slox/Resolver.swift index ccbc140..3f21835 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -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, @@ -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 @@ -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 } @@ -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, diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift index 6afb453..fba2e49 100644 --- a/slox/ResolverError.swift +++ b/slox/ResolverError.swift @@ -14,6 +14,7 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { case cannotReturnOutsideFunction case cannotReferenceThisOutsideClass case cannotReturnValueFromInitializer + case staticInitsNotAllowed var description: String { switch self { @@ -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" } } } diff --git a/slox/Statement.swift b/slox/Statement.swift index 8807e68..47b471b 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -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]) } diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 111ee78..0476cfb 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -603,6 +603,7 @@ final class InterpreterTests: XCTestCase { let statements: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + [], []), .variableDeclaration( Token(type: .identifier, lexeme: "person", line: 2), @@ -661,7 +662,8 @@ final class InterpreterTests: XCTestCase { Token(type: .identifier, lexeme: "name", line: 3), 0))), ])), - ]), + ], + []), .variableDeclaration( Token(type: .identifier, lexeme: "me", line: 6), .call( @@ -718,7 +720,8 @@ final class InterpreterTests: XCTestCase { 1), Token(type: .identifier, lexeme: "name", line: 3)))), ])), - ]), + ], + []), .variableDeclaration( Token(type: .identifier, lexeme: "me", line: 6), .call( @@ -758,6 +761,7 @@ final class InterpreterTests: XCTestCase { let statements: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + [], []), .variableDeclaration( Token(type: .identifier, lexeme: "person", line: 2), @@ -822,7 +826,8 @@ final class InterpreterTests: XCTestCase { Token(type: .identifier, lexeme: "age", line: 4), 0))), ])) - ]), + ], + []), .variableDeclaration( Token(type: .identifier, lexeme: "person", line: 7), .call( @@ -878,7 +883,8 @@ final class InterpreterTests: XCTestCase { Token(type: .identifier, lexeme: "name", line: 3), 0))), ])) - ]), + ], + []), .variableDeclaration( Token(type: .identifier, lexeme: "me", line: 6), .call( @@ -914,4 +920,56 @@ final class InterpreterTests: XCTestCase { let expected: LoxValue = .string("Becca") XCTAssertEqual(actual, expected) } + + func testInterpretClassWithStaticMethod() throws { + // class Math { + // class add(a, b) { + // return a + b; + // } + // } + // Math.add(2, 3) + let statements: [ResolvedStatement] = [ + .class( + Token(type: .identifier, lexeme: "Math", line: 1), + [], + [ + .function( + Token(type: .identifier, lexeme: "add", line: 2), + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 2), + Token(type: .identifier, lexeme: "b", line: 2), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 3), + .binary( + .variable( + Token(type: .identifier, lexeme: "a", line: 3), + 0), + Token(type: .plus, lexeme: "+", line: 3), + .variable( + Token(type: .identifier, lexeme: "b", line: 3), + 0))) + ])) + ]), + .expression( + .call( + .get( + .variable( + Token(type: .identifier, lexeme: "Math", line: 6), + 0), + Token(type: .identifier, lexeme: "add", line: 6)), + Token(type: .rightParen, lexeme: ")", line: 6), + [ + .literal(.number(2)), + .literal(.number(3)), + ])), + ] + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(statements: statements) + let expected: LoxValue = .number(5) + XCTAssertEqual(actual, expected) + } } diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index 7cca24b..18e90d0 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -830,8 +830,9 @@ final class ParserTests: XCTestCase { Token(type: .identifier, lexeme: "theAnswer", line: 1), .lambda( [], - [.print(.literal(.number(42)))]) - ), + [ + .print(.literal(.number(42))) + ])), ] XCTAssertEqual(actual, expected) } @@ -995,7 +996,8 @@ final class ParserTests: XCTestCase { .this(Token(type: .this, lexeme: "this", line: 3)), Token(type: .identifier, lexeme: "name", line: 3))) ])) - ]) + ], + []) ] XCTAssertEqual(actual, expected) } @@ -1044,4 +1046,63 @@ final class ParserTests: XCTestCase { ] XCTAssertEqual(actual, expected) } + + func testParseClassWithStaticMethod() throws { + // class Math { + // class add(a, b) { + // return a + b; + // } + // } + let tokens: [Token] = [ + Token(type: .class, lexeme: "class", line: 1), + Token(type: .identifier, lexeme: "Math", line: 1), + Token(type: .leftBrace, lexeme: "{", line: 1), + + Token(type: .class, lexeme: "class", line: 2), + Token(type: .identifier, lexeme: "add", line: 2), + Token(type: .leftParen, lexeme: "(", line: 2), + Token(type: .identifier, lexeme: "a", line: 2), + Token(type: .comma, lexeme: ",", line: 2), + Token(type: .identifier, lexeme: "b", line: 2), + Token(type: .rightParen, lexeme: ")", line: 2), + Token(type: .leftBrace, lexeme: "{", line: 2), + + Token(type: .return, lexeme: "return", line: 3), + Token(type: .identifier, lexeme: "a", line: 3), + Token(type: .plus, lexeme: "+", line: 3), + Token(type: .identifier, lexeme: "b", line: 3), + Token(type: .semicolon, lexeme: ";", line: 3), + + Token(type: .rightBrace, lexeme: "}", line: 4), + + Token(type: .rightBrace, lexeme: "}", line: 5), + Token(type: .eof, lexeme: "", line: 5), + ] + + var parser = Parser(tokens: tokens) + let actual = try parser.parse() + let expected: [Statement] = [ + .class( + Token(type: .identifier, lexeme: "Math", line: 1), + [], + [ + .function( + Token(type: .identifier, lexeme: "add", line: 2), + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 2), + Token(type: .identifier, lexeme: "b", line: 2), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 3), + .binary( + .variable(Token(type: .identifier, lexeme: "a", line: 3)), + Token(type: .plus, lexeme: "+", line: 3), + .variable(Token(type: .identifier, lexeme: "b", line: 3)))) + ])) + ]) + ] + XCTAssertEqual(actual, expected) + } } diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index b24219b..e031194 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -212,7 +212,8 @@ final class ResolverTests: XCTestCase { .this(Token(type: .this, lexeme: "this", line: 3)), Token(type: .identifier, lexeme: "name", line: 3))) ])) - ]) + ], + []) ] var resolver = Resolver() @@ -233,7 +234,8 @@ final class ResolverTests: XCTestCase { 1), Token(type: .identifier, lexeme: "name", line: 3))) ])) - ]) + ], + []) ] XCTAssertEqual(actual, expected) } @@ -280,7 +282,8 @@ final class ResolverTests: XCTestCase { Token(type: .return, lexeme: "return", line: 3), .literal(.number(42))) ])) - ]) + ], + []) ] var resolver = Resolver() @@ -288,6 +291,92 @@ final class ResolverTests: XCTestCase { XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in XCTAssertEqual(actualError as! ResolverError, expectedError) } + } + func testResolveClassWithStaticMethod() throws { + let statements: [Statement] = [ + .class( + Token(type: .identifier, lexeme: "Math", line: 1), + [], + [ + .function( + Token(type: .identifier, lexeme: "add", line: 2), + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 2), + Token(type: .identifier, lexeme: "b", line: 2), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 3), + .binary( + .variable(Token(type: .identifier, lexeme: "a", line: 3)), + Token(type: .plus, lexeme: "+", line: 3), + .variable(Token(type: .identifier, lexeme: "b", line: 3)))) + ])) + ]) + ] + + var resolver = Resolver() + let actual = try resolver.resolve(statements: statements) + let expected: [ResolvedStatement] = [ + .class( + Token(type: .identifier, lexeme: "Math", line: 1), + [], + [ + .function( + Token(type: .identifier, lexeme: "add", line: 2), + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 2), + Token(type: .identifier, lexeme: "b", line: 2), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 3), + .binary( + .variable( + Token(type: .identifier, lexeme: "a", line: 3), + 0), + Token(type: .plus, lexeme: "+", line: 3), + .variable( + Token(type: .identifier, lexeme: "b", line: 3), + 0))) + ])) + ]) + ] + XCTAssertEqual(actual, expected) + } + + func testResolveClassWithStaticInitMethod() throws { + // class BadClass { + // class init() { + // this.name = "bad"; + // } + // } + let statements: [Statement] = [ + .class( + Token(type: .identifier, lexeme: "Math", line: 1), + [], + [ + .function( + Token(type: .identifier, lexeme: "init", line: 2), + .lambda( + [], + [ + .expression( + .set( + .this(Token(type: .this, lexeme: "this", line: 3)), + Token(type: .identifier, lexeme: "name", line: 3), + .literal(.string("bad")))) + ])) + ]) + ] + + var resolver = Resolver() + let expectedError = ResolverError.staticInitsNotAllowed + XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in + XCTAssertEqual(actualError as! ResolverError, expectedError) + } } }