From 650070b0f1a232cd3922a63f03798180819deedf Mon Sep 17 00:00:00 2001 From: quephird Date: Thu, 7 Mar 2024 19:39:28 -0800 Subject: [PATCH 1/7] Very rough first draft allowing for static class methods. --- slox/Interpreter.swift | 66 +++++++++++++++++++++++++++--------- slox/LoxClass.swift | 5 +-- slox/LoxInstance.swift | 22 +++++++----- slox/LoxValue.swift | 6 +++- slox/Parser.swift | 12 +++++-- slox/ResolvedStatement.swift | 2 +- slox/Resolver.swift | 28 ++++++++++++--- slox/Statement.swift | 2 +- 8 files changed, 105 insertions(+), 38 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index c7fc6dc..ece22db 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,15 +99,38 @@ 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) + 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 metaclass = LoxClass(name: "\(nameToken.lexeme) metaclass" , + methods: staticMethodImpls, + metaclass: nil) + let newClass = LoxClass(name: nameToken.lexeme, + methods: methodImpls, + metaclass: metaclass) try environment.assignAtDepth(name: nameToken.lexeme, value: .class(newClass), depth: 0) } @@ -324,12 +349,21 @@ class Interpreter { 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 + let targetValue = try evaluate(expr: instanceExpr) + + let target: LoxInstance = switch targetValue { + case .instance(let instance): + instance + case .class(let klass): + klass + default: + fatalError() } +// guard case .instance(let instance) = instanceValue else { +// throw RuntimeError.onlyInstancesHaveProperties +// } - return try instance.get(propertyName: propertyNameToken.lexeme) + return try target.get(propertyName: propertyNameToken.lexeme) } private func handleSetExpression(instanceExpr: ResolvedExpression, diff --git a/slox/LoxClass.swift b/slox/LoxClass.swift index 4fc8410..dd45103 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"] { @@ -16,9 +16,10 @@ class LoxClass: LoxCallable, Equatable { } var methods: [String: UserDefinedFunction] - init(name: String, methods: [String: UserDefinedFunction]) { + init(name: String, methods: [String: UserDefinedFunction], metaclass: LoxClass?) { self.name = name self.methods = methods + super.init(klass: metaclass) } static func == (lhs: LoxClass, rhs: LoxClass) -> Bool { diff --git a/slox/LoxInstance.swift b/slox/LoxInstance.swift index fc7e6a0..2765e19 100644 --- a/slox/LoxInstance.swift +++ b/slox/LoxInstance.swift @@ -6,24 +6,28 @@ // class LoxInstance: Equatable { - var klass: LoxClass + var klass: LoxClass? var properties: [String: LoxValue] = [:] - init(klass: LoxClass) { + init(klass: LoxClass?) { self.klass = klass } func get(propertyName: String) throws -> LoxValue { - if let propertyValue = self.properties[propertyName] { - return propertyValue - } + if let klass { + if let propertyValue = self.properties[propertyName] { + return propertyValue + } + + if let method = klass.methods[propertyName] { + let boundMethod = method.bind(instance: self) + return .userDefinedFunction(boundMethod) + } - if let method = klass.methods[propertyName] { - let boundMethod = method.bind(instance: self) - return .userDefinedFunction(boundMethod) + throw RuntimeError.undefinedProperty(propertyName) } - throw RuntimeError.undefinedProperty(propertyName) + fatalError() } func set(propertyName: String, propertyValue: LoxValue) { diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index cb59167..18d577e 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -32,7 +32,11 @@ enum LoxValue: CustomStringConvertible, Equatable { case .class(let klass): return "" case .instance(let instance): - return "" + if let klass = instance.klass { + return "" + } + + 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..ec6d615 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,23 @@ 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 + } + + let functionType: FunctionType = if nameToken.lexeme == "init" { + .initializer + } else { + .method + } + return try handleFunctionDeclaration( + nameToken: nameToken, + lambdaExpr: lambdaExpr, + functionType: functionType) + } + + return .class(nameToken, resolvedMethods, resolvedStaticMethods) } mutating private func handleFunctionDeclaration(nameToken: Token, 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]) } From c9383841731fbe34b93eaa03eccf01195c75dc53 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 8 Mar 2024 13:29:35 -0800 Subject: [PATCH 2/7] Some tiny tweaks. --- slox/Interpreter.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index ece22db..afd2903 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -349,21 +349,17 @@ class Interpreter { private func handleGetExpression(instanceExpr: ResolvedExpression, propertyNameToken: Token) throws -> LoxValue { - let targetValue = try evaluate(expr: instanceExpr) - - let target: LoxInstance = switch targetValue { + let instance: LoxInstance = switch try evaluate(expr: instanceExpr) { case .instance(let instance): instance case .class(let klass): + // NOTA BENE: Remember that LoxClass inherits from LoxInstance now! klass default: fatalError() } -// guard case .instance(let instance) = instanceValue else { -// throw RuntimeError.onlyInstancesHaveProperties -// } - return try target.get(propertyName: propertyNameToken.lexeme) + return try instance.get(propertyName: propertyNameToken.lexeme) } private func handleSetExpression(instanceExpr: ResolvedExpression, From ca24bb6a89553e589adb50ca8d8ad63244c265ad Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 8 Mar 2024 13:36:37 -0800 Subject: [PATCH 3/7] Updated tests. --- slox/Interpreter.swift | 11 ++++++++--- sloxTests/InterpreterTests.swift | 14 ++++++++++---- sloxTests/ParserTests.swift | 3 ++- sloxTests/ResolverTests.swift | 9 ++++++--- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index afd2903..48fcfaa 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -356,7 +356,7 @@ class Interpreter { // NOTA BENE: Remember that LoxClass inherits from LoxInstance now! klass default: - fatalError() + throw RuntimeError.onlyInstancesHaveProperties } return try instance.get(propertyName: propertyNameToken.lexeme) @@ -365,8 +365,13 @@ 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 { + let instance: LoxInstance = switch try evaluate(expr: instanceExpr) { + case .instance(let instance): + instance + case .class(let klass): + // NOTA BENE: Remember that LoxClass inherits from LoxInstance now! + klass + default: throw RuntimeError.onlyInstancesHaveProperties } diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 111ee78..73eb5fb 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( diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index 7cca24b..da47a93 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -995,7 +995,8 @@ final class ParserTests: XCTestCase { .this(Token(type: .this, lexeme: "this", line: 3)), Token(type: .identifier, lexeme: "name", line: 3))) ])) - ]) + ], + []) ] XCTAssertEqual(actual, expected) } diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index b24219b..0170fa3 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() From dc8f98f5dcc5ae6cd9d5700d5c5bf7b800a6e7f1 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 8 Mar 2024 17:54:11 -0800 Subject: [PATCH 4/7] Added tests. --- sloxTests/InterpreterTests.swift | 52 ++++++++++++++++++++++++++ sloxTests/ParserTests.swift | 64 +++++++++++++++++++++++++++++++- sloxTests/ResolverTests.swift | 55 +++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 2 deletions(-) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 73eb5fb..0476cfb 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -920,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 da47a93..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) } @@ -1045,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 0170fa3..3a90aa5 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -293,4 +293,59 @@ final class ResolverTests: XCTestCase { } } + + 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) + } } From 83b28ccccdf748499d57b211142e0f97cbf483ea Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 9 Mar 2024 15:01:13 -0800 Subject: [PATCH 5/7] Since `LoxClass` now inherits from `LoxInstance`, there is no reason for the `LoxValue` enum to have a `class` case. This actually simplfies handling of calls and getters and setters. We also solved the problem of ever having a `nil` value for a parent class, since whenever we ask for it, a new `LoxClass` instance is created lazily. --- slox/Interpreter.swift | 20 ++++---------------- slox/LoxInstance.swift | 38 +++++++++++++++++++++++++------------- slox/LoxValue.swift | 9 ++------- slox/Resolver.swift | 9 ++++----- slox/ResolverError.swift | 3 +++ 5 files changed, 38 insertions(+), 41 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 48fcfaa..319af43 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -131,7 +131,7 @@ class Interpreter { let newClass = LoxClass(name: nameToken.lexeme, methods: methodImpls, metaclass: metaclass) - try environment.assignAtDepth(name: nameToken.lexeme, value: .class(newClass), depth: 0) + try environment.assignAtDepth(name: nameToken.lexeme, value: .instance(newClass), depth: 0) } private func handleFunctionDeclaration(name: Token, lambda: ResolvedExpression) throws { @@ -328,7 +328,7 @@ class Interpreter { userDefinedFunction case .nativeFunction(let nativeFunction): nativeFunction - case .class(let klass): + case .instance(let klass as LoxClass): klass default: throw RuntimeError.notACallableObject @@ -349,13 +349,7 @@ class Interpreter { private func handleGetExpression(instanceExpr: ResolvedExpression, propertyNameToken: Token) throws -> LoxValue { - let instance: LoxInstance = switch try evaluate(expr: instanceExpr) { - case .instance(let instance): - instance - case .class(let klass): - // NOTA BENE: Remember that LoxClass inherits from LoxInstance now! - klass - default: + guard case .instance(let instance) = try evaluate(expr: instanceExpr) else { throw RuntimeError.onlyInstancesHaveProperties } @@ -365,13 +359,7 @@ class Interpreter { private func handleSetExpression(instanceExpr: ResolvedExpression, propertyNameToken: Token, valueExpr: ResolvedExpression) throws -> LoxValue { - let instance: LoxInstance = switch try evaluate(expr: instanceExpr) { - case .instance(let instance): - instance - case .class(let klass): - // NOTA BENE: Remember that LoxClass inherits from LoxInstance now! - klass - default: + guard case .instance(let instance) = try evaluate(expr: instanceExpr) else { throw RuntimeError.onlyInstancesHaveProperties } diff --git a/slox/LoxInstance.swift b/slox/LoxInstance.swift index 2765e19..6c9de4a 100644 --- a/slox/LoxInstance.swift +++ b/slox/LoxInstance.swift @@ -6,28 +6,40 @@ // 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: [:], metaclass: nil) + } + return _klass! + } var properties: [String: LoxValue] = [:] init(klass: LoxClass?) { - self.klass = klass + self._klass = klass } func get(propertyName: String) throws -> LoxValue { - if let klass { - if let propertyValue = self.properties[propertyName] { - return propertyValue - } - - if let method = klass.methods[propertyName] { - let boundMethod = method.bind(instance: self) - return .userDefinedFunction(boundMethod) - } + if let propertyValue = self.properties[propertyName] { + return propertyValue + } - throw RuntimeError.undefinedProperty(propertyName) + if let method = klass.methods[propertyName] { + let boundMethod = method.bind(instance: self) + return .userDefinedFunction(boundMethod) } - fatalError() + throw RuntimeError.undefinedProperty(propertyName) } func set(propertyName: String, propertyValue: LoxValue) { diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index 18d577e..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,14 +28,10 @@ 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): - if let klass = instance.klass { - return "" - } - - return "" + return "" } } } diff --git a/slox/Resolver.swift b/slox/Resolver.swift index ec6d615..3f21835 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -118,15 +118,14 @@ struct Resolver { throw ResolverError.notAFunction } - let functionType: FunctionType = if nameToken.lexeme == "init" { - .initializer - } else { - .method + if nameToken.lexeme == "init" { + throw ResolverError.staticInitsNotAllowed } + return try handleFunctionDeclaration( nameToken: nameToken, lambdaExpr: lambdaExpr, - functionType: functionType) + functionType: .method) } return .class(nameToken, resolvedMethods, resolvedStaticMethods) 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" } } } From 85265935c2b1e6945c8e97569a5df42ed4924cc9 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 9 Mar 2024 15:18:06 -0800 Subject: [PATCH 6/7] Added test for illegal class-level `init`. --- sloxTests/ResolverTests.swift | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index 3a90aa5..e031194 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -291,7 +291,6 @@ final class ResolverTests: XCTestCase { XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in XCTAssertEqual(actualError as! ResolverError, expectedError) } - } func testResolveClassWithStaticMethod() throws { @@ -348,4 +347,36 @@ final class ResolverTests: XCTestCase { ] 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) + } + } } From 2e8ce093727396adab563e39ecd4c1a8d0389a4e Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 9 Mar 2024 20:20:55 -0800 Subject: [PATCH 7/7] A few refinements and comments to add clarity. --- slox/Interpreter.swift | 13 +++++++------ slox/LoxClass.swift | 5 +++-- slox/LoxInstance.swift | 5 ++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 319af43..230397c 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -125,12 +125,13 @@ class Interpreter { staticMethodImpls[nameToken.lexeme] = staticMethodImpl } - let metaclass = LoxClass(name: "\(nameToken.lexeme) metaclass" , - methods: staticMethodImpls, - metaclass: nil) - let newClass = LoxClass(name: nameToken.lexeme, - methods: methodImpls, - metaclass: metaclass) + 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) } diff --git a/slox/LoxClass.swift b/slox/LoxClass.swift index dd45103..7beb58f 100644 --- a/slox/LoxClass.swift +++ b/slox/LoxClass.swift @@ -16,10 +16,11 @@ class LoxClass: LoxInstance, LoxCallable { } var methods: [String: UserDefinedFunction] - init(name: String, methods: [String: UserDefinedFunction], metaclass: LoxClass?) { + init(name: String, methods: [String: UserDefinedFunction]) { self.name = name self.methods = methods - super.init(klass: metaclass) + + super.init(klass: nil) } static func == (lhs: LoxClass, rhs: LoxClass) -> Bool { diff --git a/slox/LoxInstance.swift b/slox/LoxInstance.swift index 6c9de4a..d5d9f0e 100644 --- a/slox/LoxInstance.swift +++ b/slox/LoxInstance.swift @@ -19,12 +19,15 @@ 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: [:], metaclass: nil) + _klass = LoxClass(name: "\(selfClass.name) metaclass", methods: [:]) } return _klass! } var properties: [String: LoxValue] = [:] + /// - 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 }