From 93b0defae3df0bbd3c8f8efa6daae7220a8c34a5 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 9 Mar 2024 23:04:14 -0800 Subject: [PATCH 1/6] Swept through all four phases of the interpreter to plumb the superclass into the class. --- slox/Interpreter.swift | 20 +++++++++++++++++--- slox/LoxClass.swift | 4 +++- slox/LoxInstance.swift | 2 +- slox/ParseError.swift | 3 +++ slox/Parser.swift | 15 +++++++++++++-- slox/ResolvedStatement.swift | 2 +- slox/Resolver.swift | 30 ++++++++++++++++++++++-------- slox/ResolverError.swift | 3 +++ slox/RuntimeError.swift | 4 ++++ slox/Statement.swift | 2 +- 10 files changed, 68 insertions(+), 17 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 230397c..e92bf5f 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -56,8 +56,11 @@ class Interpreter { environment: Environment(enclosingEnvironment: environment)) case .while(let expr, let stmt): try handleWhileStatement(expr: expr, stmt: stmt) - case .class(let nameToken, let methods, let staticMethods): - try handleClassDeclaration(nameToken: nameToken, methods: methods, staticMethods: staticMethods) + case .class(let nameToken, let superclassExpr, let methods, let staticMethods): + try handleClassDeclaration(nameToken: nameToken, + superclassExpr: superclassExpr, + methods: methods, + staticMethods: staticMethods) case .function(let name, let lambda): try handleFunctionDeclaration(name: name, lambda: lambda) case .return(let returnToken, let expr): @@ -81,6 +84,7 @@ class Interpreter { } private func handleClassDeclaration(nameToken: Token, + superclassExpr: ResolvedExpression?, methods: [ResolvedStatement], staticMethods: [ResolvedStatement]) throws { // NOTA BENE: We temporarily set the initial value associated with @@ -88,6 +92,14 @@ class Interpreter { // "allows references to the class inside its own methods". environment.define(name: nameToken.lexeme, value: .nil) + let superclass = try superclassExpr.map { superclassExpr in + guard case .instance(let superclass as LoxClass) = try evaluate(expr: superclassExpr) else { + throw RuntimeError.superclassMustBeAClass + } + + return superclass + } + var methodImpls: [String: UserDefinedFunction] = [:] for method in methods { guard case .function(let nameToken, let lambdaExpr) = method else { @@ -125,7 +137,9 @@ class Interpreter { staticMethodImpls[nameToken.lexeme] = staticMethodImpl } - let newClass = LoxClass(name: nameToken.lexeme, methods: methodImpls) + let newClass = LoxClass(name: nameToken.lexeme, + superclass: superclass, + methods: methodImpls) if !staticMethodImpls.isEmpty { // NOTA BENE: This assigns the static methods to the metaclass, // which is lazily created in `LoxInstance` diff --git a/slox/LoxClass.swift b/slox/LoxClass.swift index 7beb58f..326436c 100644 --- a/slox/LoxClass.swift +++ b/slox/LoxClass.swift @@ -7,6 +7,7 @@ class LoxClass: LoxInstance, LoxCallable { var name: String + var superclass: LoxClass? var arity: Int { if let initializer = methods["init"] { return initializer.params.count @@ -16,8 +17,9 @@ class LoxClass: LoxInstance, LoxCallable { } var methods: [String: UserDefinedFunction] - init(name: String, methods: [String: UserDefinedFunction]) { + init(name: String, superclass: LoxClass?, methods: [String: UserDefinedFunction]) { self.name = name + self.superclass = superclass self.methods = methods super.init(klass: nil) diff --git a/slox/LoxInstance.swift b/slox/LoxInstance.swift index d5d9f0e..156d94c 100644 --- a/slox/LoxInstance.swift +++ b/slox/LoxInstance.swift @@ -19,7 +19,7 @@ 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: [:]) + _klass = LoxClass(name: "\(selfClass.name) metaclass", superclass: nil, methods: [:]) } return _klass! } diff --git a/slox/ParseError.swift b/slox/ParseError.swift index af6d492..d0ea1ad 100644 --- a/slox/ParseError.swift +++ b/slox/ParseError.swift @@ -29,6 +29,7 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { case missingOpenBraceBeforeFunctionBody(Token) case missingCloseParenAfterArguments(Token) case missingIdentifierAfterDot(Token) + case missingSuperclassName(Token) var description: String { switch self { @@ -74,6 +75,8 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { return "[Line \(token.line)] Error: expected right parenthesis after arguments" case .missingIdentifierAfterDot(let token): return "[Line \(token.line)] Error: expected identifer after dot" + case .missingSuperclassName(let token): + return "[Line \(token.line)] Error: expected superclass name" } } } diff --git a/slox/Parser.swift b/slox/Parser.swift index 8326a86..179d4d9 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -36,7 +36,8 @@ struct Parser { // | funDecl // | varDecl // | statement ; - // classDecl → "class" IDENTIFIER "{" function* "}" ; + // classDecl → "class" IDENTIFIER ( "<" IDENTIFIER )? + // "{" function* "}" ; // funDecl → "fun" function ; // function → IDENTIFIER "(" parameters? ")" block ; // varDecl → "var" IDENTIFIER ( "=" expression )? ";" ; @@ -85,6 +86,16 @@ struct Parser { let className = currentToken advanceCursor() + var superclassExpr: Expression? = nil + if currentTokenMatchesAny(types: [.less]) { + guard case .identifier = currentToken.type else { + throw ParseError.missingSuperclassName(currentToken) + } + + superclassExpr = .variable(currentToken) + advanceCursor() + } + if !currentTokenMatchesAny(types: [.leftBrace]) { throw ParseError.missingOpenBraceBeforeClassBody(currentToken) } @@ -105,7 +116,7 @@ struct Parser { } if currentTokenMatchesAny(types: [.rightBrace]) { - return .class(className, methodStatements, staticMethodStatements) + return .class(className, superclassExpr, methodStatements, staticMethodStatements) } throw ParseError.missingClosingBrace(previousToken) diff --git a/slox/ResolvedStatement.swift b/slox/ResolvedStatement.swift index 98b8831..58c6dbd 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], [ResolvedStatement]) + case `class`(Token, ResolvedExpression?, [ResolvedStatement], [ResolvedStatement]) } diff --git a/slox/Resolver.swift b/slox/Resolver.swift index 3f21835..5b8f12c 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -38,8 +38,11 @@ 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 methods, let staticMethods): - return try handleClassDeclaration(nameToken: nameToken, methods: methods, staticMethods: staticMethods) + case .class(let nameToken, let superclassExpr, let methods, let staticMethods): + return try handleClassDeclaration(nameToken: nameToken, + superclassExpr: superclassExpr, + methods: methods, + staticMethods: staticMethods) case .function(let nameToken, let lambdaExpr): return try handleFunctionDeclaration(nameToken: nameToken, lambdaExpr: lambdaExpr, @@ -81,21 +84,32 @@ struct Resolver { } mutating private func handleClassDeclaration(nameToken: Token, + superclassExpr: Expression?, methods: [Statement], staticMethods: [Statement]) throws -> ResolvedStatement { - let previousClassType = currentClassType - currentClassType = .class - try declareVariable(name: nameToken.lexeme) defineVariable(name: nameToken.lexeme) + // ACHTUNG! We need to attmept to resolve the superclass _before_ + // pushing `this` onto the stack, otherwise we won't find it! + var resolvedSuperclassExpr: ResolvedExpression? = nil + if case .variable(let superclassName) = superclassExpr { + if superclassName.lexeme == nameToken.lexeme { + throw ResolverError.classCannotInheritFromItself + } + + resolvedSuperclassExpr = try handleVariable(nameToken: superclassName) + } + beginScope() - // NOTA BENE: Note that the scope stack is never empty at this point - scopeStack.lastMutable["this"] = true + let previousClassType = currentClassType + currentClassType = .class defer { endScope() currentClassType = previousClassType } + // NOTA BENE: Note that the scope stack is never empty at this point + scopeStack.lastMutable["this"] = true let resolvedMethods = try methods.map { method in guard case .function(let nameToken, let lambdaExpr) = method else { @@ -128,7 +142,7 @@ struct Resolver { functionType: .method) } - return .class(nameToken, resolvedMethods, resolvedStaticMethods) + return .class(nameToken, resolvedSuperclassExpr, resolvedMethods, resolvedStaticMethods) } mutating private func handleFunctionDeclaration(nameToken: Token, diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift index fba2e49..24111ef 100644 --- a/slox/ResolverError.swift +++ b/slox/ResolverError.swift @@ -15,6 +15,7 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { case cannotReferenceThisOutsideClass case cannotReturnValueFromInitializer case staticInitsNotAllowed + case classCannotInheritFromItself var description: String { switch self { @@ -32,6 +33,8 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { return "Cannot return value from an initializer" case .staticInitsNotAllowed: return "Cannot have class-level init function" + case .classCannotInheritFromItself: + return "Class cannot inherit from itself" } } } diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index 5cc41e1..cbd6110 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -21,6 +21,7 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case wrongArity(Int, Int) case notALambda case couldNotFindAncestorEnvironmentAtDepth(Int) + case superclassMustBeAClass var description: String { switch self { @@ -50,6 +51,9 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { return "Error: expected lambda as body of function declaration" case .couldNotFindAncestorEnvironmentAtDepth(let depth): return "Error: could not find ancestor environment at depth \(depth)." + case .superclassMustBeAClass: + return "Error: superclass must be a class" + } } } diff --git a/slox/Statement.swift b/slox/Statement.swift index 47b471b..f0d138e 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], [Statement]) + case `class`(Token, Expression?, [Statement], [Statement]) } From 58bfa7f64e97a32d3515223351560b3122b3e557 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 9 Mar 2024 23:28:26 -0800 Subject: [PATCH 2/6] ZOMFG I HAVE INHERITED METHODS WORKING!!!11@2!!! --- slox/LoxClass.swift | 8 ++++++++ slox/LoxInstance.swift | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/slox/LoxClass.swift b/slox/LoxClass.swift index 326436c..5952915 100644 --- a/slox/LoxClass.swift +++ b/slox/LoxClass.swift @@ -29,6 +29,14 @@ class LoxClass: LoxInstance, LoxCallable { return lhs === rhs } + func findMethod(name: String) -> UserDefinedFunction? { + if let method = methods[name] { + return method + } + + return superclass?.findMethod(name: name) + } + func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue { let newInstance = LoxInstance(klass: self) diff --git a/slox/LoxInstance.swift b/slox/LoxInstance.swift index 156d94c..8533a04 100644 --- a/slox/LoxInstance.swift +++ b/slox/LoxInstance.swift @@ -37,7 +37,7 @@ class LoxInstance: Equatable { return propertyValue } - if let method = klass.methods[propertyName] { + if let method = klass.findMethod(name: propertyName) { let boundMethod = method.bind(instance: self) return .userDefinedFunction(boundMethod) } From 616350c7df675bc2d0d8f99add23a6973b5113bd Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 10 Mar 2024 13:57:29 -0700 Subject: [PATCH 3/6] Fixed broken tests. --- sloxTests/InterpreterTests.swift | 7 +++++++ sloxTests/ParserTests.swift | 2 ++ sloxTests/ResolverTests.swift | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 0476cfb..03ebdd0 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), + nil, [], []), .variableDeclaration( @@ -645,6 +646,7 @@ final class InterpreterTests: XCTestCase { let statements: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "sayHello", line: 2), @@ -703,6 +705,7 @@ final class InterpreterTests: XCTestCase { let statements: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "greeting", line: 2), @@ -761,6 +764,7 @@ final class InterpreterTests: XCTestCase { let statements: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [], []), .variableDeclaration( @@ -798,6 +802,7 @@ final class InterpreterTests: XCTestCase { let statements: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "init", line: 2), @@ -865,6 +870,7 @@ final class InterpreterTests: XCTestCase { let statements: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "init", line: 2), @@ -931,6 +937,7 @@ final class InterpreterTests: XCTestCase { let statements: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Math", line: 1), + nil, [], [ .function( diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index 18e90d0..cdfe5c5 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -985,6 +985,7 @@ final class ParserTests: XCTestCase { let expected: [Statement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "sayName", line: 2), @@ -1084,6 +1085,7 @@ final class ParserTests: XCTestCase { let expected: [Statement] = [ .class( Token(type: .identifier, lexeme: "Math", line: 1), + nil, [], [ .function( diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index e031194..a39ccb1 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -201,6 +201,7 @@ final class ResolverTests: XCTestCase { let statements: [Statement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "sayName", line: 2), @@ -221,6 +222,7 @@ final class ResolverTests: XCTestCase { let expected: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "sayName", line: 2), @@ -272,6 +274,7 @@ final class ResolverTests: XCTestCase { let statements: [Statement] = [ .class( Token(type: .identifier, lexeme: "Answer", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "init", line: 2), @@ -297,6 +300,7 @@ final class ResolverTests: XCTestCase { let statements: [Statement] = [ .class( Token(type: .identifier, lexeme: "Math", line: 1), + nil, [], [ .function( @@ -322,6 +326,7 @@ final class ResolverTests: XCTestCase { let expected: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Math", line: 1), + nil, [], [ .function( @@ -357,6 +362,7 @@ final class ResolverTests: XCTestCase { let statements: [Statement] = [ .class( Token(type: .identifier, lexeme: "Math", line: 1), + nil, [], [ .function( From 5f47e2028fecbb29285d701c027acf5b4521f09f Mon Sep 17 00:00:00 2001 From: quephird Date: Mon, 11 Mar 2024 18:17:01 -0700 Subject: [PATCH 4/6] `super` now resolves to superclass. --- slox/Environment.swift | 2 +- slox/Expression.swift | 1 + slox/Interpreter.swift | 28 +++++++ slox/ParseError.swift | 6 ++ slox/Parser.swift | 18 +++- slox/ResolvedExpression.swift | 1 + slox/Resolver.swift | 31 +++++-- slox/RuntimeError.swift | 5 +- sloxTests/InterpreterTests.swift | 137 +++++++++++++++++++++++++++++++ sloxTests/ParserTests.swift | 65 +++++++++++++++ sloxTests/ResolverTests.swift | 10 +++ 11 files changed, 293 insertions(+), 11 deletions(-) diff --git a/slox/Environment.swift b/slox/Environment.swift index 0de7dc2..9fb24e0 100644 --- a/slox/Environment.swift +++ b/slox/Environment.swift @@ -6,7 +6,7 @@ // class Environment: Equatable { - private var enclosingEnvironment: Environment? + var enclosingEnvironment: Environment? private var values: [String: LoxValue] = [:] init(enclosingEnvironment: Environment? = nil) { diff --git a/slox/Expression.swift b/slox/Expression.swift index d3eb342..31577bc 100644 --- a/slox/Expression.swift +++ b/slox/Expression.swift @@ -18,4 +18,5 @@ indirect enum Expression: Equatable { case get(Expression, Token) case set(Expression, Token, Expression) case this(Token) + case `super`(Token, Token) } diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index e92bf5f..466acdf 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -97,6 +97,9 @@ class Interpreter { throw RuntimeError.superclassMustBeAClass } + environment = Environment(enclosingEnvironment: environment); + environment.define(name: "super", value: .instance(superclass)); + return superclass } @@ -146,6 +149,12 @@ class Interpreter { newClass.klass.methods = staticMethodImpls } + // Note that we can't accomplish this via a defer block because we need + // to assign the class to the _outermost_ environment, not the enclosing one. + if superclassExpr != nil { + environment = environment.enclosingEnvironment! + } + try environment.assignAtDepth(name: nameToken.lexeme, value: .instance(newClass), depth: 0) } @@ -231,6 +240,8 @@ class Interpreter { return try handleThis(thisToken: thisToken, depth: depth) case .lambda(let params, let statements): return try handleLambdaExpression(params: params, statements: statements) + case .super(let superToken, let methodToken, let depth): + return try handleSuperExpression(superToken: superToken, methodToken: methodToken, depth: depth) } } @@ -400,6 +411,23 @@ class Interpreter { return .userDefinedFunction(function) } + private func handleSuperExpression(superToken: Token, methodToken: Token, depth: Int) throws -> LoxValue { + guard case .instance(let superclass as LoxClass) = try environment.getValueAtDepth(name: "super", depth: depth) else { + throw RuntimeError.superclassMustBeAClass + } + + guard case .instance(let thisInstance) = try environment.getValueAtDepth(name: "this", depth: depth - 1) else { + throw RuntimeError.notAnInstance + } + + if let method = superclass.findMethod(name: methodToken.lexeme) { + return .userDefinedFunction(method.bind(instance: thisInstance)) + } + + throw RuntimeError.undefinedProperty(methodToken.lexeme) + } + + // Utility functions below private func isEqual(leftValue: LoxValue, rightValue: LoxValue) -> Bool { switch (leftValue, rightValue) { case (.nil, .nil): diff --git a/slox/ParseError.swift b/slox/ParseError.swift index d0ea1ad..03d696d 100644 --- a/slox/ParseError.swift +++ b/slox/ParseError.swift @@ -30,6 +30,8 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { case missingCloseParenAfterArguments(Token) case missingIdentifierAfterDot(Token) case missingSuperclassName(Token) + case missingDotAfterSuper(Token) + case expectedSuperclassMethodName(Token) var description: String { switch self { @@ -77,6 +79,10 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { return "[Line \(token.line)] Error: expected identifer after dot" case .missingSuperclassName(let token): return "[Line \(token.line)] Error: expected superclass name" + case .missingDotAfterSuper(let token): + return "[Line \(token.line)] Error: expected dot after super" + case .expectedSuperclassMethodName(let token): + return "[Line \(token.line)] Error: expected superclass method name" } } } diff --git a/slox/Parser.swift b/slox/Parser.swift index 179d4d9..d507d68 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -351,7 +351,8 @@ struct Parser { // | "(" expression ")" // | "this" // | IDENTIFIER - // | lambda ; + // | lambda + // | "super" "." IDENTIFIER ; // lambda → "fun" "(" parameters? ")" block ; // mutating private func parseExpression() throws -> Expression { @@ -519,6 +520,21 @@ struct Parser { throw ParseError.missingClosingParenthesis(currentToken) } + if currentTokenMatchesAny(types: [.super]) { + let superToken = previousToken + if !currentTokenMatchesAny(types: [.dot]) { + throw ParseError.missingDotAfterSuper(currentToken) + } + + guard case .identifier = currentToken.type else { + throw ParseError.expectedSuperclassMethodName(currentToken) + } + let methodToken = currentToken + advanceCursor() + + return .super(superToken, methodToken) + } + if currentTokenMatchesAny(types: [.this]) { return .this(previousToken) } diff --git a/slox/ResolvedExpression.swift b/slox/ResolvedExpression.swift index d8d63d7..cd7e085 100644 --- a/slox/ResolvedExpression.swift +++ b/slox/ResolvedExpression.swift @@ -18,4 +18,5 @@ indirect enum ResolvedExpression: Equatable { case get(ResolvedExpression, Token) case set(ResolvedExpression, Token, ResolvedExpression) case this(Token, Int) + case `super`(Token, Token, Int) } diff --git a/slox/Resolver.swift b/slox/Resolver.swift index 5b8f12c..8a609ff 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -101,6 +101,16 @@ struct Resolver { resolvedSuperclassExpr = try handleVariable(nameToken: superclassName) } + if resolvedSuperclassExpr != nil { + beginScope() + scopeStack.lastMutable["super"] = true + } + defer { + if resolvedSuperclassExpr != nil { + endScope() + } + } + beginScope() let previousClassType = currentClassType currentClassType = .class @@ -121,10 +131,9 @@ struct Resolver { } else { .method } - return try handleFunctionDeclaration( - nameToken: nameToken, - lambdaExpr: lambdaExpr, - functionType: functionType) + return try handleFunctionDeclaration(nameToken: nameToken, + lambdaExpr: lambdaExpr, + functionType: functionType) } let resolvedStaticMethods = try staticMethods.map { method in @@ -136,10 +145,9 @@ struct Resolver { throw ResolverError.staticInitsNotAllowed } - return try handleFunctionDeclaration( - nameToken: nameToken, - lambdaExpr: lambdaExpr, - functionType: .method) + return try handleFunctionDeclaration(nameToken: nameToken, + lambdaExpr: lambdaExpr, + functionType: .method) } return .class(nameToken, resolvedSuperclassExpr, resolvedMethods, resolvedStaticMethods) @@ -242,6 +250,8 @@ struct Resolver { return try handleLogical(leftExpr: leftExpr, operToken: operToken, rightExpr: rightExpr) case .lambda(let params, let statements): return try handleLambda(params: params, statements: statements, functionType: .lambda) + case .super(let superToken, let methodToken): + return try handleSuper(superToken: superToken, methodToken: methodToken) } } @@ -349,6 +359,11 @@ struct Resolver { return .lambda(params, resolvedStatements) } + mutating private func handleSuper(superToken: Token, methodToken: Token) throws -> ResolvedExpression { + let depth = getDepth(name: superToken.lexeme) + return .super(superToken, methodToken, depth) + } + // Internal helpers mutating private func beginScope() { scopeStack.append([:]) diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index cbd6110..58eef1c 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -16,6 +16,7 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case undefinedVariable(String) case notAFunctionDeclaration case notACallableObject + case notAnInstance case onlyInstancesHaveProperties case undefinedProperty(String) case wrongArity(Int, Int) @@ -40,7 +41,9 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case .notAFunctionDeclaration: return "Error: expected function declaration in class" case .notACallableObject: - return "Error: can only callable objects" + return "Error: expected a callable object" + case .notAnInstance: + return "Error: expected an instance" case .onlyInstancesHaveProperties: return "Error: can only get/set properties of instances" case .undefinedProperty(let name): diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 03ebdd0..c79becc 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -979,4 +979,141 @@ final class InterpreterTests: XCTestCase { let expected: LoxValue = .number(5) XCTAssertEqual(actual, expected) } + + func testInterpretCallToMethodOnSuperclass() throws { + // class A { + // getTheAnswer() { + // return 42; + // } + // } + // class B < A {} + // var b = B(); + // b.getTheAnswer() + let statements: [ResolvedStatement] = [ + .class( + Token(type: .identifier, lexeme: "A", line: 1), + nil, + [ + .function( + Token(type: .identifier, lexeme: "getTheAnswer", line: 2), + .lambda( + [], + [ + .return( + Token(type: .return, lexeme: "return", line: 3), + .literal(.number(42))) + ])) + ], + []), + .class( + Token(type: .identifier, lexeme: "B", line: 6), + .variable( + Token(type: .identifier, lexeme: "A", line: 6), + 0), + [], + []), + .variableDeclaration( + Token(type: .identifier, lexeme: "b", line: 7), + .call( + .variable( + Token(type: .identifier, lexeme: "B", line: 7), + 0), + Token(type: .rightParen, lexeme: ")", line: 7), + [])), + .expression( + .call( + .get( + .variable( + Token(type: .identifier, lexeme: "b", line: 8), + 0), + Token(type: .identifier, lexeme: "getTheAnswer", line: 8)), + Token(type: .rightParen, lexeme: ")", line: 8), + [])) + ] + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(statements: statements) + let expected: LoxValue = .number(42) + XCTAssertEqual(actual, expected) + } + + func testInterpretCallingMethodInSuperclassResolvesProperly() throws { + // class A { + // method() { + // return 21; + // } + // } + // class B < A { + // method() { + // return 2*super.someMethod(); + // } + // } + // var b = B(); + // b.method() + let statements: [ResolvedStatement] = [ + .class( + Token(type: .identifier, lexeme: "A", line: 1), + nil, + [ + .function( + Token(type: .identifier, lexeme: "method", line: 2), + .lambda( + [], + [ + .return( + Token(type: .return, lexeme: "return", line: 3), + .literal(.number(21))) + ])) + ], + []), + .class( + Token(type: .identifier, lexeme: "B", line: 6), + .variable( + Token(type: .identifier, lexeme: "A", line: 6), + 0), + [ + .function( + Token(type: .identifier, lexeme: "method", line: 7), + .lambda( + [], + [ + .return( + Token(type: .return, lexeme: "return", line: 8), + .binary( + .literal(.number(2)), + Token(type: .star, lexeme: "*", line: 8), + .call( + .super( + Token(type: .super, lexeme: "super", line: 8), + Token(type: .identifier, lexeme: "method", line: 8), + 2), + Token(type: .rightParen, lexeme: ")", line: 8), + []))), + ])) + ], + []), + .variableDeclaration( + Token(type: .identifier, lexeme: "b", line: 9), + .call( + .variable( + Token(type: .identifier, lexeme: "B", line: 9), + 0), + Token(type: .rightParen, lexeme: ")", line: 9), + [])), + .expression( + .call( + .get( + .variable( + Token(type: .identifier, lexeme: "b", line: 10), + 0), + Token(type: .identifier, lexeme: "method", line: 10)), + Token(type: .rightParen, lexeme: ")", line: 10), + [])), + ] + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(statements: statements) + let expected: LoxValue = .number(42) + XCTAssertEqual(actual, expected) + } } diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index cdfe5c5..62a6e13 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -1107,4 +1107,69 @@ final class ParserTests: XCTestCase { ] XCTAssertEqual(actual, expected) } + + func testParseClassThatInheritsFromAnotherAndCallsSuper() throws { + // class B < A { + // someMethod(arg) { + // return super.someMethod(arg); + // } + // } + let tokens: [Token] = [ + Token(type: .class, lexeme: "class", line: 1), + Token(type: .identifier, lexeme: "B", line: 1), + Token(type: .less, lexeme: "<", line: 1), + Token(type: .identifier, lexeme: "A", line: 1), + Token(type: .leftBrace, lexeme: "{", line: 1), + + Token(type: .identifier, lexeme: "someMethod", line: 2), + Token(type: .leftParen, lexeme: "(", line: 2), + Token(type: .identifier, lexeme: "arg", line: 2), + Token(type: .rightParen, lexeme: ")", line: 2), + Token(type: .leftBrace, lexeme: "{", line: 2), + + Token(type: .return, lexeme: "return", line: 3), + Token(type: .super, lexeme: "super", line: 3), + Token(type: .dot, lexeme: ".", line: 3), + Token(type: .identifier, lexeme: "someMethod", line: 3), + Token(type: .leftParen, lexeme: "(", line: 3), + Token(type: .identifier, lexeme: "arg", line: 3), + Token(type: .rightParen, lexeme: ")", 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: "B", line: 1), + .variable(Token(type: .identifier, lexeme: "A", line: 1)), + [ + .function( + Token(type: .identifier, lexeme: "someMethod", line: 2), + .lambda( + [ + Token(type: .identifier, lexeme: "arg", line: 2) + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 3), + .call( + .super( + Token(type: .super, lexeme: "super", line: 3), + Token(type: .identifier, lexeme: "someMethod", line: 3)), + Token(type: .rightParen, lexeme: ")", line: 3), + [ + .variable(Token(type: .identifier, lexeme: "arg", line: 3)) + ])) + ])) + ], + []), + ] + XCTAssertEqual(actual, expected) + } } diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index a39ccb1..802492b 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -198,6 +198,11 @@ final class ResolverTests: XCTestCase { } func testResolveClassDeclaration() throws { + // class Person { + // sayName() { + // print this.name; + // } + // } let statements: [Statement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), @@ -297,6 +302,11 @@ final class ResolverTests: XCTestCase { } func testResolveClassWithStaticMethod() throws { + // class Math { + // class add(a, b) { + // return a + b; + // } + // } let statements: [Statement] = [ .class( Token(type: .identifier, lexeme: "Math", line: 1), From f3c85e7344023601f607bf121901c35c28d86478 Mon Sep 17 00:00:00 2001 From: quephird Date: Mon, 11 Mar 2024 22:29:55 -0700 Subject: [PATCH 5/6] Added some checks in the resolver and supporting tests. --- slox/Resolver.swift | 21 ++++++++++++-- slox/ResolverError.swift | 6 ++++ sloxTests/ResolverTests.swift | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/slox/Resolver.swift b/slox/Resolver.swift index 8a609ff..2331dbb 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -17,6 +17,7 @@ struct Resolver { private enum ClassType { case none case `class` + case subclass } private var scopeStack: [[String: Bool]] = [] @@ -87,6 +88,12 @@ struct Resolver { superclassExpr: Expression?, methods: [Statement], staticMethods: [Statement]) throws -> ResolvedStatement { + let previousClassType = currentClassType + currentClassType = .class + defer { + currentClassType = previousClassType + } + try declareVariable(name: nameToken.lexeme) defineVariable(name: nameToken.lexeme) @@ -94,6 +101,8 @@ struct Resolver { // pushing `this` onto the stack, otherwise we won't find it! var resolvedSuperclassExpr: ResolvedExpression? = nil if case .variable(let superclassName) = superclassExpr { + currentClassType = .subclass + if superclassName.lexeme == nameToken.lexeme { throw ResolverError.classCannotInheritFromItself } @@ -112,11 +121,8 @@ struct Resolver { } beginScope() - let previousClassType = currentClassType - currentClassType = .class defer { endScope() - currentClassType = previousClassType } // NOTA BENE: Note that the scope stack is never empty at this point scopeStack.lastMutable["this"] = true @@ -360,6 +366,15 @@ struct Resolver { } mutating private func handleSuper(superToken: Token, methodToken: Token) throws -> ResolvedExpression { + switch currentClassType { + case .none: + throw ResolverError.cannotReferenceSuperOutsideClass + case .class: + throw ResolverError.cannotReferenceSuperWithoutSubclassing + case .subclass: + break + } + let depth = getDepth(name: superToken.lexeme) return .super(superToken, methodToken, depth) } diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift index 24111ef..93081a7 100644 --- a/slox/ResolverError.swift +++ b/slox/ResolverError.swift @@ -16,6 +16,8 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { case cannotReturnValueFromInitializer case staticInitsNotAllowed case classCannotInheritFromItself + case cannotReferenceSuperOutsideClass + case cannotReferenceSuperWithoutSubclassing var description: String { switch self { @@ -35,6 +37,10 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { return "Cannot have class-level init function" case .classCannotInheritFromItself: return "Class cannot inherit from itself" + case .cannotReferenceSuperOutsideClass: + return "Cannot use `super` from outside a class" + case .cannotReferenceSuperWithoutSubclassing: + return "Cannot use `super` without subclassing" } } } diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index 802492b..53da95a 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -395,4 +395,56 @@ final class ResolverTests: XCTestCase { XCTAssertEqual(actualError as! ResolverError, expectedError) } } + + func testResolveInvocationOfSuperAtTopLevel() throws { + // super.someMethod() + let statements: [Statement] = [ + .expression( + .super( + Token(type: .super, lexeme: "super", line: 1), + Token(type: .identifier, lexeme: "someMethod", line: 1))) + ] + + var resolver = Resolver() + let expectedError = ResolverError.cannotReferenceSuperOutsideClass + XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in + XCTAssertEqual(actualError as! ResolverError, expectedError) + } + } + + func testResolveInvocationOfSuperFromWithinClassThatDoesNotSubclassAnother() throws { + // class A { + // someMethod() { + // super.someMethod(); + // } + // } + let statements: [Statement] = [ + .class( + Token(type: .identifier, lexeme: "A", line: 1), + nil, + [ + .function( + Token(type: .identifier, lexeme: "someMethod", line: 2), + .lambda( + [], + [ + .expression( + .call( + .super( + Token(type: .super, lexeme: "super", line: 3), + Token(type: .identifier, lexeme: "someMethod", line: 3)), + Token(type: .rightParen, lexeme: ")", line: 3), + []) + ) + ])) + ], + []) + ] + + var resolver = Resolver() + let expectedError = ResolverError.cannotReferenceSuperWithoutSubclassing + XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in + XCTAssertEqual(actualError as! ResolverError, expectedError) + } + } } From 043ceee4c0de2513fe9d4a7ba7ff886487b61779 Mon Sep 17 00:00:00 2001 From: quephird Date: Tue, 12 Mar 2024 11:57:30 -0700 Subject: [PATCH 6/6] Updated README. --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fb46ccc..b27928d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,10 @@ So far, the following have been implemented in `slox`: - Lambda expressions - Class declaration and instantiation - Instance properties and methods +- Referencing the scoped instance via `this` +- Class-level properties and methods +- Single inheritance +- Invoking superclass methods via `super` # Design @@ -60,7 +64,7 @@ Instead of maintaining set of native functions in `Interpreter`'s constructor, t # 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. +This repository contains a fairly comprehensive suite of unit tests that exercise the scanner, parser, resolver, and interpreter; to run them, hit ⌘-U from within Xcode. # Relevant links