diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index 5fe59a3..6e8cdbc 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -51,12 +51,12 @@ 876A31652B8C04990085A350 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876A31642B8C04990085A350 /* Expression.swift */; }; 876A31672B8C11810085A350 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876A31662B8C11810085A350 /* Parser.swift */; }; 876A31692B8C3AAB0085A350 /* ParseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876A31682B8C3AAB0085A350 /* ParseError.swift */; }; - 877168C22B91A9BD00723543 /* Return.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* Return.swift */; }; + 877168C22B91A9BD00723543 /* JumpType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* JumpType.swift */; }; 87777E3C2BA0FF70002E38F2 /* LoxList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87777E3B2BA0FF70002E38F2 /* LoxList.swift */; }; 87777E3D2BA1015F002E38F2 /* LoxList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87777E3B2BA0FF70002E38F2 /* LoxList.swift */; }; 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; 87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */; }; - 87C2F3742B91C2BA00126707 /* Return.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* Return.swift */; }; + 87C2F3742B91C2BA00126707 /* JumpType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* JumpType.swift */; }; 87EEDFBE2B96F52D00C7FE6D /* LoxInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */; }; 87EEDFBF2B9920F900C7FE6D /* LoxClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8702307F2B96E9580056FE57 /* LoxClass.swift */; }; 87EEDFC02B9920FD00C7FE6D /* LoxInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */; }; @@ -102,7 +102,7 @@ 876A31642B8C04990085A350 /* Expression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Expression.swift; sourceTree = ""; }; 876A31662B8C11810085A350 /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; 876A31682B8C3AAB0085A350 /* ParseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseError.swift; sourceTree = ""; }; - 877168C12B91A9BD00723543 /* Return.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Return.swift; sourceTree = ""; }; + 877168C12B91A9BD00723543 /* JumpType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpType.swift; sourceTree = ""; }; 87777E3B2BA0FF70002E38F2 /* LoxList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxList.swift; sourceTree = ""; }; 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxCallable.swift; sourceTree = ""; }; 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefinedFunction.swift; sourceTree = ""; }; @@ -152,6 +152,7 @@ 873CCB322B8ED8B900FC249A /* Environment.swift */, 876A31642B8C04990085A350 /* Expression.swift */, 873CCB202B8D5FAE00FC249A /* Interpreter.swift */, + 877168C12B91A9BD00723543 /* JumpType.swift */, 873CCB242B8D765D00FC249A /* Lox.swift */, 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */, 8702307F2B96E9580056FE57 /* LoxClass.swift */, @@ -166,7 +167,6 @@ 873283932B95122800E49035 /* ResolvedStatement.swift */, 873283952B95127100E49035 /* Resolver.swift */, 873283972B9523AD00E49035 /* ResolverError.swift */, - 877168C12B91A9BD00723543 /* Return.swift */, 873CCB222B8D617C00FC249A /* RuntimeError.swift */, 876A31612B8986630085A350 /* ScanError.swift */, 876560062B8827F9002BDE42 /* Scanner.swift */, @@ -288,7 +288,7 @@ 873CCB232B8D617C00FC249A /* RuntimeError.swift in Sources */, 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */, 873CCB212B8D5FAE00FC249A /* Interpreter.swift in Sources */, - 877168C22B91A9BD00723543 /* Return.swift in Sources */, + 877168C22B91A9BD00723543 /* JumpType.swift in Sources */, 870230802B96E9580056FE57 /* LoxClass.swift in Sources */, 876A31672B8C11810085A350 /* Parser.swift in Sources */, 876560072B8827F9002BDE42 /* Scanner.swift in Sources */, @@ -328,7 +328,7 @@ 87777E3D2BA1015F002E38F2 /* LoxList.swift in Sources */, 873283902B93FC0900E49035 /* NativeFunction.swift in Sources */, 8702307B2B95755E0056FE57 /* ResolvedExpression.swift in Sources */, - 87C2F3742B91C2BA00126707 /* Return.swift in Sources */, + 87C2F3742B91C2BA00126707 /* JumpType.swift in Sources */, 873CCB282B8E7B6300FC249A /* Parser.swift in Sources */, 873CCB272B8E7AD100FC249A /* ParserTests.swift in Sources */, 876A315D2B897C020085A350 /* Scanner.swift in Sources */, diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 1a5a1a7..3d927fd 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -83,6 +83,11 @@ class Interpreter { environment: Environment(enclosingEnvironment: environment)) case .while(let expr, let stmt): try handleWhileStatement(expr: expr, stmt: stmt) + case .for(let initializerStmt, let testExpr, let incrementExpr, let bodyStmt): + try handleForStatement(initializerStmt: initializerStmt, + testExpr: testExpr, + incrementExpr: incrementExpr, + bodyStmt: bodyStmt) case .class(let nameToken, let superclassExpr, let methods, let staticMethods): try handleClassDeclaration(nameToken: nameToken, superclassExpr: superclassExpr, @@ -92,6 +97,10 @@ class Interpreter { try handleFunctionDeclaration(name: name, lambda: lambda) case .return(let returnToken, let expr): try handleReturnStatement(returnToken: returnToken, expr: expr) + case .break(let breakToken): + try handleBreakStatement(breakToken: breakToken) + case .continue(let continueToken): + try handleContinueStatement(continueToken: continueToken) } } @@ -207,7 +216,15 @@ class Interpreter { value = try evaluate(expr: expr) } - throw Return.return(value) + throw JumpType.return(value) + } + + private func handleBreakStatement(breakToken: Token) throws { + throw JumpType.break + } + + private func handleContinueStatement(continueToken: Token) throws { + throw JumpType.continue } private func handleVariableDeclaration(name: Token, expr: ResolvedExpression?) throws { @@ -237,7 +254,39 @@ class Interpreter { private func handleWhileStatement(expr: ResolvedExpression, stmt: ResolvedStatement) throws { while try evaluate(expr: expr).isTruthy { - try execute(statement: stmt) + do { + try execute(statement: stmt) + } catch JumpType.break { + break + } catch JumpType.continue { + continue + } + } + } + + private func handleForStatement(initializerStmt: ResolvedStatement?, + testExpr: ResolvedExpression, + incrementExpr: ResolvedExpression?, + bodyStmt: ResolvedStatement) throws { + if let initializerStmt { + try execute(statement: initializerStmt) + } + + while try evaluate(expr: testExpr).isTruthy { + do { + try execute(statement: bodyStmt) + } catch JumpType.break { + break + } catch JumpType.continue { + if let incrementExpr { + _ = try evaluate(expr: incrementExpr) + } + continue + } + + if let incrementExpr { + _ = try evaluate(expr: incrementExpr) + } } } diff --git a/slox/Return.swift b/slox/JumpType.swift similarity index 65% rename from slox/Return.swift rename to slox/JumpType.swift index e842afa..ed74921 100644 --- a/slox/Return.swift +++ b/slox/JumpType.swift @@ -7,6 +7,8 @@ import Foundation -enum Return: LocalizedError { +enum JumpType: LocalizedError { case `return`(LoxValue) + case `break` + case `continue` } diff --git a/slox/ParseError.swift b/slox/ParseError.swift index 602bf86..80e983d 100644 --- a/slox/ParseError.swift +++ b/slox/ParseError.swift @@ -34,6 +34,7 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { case missingDotAfterSuper(Token) case expectedSuperclassMethodName(Token) case missingCloseBracketForSubscriptAccess(Token) + case unsupportedJumpStatement(Token) var description: String { switch self { @@ -89,6 +90,8 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { return "[Line \(token.line)] Error: expected superclass method name" case .missingCloseBracketForSubscriptAccess(let token): return "[Line \(token.line)] Error: expected closing bracket after subscript index" + case .unsupportedJumpStatement(let token): + return "[Line \(token.line)] Error: unsupported jump statement" } } } diff --git a/slox/Parser.swift b/slox/Parser.swift index 852bd65..7da4d44 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -45,7 +45,7 @@ struct Parser { // | forStmt // | ifStmt // | printStmt - // | returnStmt + // | jumpStmt // | whileStmt // | block ; // exprStmt → expression ";" ; @@ -55,7 +55,9 @@ struct Parser { // ifStmt → "if" "(" expression ")" statement // ( "else" statement )? ; // printStmt → "print" expression ";" ; - // returnStmt → "return" expression? ";" ; + // jumpStmt → ( "return" expression? ";" + // | "break" ";" + // | "continue" ";" ) ; // whileStmt → "while" "(" expression ")" statement ; // block → "{" declaration* "}" ; mutating private func parseDeclaration() throws -> Statement { @@ -177,8 +179,8 @@ struct Parser { return try parsePrintStatement() } - if currentTokenMatchesAny(types: [.return]) { - return try parseReturnStatement() + if [.return, .break, .continue].contains(currentToken.type) { + return try parseJumpStatement() } if currentTokenMatchesAny(types: [.while]) { @@ -207,9 +209,9 @@ struct Parser { initializerStmt = try parseExpressionStatement() } - var conditionExpr: Expression? = nil + var testExpr: Expression = .literal(.boolean(true)) if !currentTokenMatches(type: .semicolon) { - conditionExpr = try parseExpression() + testExpr = try parseExpression() } if !currentTokenMatchesAny(types: [.semicolon]) { throw ParseError.missingSemicolonAfterForLoopCondition(currentToken) @@ -223,25 +225,9 @@ struct Parser { throw ParseError.missingCloseParenForForStatement(currentToken) } - var forStmt = try parseStatement() - - // Here is where we do desugaring, rewriting a for statement - // in terms of a while statement. - if let incrementExpr { - forStmt = .block([forStmt, .expression(incrementExpr)]) - } - - if let conditionExpr { - forStmt = .while(conditionExpr, forStmt) - } else { - forStmt = .while(.literal(.boolean(true)), forStmt) - } + let bodyStmt = try parseStatement() - if let initializerStmt { - forStmt = .block([initializerStmt, forStmt]) - } - - return forStmt + return .for(initializerStmt, testExpr, incrementExpr, bodyStmt) } mutating private func parseIfStatement() throws -> Statement { @@ -273,19 +259,43 @@ struct Parser { throw ParseError.missingSemicolon(currentToken) } - mutating private func parseReturnStatement() throws -> Statement { - let returnToken = previousToken + mutating private func parseJumpStatement() throws -> Statement { + if currentTokenMatchesAny(types: [.return]) { + let returnToken = previousToken + + var expr: Expression? = nil + if currentToken.type != .semicolon { + expr = try parseExpression() + } + + if !currentTokenMatchesAny(types: [.semicolon]) { + throw ParseError.missingSemicolon(currentToken) + } - var expr: Expression? = nil - if currentToken.type != .semicolon { - expr = try parseExpression() + return .return(returnToken, expr) } - if !currentTokenMatchesAny(types: [.semicolon]) { - throw ParseError.missingSemicolon(currentToken) + if currentTokenMatchesAny(types: [.break]) { + let breakToken = previousToken + + if !currentTokenMatchesAny(types: [.semicolon]) { + throw ParseError.missingSemicolon(currentToken) + } + + return .break(breakToken) + } + + if currentTokenMatchesAny(types: [.continue]) { + let continueToken = previousToken + + if !currentTokenMatchesAny(types: [.semicolon]) { + throw ParseError.missingSemicolon(currentToken) + } + + return .continue(continueToken) } - return .return(returnToken, expr) + throw ParseError.unsupportedJumpStatement(currentToken) } mutating private func parseWhileStatement() throws -> Statement { diff --git a/slox/ResolvedStatement.swift b/slox/ResolvedStatement.swift index 58c6dbd..406c147 100644 --- a/slox/ResolvedStatement.swift +++ b/slox/ResolvedStatement.swift @@ -12,7 +12,10 @@ indirect enum ResolvedStatement: Equatable { case variableDeclaration(Token, ResolvedExpression?) case block([ResolvedStatement]) case `while`(ResolvedExpression, ResolvedStatement) + case `for`(ResolvedStatement?, ResolvedExpression, ResolvedExpression?, ResolvedStatement) case function(Token, ResolvedExpression) case `return`(Token, ResolvedExpression?) case `class`(Token, ResolvedExpression?, [ResolvedStatement], [ResolvedStatement]) + case `break`(Token) + case `continue`(Token) } diff --git a/slox/Resolver.swift b/slox/Resolver.swift index cb71e4a..5a7ec58 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -20,9 +20,15 @@ struct Resolver { case subclass } + private enum LoopType { + case none + case loop + } + private var scopeStack: [[String: Bool]] = [] private var currentFunctionType: FunctionType = .none private var currentClassType: ClassType = .none + private var currentLoopType: LoopType = .none // Main point of entry mutating func resolve(statements: [Statement]) throws -> [ResolvedStatement] { @@ -58,6 +64,15 @@ struct Resolver { return try handleReturnStatement(returnToken: returnToken, expr: expr) case .while(let conditionExpr, let bodyStmt): return try handleWhile(conditionExpr: conditionExpr, bodyStmt: bodyStmt) + case .for(let initializerStmt, let testExpr, let incrementExpr, let bodyStmt): + return try handleFor(initializerStmt: initializerStmt, + testExpr: testExpr, + incrementExpr: incrementExpr, + bodyStmt: bodyStmt) + case .break(let breakToken): + return try handleBreak(breakToken: breakToken) + case .continue(let continueToken): + return try handleContinue(continueToken: continueToken) } } @@ -89,9 +104,12 @@ struct Resolver { methods: [Statement], staticMethods: [Statement]) throws -> ResolvedStatement { let previousClassType = currentClassType + let previousLoopType = currentLoopType currentClassType = .class + currentLoopType = .none defer { currentClassType = previousClassType + currentLoopType = previousLoopType } try declareVariable(name: nameToken.lexeme) @@ -219,13 +237,65 @@ struct Resolver { return .return(returnToken, nil) } + mutating private func handleBreak(breakToken: Token) throws -> ResolvedStatement { + if currentLoopType == .none { + throw ResolverError.cannotBreakOutsideLoop + } + + return .break(breakToken) + } + + mutating private func handleContinue(continueToken: Token) throws -> ResolvedStatement { + if currentLoopType == .none { + throw ResolverError.cannotContinueOutsideLoop + } + + return .continue(continueToken) + } + mutating private func handleWhile(conditionExpr: Expression, bodyStmt: Statement) throws -> ResolvedStatement { + let previousLoopType = currentLoopType + currentLoopType = .loop + defer { + currentLoopType = previousLoopType + } + let resolvedConditionExpr = try resolve(expression: conditionExpr) let resolvedBodyStmt = try resolve(statement: bodyStmt) return .while(resolvedConditionExpr, resolvedBodyStmt) } + mutating private func handleFor(initializerStmt: Statement?, + testExpr: Expression, + incrementExpr: Expression?, + bodyStmt: Statement) throws -> ResolvedStatement { + let previousLoopType = currentLoopType + currentLoopType = .loop + defer { + currentLoopType = previousLoopType + } + + var resolvedInitializerStmt: ResolvedStatement? = nil + if let initializerStmt { + resolvedInitializerStmt = try resolve(statement: initializerStmt) + } + + let resolvedTestExpr = try resolve(expression: testExpr) + + var resolvedIncrementExpr: ResolvedExpression? = nil + if let incrementExpr { + resolvedIncrementExpr = try resolve(expression: incrementExpr) + } + + let resolvedBodyStmt = try resolve(statement: bodyStmt) + + return .for(resolvedInitializerStmt, + resolvedTestExpr, + resolvedIncrementExpr, + resolvedBodyStmt) + } + // Resolver for expressions mutating private func resolve(expression: Expression) throws -> ResolvedExpression { switch expression { @@ -353,10 +423,13 @@ struct Resolver { functionType: FunctionType) throws -> ResolvedExpression { beginScope() let previousFunctionType = currentFunctionType + let previousLoopType = currentLoopType currentFunctionType = functionType + currentLoopType = .none defer { endScope() currentFunctionType = previousFunctionType + currentLoopType = previousLoopType } for param in params { diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift index 93081a7..63de6da 100644 --- a/slox/ResolverError.swift +++ b/slox/ResolverError.swift @@ -18,6 +18,8 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { case classCannotInheritFromItself case cannotReferenceSuperOutsideClass case cannotReferenceSuperWithoutSubclassing + case cannotBreakOutsideLoop + case cannotContinueOutsideLoop var description: String { switch self { @@ -41,6 +43,10 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { return "Cannot use `super` from outside a class" case .cannotReferenceSuperWithoutSubclassing: return "Cannot use `super` without subclassing" + case .cannotBreakOutsideLoop: + return "Can only `break` from inside a `while` or `for` loop" + case .cannotContinueOutsideLoop: + return "Can only `continue` from inside a `while` or `for` loop" } } } diff --git a/slox/Scanner.swift b/slox/Scanner.swift index 018a853..70edc94 100644 --- a/slox/Scanner.swift +++ b/slox/Scanner.swift @@ -33,6 +33,8 @@ struct Scanner { "true": .true, "var": .var, "while": .while, + "break": .break, + "continue": .continue, ] init(source: String) { diff --git a/slox/Statement.swift b/slox/Statement.swift index f0d138e..7bc3225 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -12,7 +12,10 @@ indirect enum Statement: Equatable { case variableDeclaration(Token, Expression?) case block([Statement]) case `while`(Expression, Statement) + case `for`(Statement?, Expression, Expression?, Statement) case function(Token, Expression) case `return`(Token, Expression?) case `class`(Token, Expression?, [Statement], [Statement]) + case `break`(Token) + case `continue`(Token) } diff --git a/slox/TokenType.swift b/slox/TokenType.swift index 7e631f8..b23140e 100644 --- a/slox/TokenType.swift +++ b/slox/TokenType.swift @@ -7,16 +7,54 @@ enum TokenType: Equatable { // Single-character tokens - case leftParen, rightParen, leftBrace, rightBrace, comma, dot, minus, plus, semicolon, slash, star, leftBracket, rightBracket + case leftParen + case rightParen + case leftBrace + case rightBrace + case comma + case dot + case minus + case plus + case semicolon + case slash + case star + case leftBracket + case rightBracket // One of two character tokens - case bang, bangEqual, equal, equalEqual, greater, greaterEqual, less, lessEqual + case bang + case bangEqual + case equal + case equalEqual + case greater + case greaterEqual + case less + case lessEqual // Literals - case identifier, string, number + case identifier + case string + case number // Keywords - case and, `class`, `else`, `false`, fun, `for`, `if`, `nil`, or, `print`, `return`, `super`, this, `true`, `var`, `while` + case and + case `class` + case `else` + case `false` + case fun + case `for` + case `if` + case `nil` + case or + case `print` + case `return` + case `super` + case this + case `true` + case `var` + case `while` + case `break` + case `continue` case eof } diff --git a/slox/UserDefinedFunction.swift b/slox/UserDefinedFunction.swift index cfc7254..0ade56a 100644 --- a/slox/UserDefinedFunction.swift +++ b/slox/UserDefinedFunction.swift @@ -24,7 +24,7 @@ struct UserDefinedFunction: LoxCallable, Equatable { do { try interpreter.handleBlock(statements: body, environment: newEnvironment) - } catch Return.return(let value) { + } catch JumpType.return(let value) { // This is for when we call `init()` explicitly from an instance if isInitializer { return try enclosingEnvironment.getValueAtDepth(name: "this", depth: 0) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 53320e3..c452718 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -506,4 +506,126 @@ foo.count let expected: LoxValue = .number(2) XCTAssertEqual(actual, expected) } + + func testInterpretForLoopWithBreakStatement() throws { + let input = """ +var sum = 0; +for (var i = 1; i < 10; i = i + 1) { + sum = sum + i; + if (i == 3) { + break; + } +} +sum +""" + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(source: input) + let expected: LoxValue = .number(6) + XCTAssertEqual(actual, expected) + } + + func testInterpretWhileLoopWithBreakStatement() throws { + let input = """ +var sum = 0; +var i = 1; +while (i < 10) { + sum = sum + i; + if (i == 3) { + break; + } + i = i + 1; +} +sum +""" + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(source: input) + let expected: LoxValue = .number(6) + XCTAssertEqual(actual, expected) + } + + func testInterpretNestedLoopWithBreakStatementInsideInnerLoop() throws { + let input = """ +var sum = 0; +for (var i = 1; i <= 5; i = i + 1) { + for (var j = 1; j <= 5; j = j + 1) { + sum = sum + i*j; + if (j == 2) { + break; + } + } +} +sum +""" + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(source: input) + let expected: LoxValue = .number(45) + XCTAssertEqual(actual, expected) + } + + func testInterpretWhileLoopWithContinue() throws { + let input = """ +var i = 0; +var sum = 0; +while (i < 5) { + i = i + 1; + if (i == 3) { + continue; + } + print i; + sum = sum + i; +} +sum +""" + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(source: input) + let expected: LoxValue = .number(12) + XCTAssertEqual(actual, expected) + } + + func testInterpretForLoopWithContinue() throws { + let input = """ +var sum = 0; +for (var i = 1; i <= 5; i = i + 1) { + if (i == 3) { + continue; + } + sum = sum + i; +} +sum +""" + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(source: input) + let expected: LoxValue = .number(12) + XCTAssertEqual(actual, expected) + } + + func testInterpretNestedLoopsWithBreakAndContinue() throws { + let input = """ +var sum = 0; +for (var i = 1; i <= 3; i = i + 1) { + if (i == 2) { + continue; + } + + for (var j = 1; j <= 3; j = j + 1) { + if (j == 2) { + break; + } + + sum = sum + i*j; + } +} +sum +""" + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(source: input) + let expected: LoxValue = .number(4) + XCTAssertEqual(actual, expected) + } } diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index fb4b3a7..8b20d9d 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -373,27 +373,22 @@ final class ParserTests: XCTestCase { let actual = try parser.parse() let expected: [Statement] = [ - .block([ + .for( .variableDeclaration( Token(type: .identifier, lexeme: "i", line: 1), .literal(.number(0))), - .while( + .binary( + .variable(Token(type: .identifier, lexeme: "i", line: 1)), + Token(type: .less, lexeme: "<", line: 1), + .literal(.number(5))), + .assignment( + Token(type: .identifier, lexeme: "i", line: 1), .binary( .variable(Token(type: .identifier, lexeme: "i", line: 1)), - Token(type: .less, lexeme: "<", line: 1), - .literal(.number(5))), - .block([ - .print( - .variable(Token(type: .identifier, lexeme: "i", line: 2))), - .expression( - .assignment( - Token(type: .identifier, lexeme: "i", line: 1), - .binary( - .variable(Token(type: .identifier, lexeme: "i", line: 1)), - Token(type: .plus, lexeme: "+", line: 1), - .literal(.number(1))))), - ])) - ]) + Token(type: .plus, lexeme: "+", line: 1), + .literal(.number(1)))), + .print( + .variable(Token(type: .identifier, lexeme: "i", line: 2)))), ] XCTAssertEqual(actual, expected) } @@ -418,8 +413,10 @@ final class ParserTests: XCTestCase { let actual = try parser.parse() let expected: [Statement] = [ - .while( + .for( + nil, .literal(.boolean(true)), + nil, .print( .variable(Token(type: .identifier, lexeme: "i", line: 2)))) ] diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index 53da95a..196157a 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -447,4 +447,55 @@ final class ResolverTests: XCTestCase { XCTAssertEqual(actualError as! ResolverError, expectedError) } } + + func testResolveBreakStatementOutsideLoop() throws { + // if (true) { + // break; + // } + let statements: [Statement] = [ + .if( + .literal(.boolean(true)), + .break(Token(type: .break, lexeme: "break", line: 2)), + nil) + ] + + var resolver = Resolver() + let expectedError = ResolverError.cannotBreakOutsideLoop + XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in + XCTAssertEqual(actualError as! ResolverError, expectedError) + } + } + + func testResolveBreakStatementFunctionInsideWhileLoop() throws { + // while (true) { + // fun foo() { + // break; + // } + // foo(); + //} + let statements: [Statement] = [ + .while( + .literal(.boolean(true)), + .block([ + .function( + Token(type: .identifier, lexeme: "foo", line: 2), + .lambda( + [], + [ + .break(Token(type: .break, lexeme: "break", line: 3)) + ])), + .expression( + .call( + .variable(Token(type: .identifier, lexeme: "foo", line: 5)), + Token(type: .rightParen, lexeme: ")", line: 5), + [])) + ])) + ] + + var resolver = Resolver() + let expectedError = ResolverError.cannotBreakOutsideLoop + XCTAssertThrowsError(try resolver.resolve(statements: statements)) { actualError in + XCTAssertEqual(actualError as! ResolverError, expectedError) + } + } } diff --git a/sloxTests/ScannerTests.swift b/sloxTests/ScannerTests.swift index ae0ca22..183776c 100644 --- a/sloxTests/ScannerTests.swift +++ b/sloxTests/ScannerTests.swift @@ -98,7 +98,7 @@ final class ScannerTests: XCTestCase { } func testScanningOfKeywords() throws { - let source = "and class else false for fun if nil or print return super this true var while" + let source = "and class else false for fun if nil or print return super this true var while break continue" var scanner = Scanner(source: source) let actual = try! scanner.scanTokens() let expected: [Token] = [ @@ -118,6 +118,8 @@ final class ScannerTests: XCTestCase { Token(type: .true, lexeme: "true", line: 1), Token(type: .var, lexeme: "var", line: 1), Token(type: .while, lexeme: "while", line: 1), + Token(type: .break, lexeme: "break", line: 1), + Token(type: .continue, lexeme: "continue", line: 1), Token(type: .eof, lexeme: "", line: 1), ]