diff --git a/TestPlan.xctestplan b/TestPlan.xctestplan new file mode 100644 index 0000000..50e3994 --- /dev/null +++ b/TestPlan.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "B46B5910-702E-49A1-AC55-98D04F278EE5", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:slox.xcodeproj", + "identifier" : "876A31532B8979BD0085A350", + "name" : "sloxTests" + } + } + ], + "version" : 1 +} diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index fbf92bd..b5f5b8d 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -7,8 +7,21 @@ objects = { /* Begin PBXBuildFile section */ + 870230752B9571490056FE57 /* MutableCollection+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870230742B9571490056FE57 /* MutableCollection+Extension.swift */; }; + 870230772B9575250056FE57 /* ResolverError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283972B9523AD00E49035 /* ResolverError.swift */; }; + 870230782B95752C0056FE57 /* FunctionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283992B956A2F00E49035 /* FunctionType.swift */; }; + 870230792B9575420056FE57 /* MutableCollection+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 870230742B9571490056FE57 /* MutableCollection+Extension.swift */; }; + 8702307A2B9575460056FE57 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283952B95127100E49035 /* Resolver.swift */; }; + 8702307B2B95755E0056FE57 /* ResolvedExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283912B95118A00E49035 /* ResolvedExpression.swift */; }; + 8702307C2B9575610056FE57 /* ResolvedStatement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283932B95122800E49035 /* ResolvedStatement.swift */; }; + 8702307E2B95AA2A0056FE57 /* ResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8702307D2B95AA2A0056FE57 /* ResolverTests.swift */; }; 8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732838E2B93F89300E49035 /* NativeFunction.swift */; }; 873283902B93FC0900E49035 /* NativeFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732838E2B93F89300E49035 /* NativeFunction.swift */; }; + 873283922B95118A00E49035 /* ResolvedExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283912B95118A00E49035 /* ResolvedExpression.swift */; }; + 873283942B95122800E49035 /* ResolvedStatement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283932B95122800E49035 /* ResolvedStatement.swift */; }; + 873283962B95127100E49035 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283952B95127100E49035 /* Resolver.swift */; }; + 873283982B9523AD00E49035 /* ResolverError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283972B9523AD00E49035 /* ResolverError.swift */; }; + 8732839A2B956A2F00E49035 /* FunctionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283992B956A2F00E49035 /* FunctionType.swift */; }; 873CCB212B8D5FAE00FC249A /* Interpreter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB202B8D5FAE00FC249A /* Interpreter.swift */; }; 873CCB232B8D617C00FC249A /* RuntimeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB222B8D617C00FC249A /* RuntimeError.swift */; }; 873CCB252B8D765D00FC249A /* Lox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB242B8D765D00FC249A /* Lox.swift */; }; @@ -58,7 +71,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 870230742B9571490056FE57 /* MutableCollection+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MutableCollection+Extension.swift"; sourceTree = ""; }; + 870230762B9574A90056FE57 /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; + 8702307D2B95AA2A0056FE57 /* ResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolverTests.swift; sourceTree = ""; }; 8732838E2B93F89300E49035 /* NativeFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeFunction.swift; sourceTree = ""; }; + 873283912B95118A00E49035 /* ResolvedExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolvedExpression.swift; sourceTree = ""; }; + 873283932B95122800E49035 /* ResolvedStatement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolvedStatement.swift; sourceTree = ""; }; + 873283952B95127100E49035 /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = ""; }; + 873283972B9523AD00E49035 /* ResolverError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolverError.swift; sourceTree = ""; }; + 873283992B956A2F00E49035 /* FunctionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionType.swift; sourceTree = ""; }; 873CCB202B8D5FAE00FC249A /* Interpreter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interpreter.swift; sourceTree = ""; }; 873CCB222B8D617C00FC249A /* RuntimeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeError.swift; sourceTree = ""; }; 873CCB242B8D765D00FC249A /* Lox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lox.swift; sourceTree = ""; }; @@ -105,6 +126,7 @@ children = ( 87655FFA2B88210A002BDE42 /* slox */, 876A31552B8979BD0085A350 /* sloxTests */, + 870230762B9574A90056FE57 /* TestPlan.xctestplan */, 87655FF92B88210A002BDE42 /* Products */, ); sourceTree = ""; @@ -123,13 +145,19 @@ children = ( 873CCB322B8ED8B900FC249A /* Environment.swift */, 876A31642B8C04990085A350 /* Expression.swift */, + 873283992B956A2F00E49035 /* FunctionType.swift */, 873CCB202B8D5FAE00FC249A /* Interpreter.swift */, 873CCB242B8D765D00FC249A /* Lox.swift */, 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */, 876A315E2B897EEB0085A350 /* LoxValue.swift */, + 870230742B9571490056FE57 /* MutableCollection+Extension.swift */, 8732838E2B93F89300E49035 /* NativeFunction.swift */, 876A31682B8C3AAB0085A350 /* ParseError.swift */, 876A31662B8C11810085A350 /* Parser.swift */, + 873283912B95118A00E49035 /* ResolvedExpression.swift */, + 873283932B95122800E49035 /* ResolvedStatement.swift */, + 873283952B95127100E49035 /* Resolver.swift */, + 873283972B9523AD00E49035 /* ResolverError.swift */, 877168C12B91A9BD00723543 /* Return.swift */, 873CCB222B8D617C00FC249A /* RuntimeError.swift */, 876A31612B8986630085A350 /* ScanError.swift */, @@ -147,6 +175,7 @@ children = ( 876A31562B8979BD0085A350 /* ScannerTests.swift */, 873CCB262B8E7AD100FC249A /* ParserTests.swift */, + 8702307D2B95AA2A0056FE57 /* ResolverTests.swift */, 873CCB2B2B8E88A500FC249A /* InterpreterTests.swift */, ); path = sloxTests; @@ -243,20 +272,26 @@ files = ( 876A31622B8986630085A350 /* ScanError.swift in Sources */, 876560052B8825AC002BDE42 /* Token.swift in Sources */, + 873283942B95122800E49035 /* ResolvedStatement.swift in Sources */, 876A315F2B897EEB0085A350 /* LoxValue.swift in Sources */, + 873283982B9523AD00E49035 /* ResolverError.swift in Sources */, 873CCB302B8EAEC100FC249A /* Statement.swift in Sources */, 873CCB232B8D617C00FC249A /* RuntimeError.swift in Sources */, 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */, 873CCB212B8D5FAE00FC249A /* Interpreter.swift in Sources */, 877168C22B91A9BD00723543 /* Return.swift in Sources */, 876A31672B8C11810085A350 /* Parser.swift in Sources */, + 8732839A2B956A2F00E49035 /* FunctionType.swift in Sources */, 876560072B8827F9002BDE42 /* Scanner.swift in Sources */, 87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */, 873CCB332B8ED8B900FC249A /* Environment.swift in Sources */, 876A31652B8C04990085A350 /* Expression.swift in Sources */, 876560032B882259002BDE42 /* TokenType.swift in Sources */, + 873283962B95127100E49035 /* Resolver.swift in Sources */, 8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */, + 873283922B95118A00E49035 /* ResolvedExpression.swift in Sources */, 876A31692B8C3AAB0085A350 /* ParseError.swift in Sources */, + 870230752B9571490056FE57 /* MutableCollection+Extension.swift in Sources */, 873CCB252B8D765D00FC249A /* Lox.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -265,24 +300,31 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8702307C2B9575610056FE57 /* ResolvedStatement.swift in Sources */, 873CCB2D2B8E88C900FC249A /* Interpreter.swift in Sources */, 876A31632B8987740085A350 /* ScanError.swift in Sources */, 873CCB2C2B8E88A500FC249A /* InterpreterTests.swift in Sources */, + 8702307A2B9575460056FE57 /* Resolver.swift in Sources */, 873CCB2E2B8E88D200FC249A /* RuntimeError.swift in Sources */, 873CCB342B8EE0FF00FC249A /* Environment.swift in Sources */, 8755B8B42B91983F00530DC4 /* UserDefinedFunction.swift in Sources */, + 870230772B9575250056FE57 /* ResolverError.swift in Sources */, + 8702307E2B95AA2A0056FE57 /* ResolverTests.swift in Sources */, 876A315C2B897C000085A350 /* Token.swift in Sources */, 876A31572B8979BD0085A350 /* ScannerTests.swift in Sources */, 876A31602B89827B0085A350 /* LoxValue.swift in Sources */, + 870230782B95752C0056FE57 /* FunctionType.swift in Sources */, 873CCB312B8EBB7800FC249A /* Statement.swift in Sources */, 8755B8B52B91984C00530DC4 /* LoxCallable.swift in Sources */, 873283902B93FC0900E49035 /* NativeFunction.swift in Sources */, + 8702307B2B95755E0056FE57 /* ResolvedExpression.swift in Sources */, 87C2F3742B91C2BA00126707 /* Return.swift in Sources */, 873CCB282B8E7B6300FC249A /* Parser.swift in Sources */, 873CCB272B8E7AD100FC249A /* ParserTests.swift in Sources */, 876A315D2B897C020085A350 /* Scanner.swift in Sources */, 876A315B2B897BFD0085A350 /* TokenType.swift in Sources */, 873CCB292B8E7B6900FC249A /* ParseError.swift in Sources */, + 870230792B9575420056FE57 /* MutableCollection+Extension.swift in Sources */, 873CCB2A2B8E7BB600FC249A /* Expression.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/slox/Environment.swift b/slox/Environment.swift index 43363ed..0de7dc2 100644 --- a/slox/Environment.swift +++ b/slox/Environment.swift @@ -17,30 +17,41 @@ class Environment: Equatable { values[name] = value } - func assign(name: String, value: LoxValue) throws { - if values.keys.contains(name) { - values[name] = value - return - } + func assignAtDepth(name: String, value: LoxValue, depth: Int) throws { + let ancestor = try ancestor(depth: depth) - if let enclosingEnvironment = enclosingEnvironment { - try enclosingEnvironment.assign(name: name, value: value) + if ancestor.values.keys.contains(name) { + ancestor.values[name] = value return } throw RuntimeError.undefinedVariable(name) } - func getValue(name: String) throws -> LoxValue { - if let value = values[name] { + func getValueAtDepth(name: String, depth: Int) throws -> LoxValue { + let ancestor = try ancestor(depth: depth) + + if let value = ancestor.values[name] { return value } - if let enclosingEnvironment = enclosingEnvironment { - return try enclosingEnvironment.getValue(name: name) + throw RuntimeError.undefinedVariable(name) + } + + private func ancestor(depth: Int) throws -> Environment { + var i = 0 + var ancestor: Environment = self + while i < depth { + guard let parent = ancestor.enclosingEnvironment else { + // NOTA BENE: This should not happen but it _is_ possible + throw RuntimeError.couldNotFindAncestorEnvironmentAtDepth(depth) + } + + ancestor = parent + i = i + 1 } - throw RuntimeError.undefinedVariable(name) + return ancestor } static func == (lhs: Environment, rhs: Environment) -> Bool { diff --git a/slox/FunctionType.swift b/slox/FunctionType.swift new file mode 100644 index 0000000..b3b5f98 --- /dev/null +++ b/slox/FunctionType.swift @@ -0,0 +1,12 @@ +// +// FunctionType.swift +// slox +// +// Created by Danielle Kefford on 3/3/24. +// + +enum FunctionType { + case none + case function + case lambda +} diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 4c3ab91..67e9ab3 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -21,13 +21,13 @@ class Interpreter { } } - func interpret(statements: [Statement]) throws { + func interpret(statements: [ResolvedStatement]) throws { for statement in statements { try execute(statement: statement) } } - func interpretRepl(statements: [Statement]) throws -> LoxValue? { + func interpretRepl(statements: [ResolvedStatement]) throws -> LoxValue? { for (i, statement) in statements.enumerated() { if i == statements.endIndex-1, case .expression(let expr) = statement { return try evaluate(expr: expr) @@ -39,7 +39,7 @@ class Interpreter { return nil } - private func execute(statement: Statement) throws { + private func execute(statement: ResolvedStatement) throws { switch statement { case .expression(let expr): let _ = try evaluate(expr: expr) @@ -63,9 +63,9 @@ class Interpreter { } } - private func handleIfStatement(testExpr: Expression, - consequentStmt: Statement, - alternativeStmt: Statement?) throws { + private func handleIfStatement(testExpr: ResolvedExpression, + consequentStmt: ResolvedStatement, + alternativeStmt: ResolvedStatement?) throws { if isTruthy(value: try evaluate(expr: testExpr)) { try execute(statement: consequentStmt) } else if let alternativeStmt { @@ -73,12 +73,12 @@ class Interpreter { } } - private func handlePrintStatement(expr: Expression) throws { + private func handlePrintStatement(expr: ResolvedExpression) throws { let literal = try evaluate(expr: expr) print(literal) } - private func handleFunctionDeclaration(name: Token, lambda: Expression) throws { + private func handleFunctionDeclaration(name: Token, lambda: ResolvedExpression) throws { guard case .lambda(let params, let body) = lambda else { throw RuntimeError.notALambda } @@ -91,7 +91,7 @@ class Interpreter { environment.define(name: name.lexeme, value: .userDefinedFunction(function)) } - private func handleReturnStatement(returnToken: Token, expr: Expression?) throws { + private func handleReturnStatement(returnToken: Token, expr: ResolvedExpression?) throws { var value: LoxValue = .nil if let expr { value = try evaluate(expr: expr) @@ -100,7 +100,7 @@ class Interpreter { throw Return.return(value) } - private func handleVariableDeclaration(name: Token, expr: Expression?) throws { + private func handleVariableDeclaration(name: Token, expr: ResolvedExpression?) throws { var value: LoxValue = .nil if let expr = expr { value = try evaluate(expr: expr) @@ -109,7 +109,7 @@ class Interpreter { environment.define(name: name.lexeme, value: value) } - func handleBlock(statements: [Statement], environment: Environment) throws { + func handleBlock(statements: [ResolvedStatement], environment: Environment) throws { let environmentBeforeBlock = self.environment self.environment = environment @@ -125,13 +125,13 @@ class Interpreter { } } - private func handleWhileStatement(expr: Expression, stmt: Statement) throws { + private func handleWhileStatement(expr: ResolvedExpression, stmt: ResolvedStatement) throws { while isTruthy(value: try evaluate(expr: expr)) { try execute(statement: stmt) } } - private func evaluate(expr: Expression) throws -> LoxValue { + private func evaluate(expr: ResolvedExpression) throws -> LoxValue { switch expr { case .literal(let literal): return literal @@ -141,10 +141,10 @@ class Interpreter { return try handleUnaryExpression(oper: oper, expr: expr) case .binary(let leftExpr, let oper, let rightExpr): return try handleBinaryExpression(leftExpr: leftExpr, oper: oper, rightExpr: rightExpr) - case .variable(let varToken): - return try environment.getValue(name: varToken.lexeme) - case .assignment(let varToken, let valueExpr): - return try handleAssignmentExpression(name: varToken, expr: valueExpr) + case .variable(let varToken, let depth): + return try handleVariableExpression(varToken: varToken, depth: depth) + case .assignment(let varToken, let valueExpr, let depth): + return try handleAssignmentExpression(name: varToken, expr: valueExpr, depth: depth) case .logical(let leftExpr, let oper, let rightExpr): return try handleLogicalExpression(leftExpr: leftExpr, oper: oper, rightExpr: rightExpr) case .call(let calleeExpr, let rightParen, let args): @@ -154,7 +154,7 @@ class Interpreter { } } - private func handleUnaryExpression(oper: Token, expr: Expression) throws -> LoxValue { + private func handleUnaryExpression(oper: Token, expr: ResolvedExpression) throws -> LoxValue { let value = try evaluate(expr: expr) switch oper.type { @@ -171,9 +171,9 @@ class Interpreter { } } - private func handleBinaryExpression(leftExpr: Expression, + private func handleBinaryExpression(leftExpr: ResolvedExpression, oper: Token, - rightExpr: Expression) throws -> LoxValue { + rightExpr: ResolvedExpression) throws -> LoxValue { let leftValue = try evaluate(expr: leftExpr) let rightValue = try evaluate(expr: rightExpr) @@ -221,15 +221,21 @@ class Interpreter { } } - private func handleAssignmentExpression(name: Token, expr: Expression) throws -> LoxValue { + private func handleVariableExpression(varToken: Token, depth: Int) throws -> LoxValue { + return try environment.getValueAtDepth(name: varToken.lexeme, depth: depth) + } + + private func handleAssignmentExpression(name: Token, + expr: ResolvedExpression, + depth: Int) throws -> LoxValue { let value = try evaluate(expr: expr) - try environment.assign(name: name.lexeme, value: value) + try environment.assignAtDepth(name: name.lexeme, value: value, depth: depth) return value } - private func handleLogicalExpression(leftExpr: Expression, + private func handleLogicalExpression(leftExpr: ResolvedExpression, oper: Token, - rightExpr: Expression) throws -> LoxValue { + rightExpr: ResolvedExpression) throws -> LoxValue { let leftValue = try evaluate(expr: leftExpr) if case .and = oper.type { @@ -247,9 +253,9 @@ class Interpreter { } } - private func handleFunctionCallExpression(calleeExpr: Expression, + private func handleFunctionCallExpression(calleeExpr: ResolvedExpression, rightParen: Token, - args: [Expression]) throws -> LoxValue { + args: [ResolvedExpression]) throws -> LoxValue { let callee = try evaluate(expr: calleeExpr) let actualFunction: LoxCallable = switch callee { @@ -274,7 +280,7 @@ class Interpreter { return try actualFunction.call(interpreter: self, args: argValues) } - private func handleLambdaExpression(params: [Token], statements: [Statement]) throws -> LoxValue { + private func handleLambdaExpression(params: [Token], statements: [ResolvedStatement]) throws -> LoxValue { let environmentWhenDeclared = self.environment let function = UserDefinedFunction(name: "", diff --git a/slox/Lox.swift b/slox/Lox.swift index 6d9e4e1..59da676 100644 --- a/slox/Lox.swift +++ b/slox/Lox.swift @@ -46,7 +46,9 @@ struct Lox { let tokens = try scanner.scanTokens() var parser = Parser(tokens: tokens) let statements = try parser.parse() - if let result = try interpreter.interpretRepl(statements: statements) { + var resolver = Resolver() + let resolvedStatements = try resolver.resolve(statements: statements) + if let result = try interpreter.interpretRepl(statements: resolvedStatements) { print(result) } } catch { @@ -62,6 +64,8 @@ struct Lox { let tokens = try scanner.scanTokens() var parser = Parser(tokens: tokens) let statements = try parser.parse() - try interpreter.interpret(statements: statements) + var resolver = Resolver() + let resolvedStatements = try resolver.resolve(statements: statements) + try interpreter.interpret(statements: resolvedStatements) } } diff --git a/slox/MutableCollection+Extension.swift b/slox/MutableCollection+Extension.swift new file mode 100644 index 0000000..1d57da2 --- /dev/null +++ b/slox/MutableCollection+Extension.swift @@ -0,0 +1,22 @@ +// +// MutableCollection+Extension.swift +// slox +// +// Created by Danielle Kefford on 3/3/24. +// + +extension MutableCollection where Self: BidirectionalCollection { + // Accesses the last element of the collection, mutably. + // + // - Precondition: Collection is not empty. + var lastMutable: Element { + get { + precondition(!isEmpty) + return self[index(before: endIndex)] + } + set { + precondition(!isEmpty) + self[index(before: endIndex)] = newValue + } + } +} diff --git a/slox/ResolvedExpression.swift b/slox/ResolvedExpression.swift new file mode 100644 index 0000000..c432ffa --- /dev/null +++ b/slox/ResolvedExpression.swift @@ -0,0 +1,18 @@ +// +// ResolvedExpression.swift +// slox +// +// Created by Danielle Kefford on 3/3/24. +// + +indirect enum ResolvedExpression: Equatable { + case binary(ResolvedExpression, Token, ResolvedExpression) + case unary(Token, ResolvedExpression) + case literal(LoxValue) + case grouping(ResolvedExpression) + case variable(Token, Int) + case assignment(Token, ResolvedExpression, Int) + case logical(ResolvedExpression, Token, ResolvedExpression) + case call(ResolvedExpression, Token, [ResolvedExpression]) + case lambda([Token], [ResolvedStatement]) +} diff --git a/slox/ResolvedStatement.swift b/slox/ResolvedStatement.swift new file mode 100644 index 0000000..379ac46 --- /dev/null +++ b/slox/ResolvedStatement.swift @@ -0,0 +1,17 @@ +// +// ResolvedStatement.swift +// slox +// +// Created by Danielle Kefford on 3/3/24. +// + +indirect enum ResolvedStatement: Equatable { + case expression(ResolvedExpression) + case `if`(ResolvedExpression, ResolvedStatement, ResolvedStatement?) + case print(ResolvedExpression) + case variableDeclaration(Token, ResolvedExpression?) + case block([ResolvedStatement]) + case `while`(ResolvedExpression, ResolvedStatement) + case function(Token, ResolvedExpression) + case `return`(Token, ResolvedExpression?) +} diff --git a/slox/Resolver.swift b/slox/Resolver.swift new file mode 100644 index 0000000..f45ca07 --- /dev/null +++ b/slox/Resolver.swift @@ -0,0 +1,277 @@ +// +// Resolver.swift +// slox +// +// Created by Danielle Kefford on 3/3/24. +// + +struct Resolver { + private var scopeStack: [[String: Bool]] = [] + private var currentFunctionType: FunctionType = .none + + // Main point of entry + mutating func resolve(statements: [Statement]) throws -> [ResolvedStatement] { + let resolvedStatements = try statements.map { statement in + return try resolve(statement: statement) + } + return resolvedStatements + } + + // Resolver for statements + private mutating func resolve(statement: Statement) throws -> ResolvedStatement { + switch statement { + case .block(let statements): + return try handleBlock(statements: statements) + case .variableDeclaration(let nameToken, let initializeExpr): + return try handleVariableDeclaration(nameToken: nameToken, initializeExpr: initializeExpr) + case .function(let nameToken, let lambdaExpr): + return try handleFunctionDeclaration(nameToken: nameToken, lambdaExpr: lambdaExpr) + case .expression(let expr): + return try handleExpressionStatement(expr: expr) + case .if(let testExpr, let consequentStmt, let alternativeStmt): + return try handleIf(testExpr: testExpr, consequentStmt: consequentStmt, alternativeStmt: alternativeStmt) + case .print(let expr): + return try handlePrintStatement(expr: expr) + case .return(let returnToken, let expr): + return try handleReturnStatement(returnToken: returnToken, expr: expr) + case .while(let conditionExpr, let bodyStmt): + return try handleWhile(conditionExpr: conditionExpr, bodyStmt: bodyStmt) + } + } + + mutating private func handleBlock(statements: [Statement]) throws -> ResolvedStatement { + beginScope() + defer { + endScope() + } + + let resolvedStatements = try resolve(statements: statements) + + return .block(resolvedStatements) + } + + mutating private func handleVariableDeclaration(nameToken: Token, initializeExpr: Expression?) throws -> ResolvedStatement { + try declareVariable(name: nameToken.lexeme) + + var resolvedInitializerExpr: ResolvedExpression? = nil + if let initializeExpr { + resolvedInitializerExpr = try resolve(expression: initializeExpr) + } + + defineVariable(name: nameToken.lexeme) + return .variableDeclaration(nameToken, resolvedInitializerExpr) + } + + mutating private func handleFunctionDeclaration(nameToken: Token, lambdaExpr: Expression) throws -> ResolvedStatement { + guard case .lambda(let paramTokens, let statements) = lambdaExpr else { + throw ResolverError.notAFunction + } + + try declareVariable(name: nameToken.lexeme) + defineVariable(name: nameToken.lexeme) + + let resolvedLambda = try handleLambda(params: paramTokens, + statements: statements, + functionType: .function) + + return .function(nameToken, resolvedLambda) + } + + mutating private func handleExpressionStatement(expr: Expression) throws -> ResolvedStatement { + let resolvedExpression = try resolve(expression: expr) + return .expression(resolvedExpression) + } + + mutating private func handleIf(testExpr: Expression, + consequentStmt: Statement, + alternativeStmt: Statement?) throws -> ResolvedStatement { + let resolvedTestExpr = try resolve(expression: testExpr) + let resolvedConsequentStmt = try resolve(statement: consequentStmt) + + var resolvedAlternativeStmt: ResolvedStatement? = nil + if let alternativeStmt { + resolvedAlternativeStmt = try resolve(statement: alternativeStmt) + } + + return .if(resolvedTestExpr, resolvedConsequentStmt, resolvedAlternativeStmt) + } + + mutating private func handlePrintStatement(expr: Expression) throws -> ResolvedStatement { + let resolvedExpression = try resolve(expression: expr) + return .print(resolvedExpression) + } + + mutating private func handleReturnStatement(returnToken: Token, expr: Expression?) throws -> ResolvedStatement { + if currentFunctionType == .none { + throw ResolverError.cannotReturnOutsideFunction + } + + if let expr { + let resolvedExpr = try resolve(expression: expr) + return .return(returnToken, resolvedExpr) + } + + return .return(returnToken, nil) + } + + mutating private func handleWhile(conditionExpr: Expression, bodyStmt: Statement) throws -> ResolvedStatement { + let resolvedConditionExpr = try resolve(expression: conditionExpr) + let resolvedBodyStmt = try resolve(statement: bodyStmt) + + return .while(resolvedConditionExpr, resolvedBodyStmt) + } + + // Resolver for expressions + mutating private func resolve(expression: Expression) throws -> ResolvedExpression { + switch expression { + case .variable(let nameToken): + return try handleVariable(nameToken: nameToken) + case .assignment(let nameToken, let valueExpr): + return try handleAssignment(nameToken: nameToken, valueExpr: valueExpr) + case .binary(let leftExpr, let operToken, let rightExpr): + return try handleBinary(leftExpr: leftExpr, operToken: operToken, rightExpr: rightExpr) + case .unary(let operToken, let rightExpr): + return try handleUnary(operToken: operToken, rightExpr: rightExpr) + case .call(let calleeExpr, let rightParenToken, let args): + return try handleCall(calleeExpr: calleeExpr, rightParenToken: rightParenToken, args: args) + case .literal(let value): + return .literal(value) + case .grouping(let expr): + let resolvedExpr = try resolve(expression: expr) + return .grouping(resolvedExpr) + case .logical(let leftExpr, let operToken, let rightExpr): + return try handleLogical(leftExpr: leftExpr, operToken: operToken, rightExpr: rightExpr) + case .lambda(let params, let statements): + return try handleLambda(params: params, statements: statements, functionType: .lambda) + } + } + + mutating private func handleVariable(nameToken: Token) throws -> ResolvedExpression { + if !scopeStack.isEmpty && scopeStack.lastMutable[nameToken.lexeme] == false { + throw ResolverError.variableAccessedBeforeInitialization + } + + let depth = getDepth(name: nameToken.lexeme) + return .variable(nameToken, depth) + } + + mutating private func handleAssignment(nameToken: Token, valueExpr: Expression) throws -> ResolvedExpression { + let resolveValueExpr = try resolve(expression: valueExpr) + let depth = getDepth(name: nameToken.lexeme) + + return .assignment(nameToken, resolveValueExpr, depth) + } + + mutating private func handleBinary(leftExpr: Expression, + operToken: Token, + rightExpr: Expression) throws -> ResolvedExpression { + let resolvedLeftExpr = try resolve(expression: leftExpr) + let resolvedRightExpr = try resolve(expression: rightExpr) + + return .binary(resolvedLeftExpr, operToken, resolvedRightExpr) + } + + mutating private func handleUnary(operToken: Token, rightExpr: Expression) throws -> ResolvedExpression { + let resolvedRightExpr = try resolve(expression: rightExpr) + + return .unary(operToken, resolvedRightExpr) + } + + mutating private func handleCall(calleeExpr: Expression, + rightParenToken: Token, + args: [Expression]) throws -> ResolvedExpression { + let resolvedCalleeExpr = try resolve(expression: calleeExpr) + + let resolvedArgs = try args.map { arg in + try resolve(expression: arg) + } + + return .call(resolvedCalleeExpr, rightParenToken, resolvedArgs) + } + + mutating private func handleLogical(leftExpr: Expression, + operToken: Token, + rightExpr: Expression) throws -> ResolvedExpression { + let resolvedLeftExpr = try resolve(expression: leftExpr) + let resolvedRightExpr = try resolve(expression: rightExpr) + + return .logical(resolvedLeftExpr, operToken, resolvedRightExpr) + } + + mutating private func handleLambda(params: [Token], + statements: [Statement], + functionType: FunctionType) throws -> ResolvedExpression { + beginScope() + let previousFunctionType = currentFunctionType + currentFunctionType = .function + defer { + endScope() + currentFunctionType = previousFunctionType + } + + for param in params { + try declareVariable(name: param.lexeme) + defineVariable(name: param.lexeme) + } + + let resolvedStatements = try statements.map { statement in + try resolve(statement: statement) + } + + return .lambda(params, resolvedStatements) + } + + // Internal helpers + mutating private func beginScope() { + scopeStack.append([:]) + } + + mutating private func endScope() { + scopeStack.removeLast() + } + + mutating private func declareVariable(name: String) throws { + // ACHTUNG!!! Only variables declared/defined in local + // blocks are tracked by the resolver, which is why + // we bail here since the stack is empty in the + // global environment. + if scopeStack.isEmpty { + return + } + + if scopeStack.lastMutable.keys.contains(name) { + throw ResolverError.variableAlreadyDefined(name) + } + + scopeStack.lastMutable[name] = false + } + + mutating private func defineVariable(name: String) { + // ACHTUNG!!! Only variables declared/defined in local + // blocks are tracked by the resolver, which is why + // we bail here since the stack is empty in the + // global environment. + if scopeStack.isEmpty { + return + } + + scopeStack.lastMutable[name] = true + } + + private func getDepth(name: String) -> Int { + var i = scopeStack.count - 1 + while i >= 0 { + if let _ = scopeStack[i][name] { + return scopeStack.count - 1 - i + } + + i = i - 1 + } + + // If we get here, the variable must be defined + // in the global environment, and not tracked by the + // resolver, and so we return the depth required to + // fetch the value of that variable. + return scopeStack.count + } +} diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift new file mode 100644 index 0000000..d3fb451 --- /dev/null +++ b/slox/ResolverError.swift @@ -0,0 +1,28 @@ +// +// ResolverError.swift +// slox +// +// Created by Danielle Kefford on 3/3/24. +// + +import Foundation + +enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { + case variableAccessedBeforeInitialization + case notAFunction + case variableAlreadyDefined(String) + case cannotReturnOutsideFunction + + var description: String { + switch self { + case .variableAccessedBeforeInitialization: + return "Can't read local variable in its own initializer" + case .notAFunction: + return "Expected lambda as body of function declaration" + case .variableAlreadyDefined(let name): + return "Variable \(name) already defined in this scope" + case .cannotReturnOutsideFunction: + return "Cannot return from outside a function" + } + } +} diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index 61a45fe..4454a07 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -17,6 +17,7 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case notAFunction case wrongArity(Int, Int) case notALambda + case couldNotFindAncestorEnvironmentAtDepth(Int) var description: String { switch self { @@ -38,6 +39,8 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { return "Incorrect number of arguments: expected \(expected), got \(actual)" case .notALambda: return "Expected lambda as body of function declaration" + case .couldNotFindAncestorEnvironmentAtDepth(let depth): + return "Could not find ancestor environment at depth \(depth)." } } } diff --git a/slox/UserDefinedFunction.swift b/slox/UserDefinedFunction.swift index 155b91a..202a252 100644 --- a/slox/UserDefinedFunction.swift +++ b/slox/UserDefinedFunction.swift @@ -12,7 +12,7 @@ struct UserDefinedFunction: LoxCallable, Equatable { return params.count } var enclosingEnvironment: Environment - var body: [Statement] + var body: [ResolvedStatement] func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue { let newEnvironment = Environment(enclosingEnvironment: enclosingEnvironment) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 6a52117..b7f2d6c 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -9,7 +9,8 @@ import XCTest final class InterpreterTests: XCTestCase { func testInterpretStringLiteralExpression() throws { - let stmt: Statement = .expression(.literal(.string("forty-two"))) + // "forty-two" + let stmt: ResolvedStatement = .expression(.literal(.string("forty-two"))) let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt])! let expected: LoxValue = .string("forty-two") @@ -17,7 +18,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretNumericLiteralExpression() throws { - let stmt: Statement = .expression(.literal(.number(42))) + // 42 + let stmt: ResolvedStatement = .expression(.literal(.number(42))) let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt])! let expected: LoxValue = .number(42) @@ -25,7 +27,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretGroupingExpression() throws { - let stmt: Statement = .expression(.grouping(.literal(.number(42)))) + // (42) + let stmt: ResolvedStatement = .expression(.grouping(.literal(.number(42)))) let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt])! let expected: LoxValue = .number(42) @@ -33,7 +36,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretUnaryExpression() throws { - let stmt: Statement = + // !true + let stmt: ResolvedStatement = .expression( .unary( Token(type: .bang, lexeme: "!", line: 1), @@ -45,7 +49,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretInvalidUnaryExpression() throws { - let stmt: Statement = + // -"forty-two" + let stmt: ResolvedStatement = .expression( .unary( Token(type: .minus, lexeme: "-", line: 1), @@ -59,7 +64,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretNumericBinaryExpression() throws { - let stmt: Statement = + // 21 * 2 + let stmt: ResolvedStatement = .expression( .binary( .literal(.number(21)), @@ -72,7 +78,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretStringlyBinaryExpression() throws { - let stmt: Statement = + // "forty" + "-two" + let stmt: ResolvedStatement = .expression( .binary( .literal(.string("forty")), @@ -85,7 +92,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretEqualityExpression() throws { - let stmt: Statement = + // true != false + let stmt: ResolvedStatement = .expression( .binary( .literal(.boolean(true)), @@ -98,7 +106,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretInvalidBinaryExpression() throws { - let stmt: Statement = + // "twenty-one" * 2 + let stmt: ResolvedStatement = .expression( .binary( .literal(.string("twenty-one")), @@ -113,7 +122,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretComplexExpression() throws { - let stmt: Statement = + // (-2) * (3 + 4) + let stmt: ResolvedStatement = .expression( .binary( .grouping(.unary( @@ -131,7 +141,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretLogicalExpression() throws { - let stmt: Statement = + // true and false or true + let stmt: ResolvedStatement = .expression( .logical( .logical( @@ -147,7 +158,8 @@ final class InterpreterTests: XCTestCase { } func testInterpretComparisonExpression() throws { - let stmt: Statement = + // 10 < 20 + let stmt: ResolvedStatement = .expression( .binary( .literal(.number(10)), @@ -161,32 +173,38 @@ final class InterpreterTests: XCTestCase { } func testInterpretVariableDeclaration() throws { - let stmt: Statement = + // var theAnswer = 42; theAnswer + let statements: [ResolvedStatement] = [ .variableDeclaration( Token(type: .identifier, lexeme: "theAnswer", line: 1), - .literal(.number(42))) + .literal(.number(42))), + .expression( + .variable( + Token(type: .identifier, lexeme: "theAnswer", line: 1), + 0)), + ] let interpreter = Interpreter() - let _ = try interpreter.interpretRepl(statements: [stmt]) - let environment = interpreter.environment - let actual = try environment.getValue(name: "theAnswer") + let actual = try interpreter.interpretRepl(statements: statements) let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) } func testInterpretCompoundStatementInvolvingAVariable() throws { // var theAnswer; theAnswer = 42; theAnswer - let statements: [Statement] = [ + let statements: [ResolvedStatement] = [ .variableDeclaration( Token(type: .identifier, lexeme: "theAnswer", line: 1), .literal(.nil)), .expression( .assignment( Token(type: .identifier, lexeme: "theAnswer", line: 1), - .literal(.number(42)))), + .literal(.number(42)), + 0)), .expression( .variable( - Token(type: .identifier, lexeme: "theAnswer", line: 1))) + Token(type: .identifier, lexeme: "theAnswer", line: 1), + 0)) ] let interpreter = Interpreter() @@ -195,44 +213,55 @@ final class InterpreterTests: XCTestCase { XCTAssertEqual(actual, expected) } - func testInterpretWhileStatement() throws { + func testInterpretWhileStatementWithMutationOfVariable() throws { // var i = 0; // while (i < 3) { // i = i + 1; // } - let statements: [Statement] = [ + // i + let statements: [ResolvedStatement] = [ .variableDeclaration( Token(type: .identifier, lexeme: "i", line: 1), .literal(.number(0))), .while( .binary( - .variable(Token(type: .identifier, lexeme: "i", line: 2)), + .variable( + Token(type: .identifier, lexeme: "i", line: 2), + 0), Token(type: .less, lexeme: "<", line: 2), .literal(.number(3))), - .expression( - .assignment( - Token(type: .identifier, lexeme: "i", line: 3), - .binary( - .variable(Token(type: .identifier, lexeme: "i", line: 3)), - Token(type: .plus, lexeme: "+", line: 3), - .literal(.number(1)))))), + .block([ + .expression( + .assignment( + Token(type: .identifier, lexeme: "i", line: 3), + .binary( + .variable( + Token(type: .identifier, lexeme: "i", line: 3), + 1), + Token(type: .plus, lexeme: "+", line: 3), + .literal(.number(1))), + 1)) + ])), + .expression( + .variable( + Token(type: .identifier, lexeme: "i", line: 5), + 0)), ] let interpreter = Interpreter() - let _ = try interpreter.interpretRepl(statements: statements) - let environment = interpreter.environment - let actual = try environment.getValue(name: "i") + let actual = try interpreter.interpretRepl(statements: statements) let expected: LoxValue = .number(3) XCTAssertEqual(actual, expected) } - func testInterpretIfStatement() throws { - // var x; + func testInterpretIfStatementWithConditionalMutationOfVariable() throws { + // var theAnswer; // if (true) // x = 42; // else // x = 0; - let statements: [Statement] = [ + // theAnswer + let statements: [ResolvedStatement] = [ .variableDeclaration( Token(type: .identifier, lexeme: "theAnswer", line: 1), nil), @@ -241,27 +270,32 @@ final class InterpreterTests: XCTestCase { .expression( .assignment( Token(type: .identifier, lexeme: "theAnswer", line: 3), - .literal(.number(42)))), + .literal(.number(42)), + 0)), .expression( .assignment( Token(type: .identifier, lexeme: "theAnswer", line: 3), - .literal(.number(0))))), + .literal(.number(0)), + 0))), + .expression( + .variable( + Token(type: .identifier, lexeme: "theAnswer", line: 6), + 0)), ] let interpreter = Interpreter() - let _ = try interpreter.interpretRepl(statements: statements) - let environment = interpreter.environment - let actual = try environment.getValue(name: "theAnswer") + let actual = try interpreter.interpretRepl(statements: statements) let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) } - func testInterpretBlockStatement() throws { + func testInterpretBlockStatementThatMutatesVariableAtTopLevel() throws { // var theAnswer = 21 // { // theAnswer = theAnswer * 2; // } - let statements: [Statement] = [ + // theAnswer + let statements: [ResolvedStatement] = [ .variableDeclaration( Token(type: .identifier, lexeme: "theAnswer", line: 1), .literal(.number(21))), @@ -270,16 +304,21 @@ final class InterpreterTests: XCTestCase { .assignment( Token(type: .identifier, lexeme: "theAnswer", line: 3), .binary( - .variable(Token(type: .identifier, lexeme: "theAnswer", line: 3)), + .variable( + Token(type: .identifier, lexeme: "theAnswer", line: 3), + 1), Token(type: .star, lexeme: "*", line: 3), - .literal(.number(2))))), - ]) + .literal(.number(2))), + 1)), + ]), + .expression( + .variable( + Token(type: .identifier, lexeme: "theAnswer", line: 5), + 0)), ] let interpreter = Interpreter() - let _ = try interpreter.interpretRepl(statements: statements) - let environment = interpreter.environment - let actual = try environment.getValue(name: "theAnswer") + let actual = try interpreter.interpretRepl(statements: statements) let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) } @@ -289,7 +328,8 @@ final class InterpreterTests: XCTestCase { // { // var theAnswer = "forty-two"; // } - let statements: [Statement] = [ + // theAnswer + let statements: [ResolvedStatement] = [ .variableDeclaration( Token(type: .identifier, lexeme: "theAnswer", line: 1), .literal(.number(42))), @@ -297,13 +337,15 @@ final class InterpreterTests: XCTestCase { .variableDeclaration( Token(type: .identifier, lexeme: "theAnswer", line: 1), .literal(.string("forty-two"))), - ]) + ]), + .expression( + .variable( + Token(type: .identifier, lexeme: "theAnswer", line: 5), + 0)), ] let interpreter = Interpreter() - let _ = try interpreter.interpretRepl(statements: statements) - let environment = interpreter.environment - let actual = try environment.getValue(name: "theAnswer") + let actual = try interpreter.interpretRepl(statements: statements) let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) } @@ -313,7 +355,7 @@ final class InterpreterTests: XCTestCase { // return a + b; // } // add(1, 2) - let statements: [Statement] = [ + let statements: [ResolvedStatement] = [ .function( Token(type: .identifier, lexeme: "add", line: 1), .lambda( @@ -325,14 +367,20 @@ final class InterpreterTests: XCTestCase { .return( Token(type: .return, lexeme: "return", line: 2), .binary( - .variable(Token(type: .identifier, lexeme: "a", line: 2)), + .variable( + Token(type: .identifier, lexeme: "a", line: 2), + 0), Token(type: .plus, lexeme: "+", line: 2), - .variable(Token(type: .identifier, lexeme: "b", line: 2)))) + .variable( + Token(type: .identifier, lexeme: "b", line: 2), + 0))) ]) ), .expression( .call( - .variable(Token(type: .identifier, lexeme: "add", line: 4)), + .variable( + Token(type: .identifier, lexeme: "add", line: 4), + 0), Token(type: .rightParen, lexeme: ")", line: 4), [ .literal(.number(1)), @@ -353,7 +401,7 @@ final class InterpreterTests: XCTestCase { // return n * fact(n-1); // } // fact(5) - let statements: [Statement] = [ + let statements: [ResolvedStatement] = [ .function( Token(type: .identifier, lexeme: "fact", line: 1), .lambda( @@ -363,7 +411,9 @@ final class InterpreterTests: XCTestCase { [ .if( .binary( - .variable(Token(type: .identifier, lexeme: "n", line: 2)), + .variable( + Token(type: .identifier, lexeme: "n", line: 2), + 0), Token(type: .lessEqual, lexeme: "<=", line: 2), .literal(.number(1))), .return( @@ -373,21 +423,29 @@ final class InterpreterTests: XCTestCase { .return( Token(type: .return, lexeme: "return", line: 4), .binary( - .variable(Token(type: .identifier, lexeme: "n", line: 4)), + .variable( + Token(type: .identifier, lexeme: "n", line: 4), + 0), Token(type: .star, lexeme: "*", line: 4), .call( - .variable(Token(type: .identifier, lexeme: "fact", line: 4)), + .variable( + Token(type: .identifier, lexeme: "fact", line: 4), + 1), Token(type: .rightParen, lexeme: ")", line: 4), [ .binary( - .variable(Token(type: .identifier, lexeme: "n", line: 4)), + .variable( + Token(type: .identifier, lexeme: "n", line: 4), + 0), Token(type: .minus, lexeme: "-", line: 4), .literal(.number(1))) ]))) ])), .expression( .call( - .variable(Token(type: .identifier, lexeme: "fact", line: 5)), + .variable( + Token(type: .identifier, lexeme: "fact", line: 5), + 0), Token(type: .rightParen, lexeme: ")", line: 5), [.literal(.number(5)),])), ] @@ -400,7 +458,7 @@ final class InterpreterTests: XCTestCase { func testInterpretLambdaExpression() throws { // fun (a, b) { return a + b; }(2, 3) - let statements: [Statement] = [ + let statements: [ResolvedStatement] = [ .expression( .call( .lambda( @@ -412,9 +470,13 @@ final class InterpreterTests: XCTestCase { .return( Token(type: .return, lexeme: "return", line: 1), .binary( - .variable(Token(type: .identifier, lexeme: "a", line: 1)), + .variable( + Token(type: .identifier, lexeme: "a", line: 1), + 0), Token(type: .plus, lexeme: "+", line: 1), - .variable(Token(type: .identifier, lexeme: "b", line: 1)))) + .variable( + Token(type: .identifier, lexeme: "b", line: 1), + 0))) ]), Token(type: .rightParen, lexeme: ")", line: 1), [ @@ -435,7 +497,7 @@ final class InterpreterTests: XCTestCase { // } // var addTwo = makeAdder(2); // addTwo(5) - let statements: [Statement] = [ + let statements: [ResolvedStatement] = [ .function( Token(type: .identifier, lexeme: "makeAdder", line: 1), .lambda( @@ -453,22 +515,30 @@ final class InterpreterTests: XCTestCase { .return( Token(type: .return, lexeme: "return", line: 2), .binary( - .variable(Token(type: .identifier, lexeme: "n", line: 2)), + .variable( + Token(type: .identifier, lexeme: "n", line: 2), + 1), Token(type: .plus, lexeme: "+", line: 2), - .variable(Token(type: .identifier, lexeme: "a", line: 2)))) + .variable( + Token(type: .identifier, lexeme: "a", line: 2), + 0))) ])) ])), .variableDeclaration( Token(type: .identifier, lexeme: "addTwo", line: 4), .call( - .variable(Token(type: .identifier, lexeme: "makeAdder", line: 4)), + .variable( + Token(type: .identifier, lexeme: "makeAdder", line: 4), + 0), Token(type: .rightParen, lexeme: ")", line: 4), [ .literal(.number(2)) ])), .expression( .call( - .variable(Token(type: .identifier, lexeme: "addTwo", line: 5)), + .variable( + Token(type: .identifier, lexeme: "addTwo", line: 5), + 0), Token(type: .rightParen, lexeme: ")", line: 5), [.literal(.number(5))])) ] @@ -479,11 +549,11 @@ final class InterpreterTests: XCTestCase { XCTAssertEqual(actual, expected) } - func testInterpretFunctionInvocationDoesNotDisaffectGlobalEnvironment() throws { + func testInterpretVariablesReferencedInsideFunctionDeclarationDoNotLeakOut() throws { // fun add(a, b) { return a + b; } // add(2, 3) // a - let statements: [Statement] = [ + let statements: [ResolvedStatement] = [ .function( Token(type: .identifier, lexeme: "add", line: 1), .lambda( @@ -495,21 +565,29 @@ final class InterpreterTests: XCTestCase { .return( Token(type: .return, lexeme: "return", line: 1), .binary( - .variable(Token(type: .identifier, lexeme: "a", line: 1)), + .variable( + Token(type: .identifier, lexeme: "a", line: 1), + 0), Token(type: .plus, lexeme: "+", line: 1), - .variable(Token(type: .identifier, lexeme: "b", line: 1)))) + .variable( + Token(type: .identifier, lexeme: "b", line: 1), + 0))) ]) ), .expression( .call( - .variable(Token(type: .identifier, lexeme: "add", line: 2)), + .variable( + Token(type: .identifier, lexeme: "add", line: 2), + 0), Token(type: .rightParen, lexeme: ")", line: 2), [ .literal(.number(1)), .literal(.number(2)), ])), .expression( - .variable(Token(type: .identifier, lexeme: "a", line: 3))) + .variable( + Token(type: .identifier, lexeme: "a", line: 3), + 0)) ] let interpreter = Interpreter() diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift new file mode 100644 index 0000000..c3cb61a --- /dev/null +++ b/sloxTests/ResolverTests.swift @@ -0,0 +1,199 @@ +// +// ResolverTests.swift +// sloxTests +// +// Created by Danielle Kefford on 3/3/24. +// + +import XCTest + +final class ResolverTests: XCTestCase { + func testResolveLiteralExpression() throws { + // "forty-two" + let statements: [Statement] = [ + .expression( + .literal( + .string("forty-two"))), + ] + + var resolver = Resolver() + let actual = try resolver.resolve(statements: statements) + let expected: [ResolvedStatement] = [ + .expression( + .literal( + .string("forty-two"))) + ] + XCTAssertEqual(actual, expected) + } + + func testResolveVariableDeclaration() throws { + // var answer = 42; + let statements: [Statement] = [ + .variableDeclaration( + Token(type: .identifier, lexeme: "answer", line: 1), + .literal(.number(42))), + ] + + var resolver = Resolver() + let actual = try resolver.resolve(statements: statements) + let expected: [ResolvedStatement] = [ + .variableDeclaration( + Token(type: .identifier, lexeme: "answer", line: 1), + .literal(.number(42))), + ] + XCTAssertEqual(actual, expected) + } + + func testResolveFunctionDeclaration() throws { + // fun add(a, b) { + // return a + b; + // } + let statements: [Statement] = [ + .function( + Token(type: .identifier, lexeme: "add", line: 1), + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 1), + Token(type: .identifier, lexeme: "b", line: 1), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 2), + .binary( + .variable( + Token(type: .identifier, lexeme: "a", line: 2)), + Token(type: .plus, lexeme: "+", line: 2), + .variable( + Token(type: .identifier, lexeme: "b", line: 2)))), + ])), + ] + + var resolver = Resolver() + let actual = try resolver.resolve(statements: statements) + let expected: [ResolvedStatement] = [ + .function( + Token(type: .identifier, lexeme: "add", line: 1), + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 1), + Token(type: .identifier, lexeme: "b", line: 1), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 2), + .binary( + .variable( + Token(type: .identifier, lexeme: "a", line: 2), + 0), + Token(type: .plus, lexeme: "+", line: 2), + .variable( + Token(type: .identifier, lexeme: "b", line: 2), + 0))), + ])), + ] + XCTAssertEqual(actual, expected) + } + + func testResolveVariableExpressionInDeeplyNestedBlock() throws { + // var becca; {{{ becca = "awesome"; }}} + let statements: [Statement] = [ + .variableDeclaration( + Token(type: .identifier, lexeme: "becca", line: 1), + nil), + .block([ + .block([ + .block([ + .expression( + .assignment( + Token(type: .identifier, lexeme: "becca", line: 1), + .literal(.string("awesome")))) + ]) + ]) + ]) + ] + + var resolver = Resolver() + let actual = try resolver.resolve(statements: statements) + let expected: [ResolvedStatement] = [ + .variableDeclaration( + Token(type: .identifier, lexeme: "becca", line: 1), + nil), + .block([ + .block([ + .block([ + .expression( + .assignment( + Token(type: .identifier, lexeme: "becca", line: 1), + .literal(.string("awesome")), + 3)) + ]) + ]) + ]) + ] + XCTAssertEqual(actual, expected) + } + + func testResolveReturnStatementOutsideFunctionBody() throws { + // { return 42; } + let statements: [Statement] = [ + .block([ + .return( + Token(type: .return, lexeme: "return", line: 1), + .literal(.number(42))), + ]) + ] + + var resolver = Resolver() + let expectedError = ResolverError.cannotReturnOutsideFunction + XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in + XCTAssertEqual(actualError as! ResolverError, expectedError) + } + } + + func testResolveVariableReferencedInItsOwnInitializer() throws { + // var a = "outer"; + // { + // var a = a; + // } + let statements: [Statement] = [ + .variableDeclaration( + Token(type: .identifier, lexeme: "x", line: 1), + .literal(.string("outer"))), + .block([ + .variableDeclaration( + Token(type: .identifier, lexeme: "x", line: 3), + .variable( + Token(type: .identifier, lexeme: "x", line: 3))), + ]) + ] + + var resolver = Resolver() + let expectedError = ResolverError.variableAccessedBeforeInitialization + XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in + XCTAssertEqual(actualError as! ResolverError, expectedError) + } + } + + func testResolveVariableBeingDeclaredTwiceInTheSameScope() throws { + // { + // var a = "first"; + // var a = "second"; + // } + let statements: [Statement] = [ + .block([ + .variableDeclaration( + Token(type: .identifier, lexeme: "a", line: 2), + .literal(.string("first"))), + .variableDeclaration( + Token(type: .identifier, lexeme: "a", line: 3), + .literal(.string("second"))), + ]) + ] + + var resolver = Resolver() + let expectedError = ResolverError.variableAlreadyDefined("a") + XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in + XCTAssertEqual(actualError as! ResolverError, expectedError) + } + } +}