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 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 230397c..466acdf 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,17 @@ 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 + } + + environment = Environment(enclosingEnvironment: environment); + environment.define(name: "super", value: .instance(superclass)); + + return superclass + } + var methodImpls: [String: UserDefinedFunction] = [:] for method in methods { guard case .function(let nameToken, let lambdaExpr) = method else { @@ -125,13 +140,21 @@ 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` 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) } @@ -217,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) } } @@ -386,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/LoxClass.swift b/slox/LoxClass.swift index 7beb58f..5952915 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) @@ -27,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 d5d9f0e..8533a04 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! } @@ -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) } diff --git a/slox/ParseError.swift b/slox/ParseError.swift index af6d492..03d696d 100644 --- a/slox/ParseError.swift +++ b/slox/ParseError.swift @@ -29,6 +29,9 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { case missingOpenBraceBeforeFunctionBody(Token) case missingCloseParenAfterArguments(Token) case missingIdentifierAfterDot(Token) + case missingSuperclassName(Token) + case missingDotAfterSuper(Token) + case expectedSuperclassMethodName(Token) var description: String { switch self { @@ -74,6 +77,12 @@ 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" + 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 8326a86..d507d68 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) @@ -340,7 +351,8 @@ struct Parser { // | "(" expression ")" // | "this" // | IDENTIFIER - // | lambda ; + // | lambda + // | "super" "." IDENTIFIER ; // lambda → "fun" "(" parameters? ")" block ; // mutating private func parseExpression() throws -> Expression { @@ -508,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/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..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]] = [] @@ -38,8 +39,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 +85,47 @@ struct Resolver { } mutating private func handleClassDeclaration(nameToken: Token, + 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) + // 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 { + currentClassType = .subclass + + if superclassName.lexeme == nameToken.lexeme { + throw ResolverError.classCannotInheritFromItself + } + + resolvedSuperclassExpr = try handleVariable(nameToken: superclassName) + } + + if resolvedSuperclassExpr != nil { + beginScope() + scopeStack.lastMutable["super"] = true + } + defer { + if resolvedSuperclassExpr != nil { + endScope() + } + } + beginScope() - // NOTA BENE: Note that the scope stack is never empty at this point - scopeStack.lastMutable["this"] = true 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 { @@ -107,10 +137,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 @@ -122,13 +151,12 @@ 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, resolvedMethods, resolvedStaticMethods) + return .class(nameToken, resolvedSuperclassExpr, resolvedMethods, resolvedStaticMethods) } mutating private func handleFunctionDeclaration(nameToken: Token, @@ -228,6 +256,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) } } @@ -335,6 +365,20 @@ struct Resolver { return .lambda(params, resolvedStatements) } + 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) + } + // Internal helpers mutating private func beginScope() { scopeStack.append([:]) diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift index fba2e49..93081a7 100644 --- a/slox/ResolverError.swift +++ b/slox/ResolverError.swift @@ -15,6 +15,9 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { case cannotReferenceThisOutsideClass case cannotReturnValueFromInitializer case staticInitsNotAllowed + case classCannotInheritFromItself + case cannotReferenceSuperOutsideClass + case cannotReferenceSuperWithoutSubclassing var description: String { switch self { @@ -32,6 +35,12 @@ 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" + case .cannotReferenceSuperOutsideClass: + return "Cannot use `super` from outside a class" + case .cannotReferenceSuperWithoutSubclassing: + return "Cannot use `super` without subclassing" } } } diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index 5cc41e1..58eef1c 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -16,11 +16,13 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case undefinedVariable(String) case notAFunctionDeclaration case notACallableObject + case notAnInstance case onlyInstancesHaveProperties case undefinedProperty(String) case wrongArity(Int, Int) case notALambda case couldNotFindAncestorEnvironmentAtDepth(Int) + case superclassMustBeAClass var description: String { switch self { @@ -39,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): @@ -50,6 +54,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]) } diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 0476cfb..c79becc 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( @@ -972,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 18e90d0..62a6e13 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( @@ -1105,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 e031194..53da95a 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -198,9 +198,15 @@ final class ResolverTests: XCTestCase { } func testResolveClassDeclaration() throws { + // class Person { + // sayName() { + // print this.name; + // } + // } let statements: [Statement] = [ .class( Token(type: .identifier, lexeme: "Person", line: 1), + nil, [ .function( Token(type: .identifier, lexeme: "sayName", line: 2), @@ -221,6 +227,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 +279,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), @@ -294,9 +302,15 @@ 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), + nil, [], [ .function( @@ -322,6 +336,7 @@ final class ResolverTests: XCTestCase { let expected: [ResolvedStatement] = [ .class( Token(type: .identifier, lexeme: "Math", line: 1), + nil, [], [ .function( @@ -357,6 +372,7 @@ final class ResolverTests: XCTestCase { let statements: [Statement] = [ .class( Token(type: .identifier, lexeme: "Math", line: 1), + nil, [], [ .function( @@ -379,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) + } + } }