From f3ad9d4bea0c19564c3de9e65dadd35d15d8af6a Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 15 Mar 2024 19:27:59 -0700 Subject: [PATCH 01/13] Can now scan `break` keyword. --- slox/Scanner.swift | 1 + slox/TokenType.swift | 2 +- sloxTests/ScannerTests.swift | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/slox/Scanner.swift b/slox/Scanner.swift index 018a853..cbf7a02 100644 --- a/slox/Scanner.swift +++ b/slox/Scanner.swift @@ -33,6 +33,7 @@ struct Scanner { "true": .true, "var": .var, "while": .while, + "break": .break ] init(source: String) { diff --git a/slox/TokenType.swift b/slox/TokenType.swift index 7e631f8..f7b568e 100644 --- a/slox/TokenType.swift +++ b/slox/TokenType.swift @@ -16,7 +16,7 @@ enum TokenType: Equatable { case identifier, string, number // Keywords - case and, `class`, `else`, `false`, fun, `for`, `if`, `nil`, or, `print`, `return`, `super`, this, `true`, `var`, `while` + case and, `class`, `else`, `false`, fun, `for`, `if`, `nil`, or, `print`, `return`, `super`, this, `true`, `var`, `while`, `break` case eof } diff --git a/sloxTests/ScannerTests.swift b/sloxTests/ScannerTests.swift index ae0ca22..1a62533 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" var scanner = Scanner(source: source) let actual = try! scanner.scanTokens() let expected: [Token] = [ @@ -118,6 +118,7 @@ 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: .eof, lexeme: "", line: 1), ] From 617d5c0efad35ab922408a83fd73b555184d9f01 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 15 Mar 2024 21:44:58 -0700 Subject: [PATCH 02/13] First draft of being able to interpret and handle `break` statements. --- slox.xcodeproj/project.pbxproj | 12 ++++----- slox/Interpreter.swift | 15 +++++++++-- slox/{Return.swift => JumpType.swift} | 3 ++- slox/ParseError.swift | 3 +++ slox/Parser.swift | 39 ++++++++++++++++++--------- slox/ResolvedStatement.swift | 1 + slox/Resolver.swift | 2 ++ slox/Statement.swift | 1 + slox/UserDefinedFunction.swift | 2 +- 9 files changed, 56 insertions(+), 22 deletions(-) rename slox/{Return.swift => JumpType.swift} (72%) 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..1a7c1bc 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -92,6 +92,8 @@ 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) } } @@ -207,7 +209,11 @@ 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 handleVariableDeclaration(name: Token, expr: ResolvedExpression?) throws { @@ -236,8 +242,13 @@ class Interpreter { } private func handleWhileStatement(expr: ResolvedExpression, stmt: ResolvedStatement) throws { + outer: while try evaluate(expr: expr).isTruthy { - try execute(statement: stmt) + do { + try execute(statement: stmt) + } catch JumpType.break { + break outer + } } } diff --git a/slox/Return.swift b/slox/JumpType.swift similarity index 72% rename from slox/Return.swift rename to slox/JumpType.swift index e842afa..79145e8 100644 --- a/slox/Return.swift +++ b/slox/JumpType.swift @@ -7,6 +7,7 @@ import Foundation -enum Return: LocalizedError { +enum JumpType: LocalizedError { case `return`(LoxValue) + case `break` } 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..ee44a36 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,8 @@ struct Parser { // ifStmt → "if" "(" expression ")" statement // ( "else" statement )? ; // printStmt → "print" expression ";" ; - // returnStmt → "return" expression? ";" ; + // jumpStmt → ( "return" expression? ";" + // | "break" ";" ) ; // whileStmt → "while" "(" expression ")" statement ; // block → "{" declaration* "}" ; mutating private func parseDeclaration() throws -> Statement { @@ -177,8 +178,8 @@ struct Parser { return try parsePrintStatement() } - if currentTokenMatchesAny(types: [.return]) { - return try parseReturnStatement() + if currentToken.type == .return || currentToken.type == .break { + return try parseJumpStatement() } if currentTokenMatchesAny(types: [.while]) { @@ -273,19 +274,33 @@ 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) } - 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..a686dde 100644 --- a/slox/ResolvedStatement.swift +++ b/slox/ResolvedStatement.swift @@ -15,4 +15,5 @@ indirect enum ResolvedStatement: Equatable { case function(Token, ResolvedExpression) case `return`(Token, ResolvedExpression?) case `class`(Token, ResolvedExpression?, [ResolvedStatement], [ResolvedStatement]) + case `break`(Token) } diff --git a/slox/Resolver.swift b/slox/Resolver.swift index cb71e4a..cf884e3 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -58,6 +58,8 @@ struct Resolver { return try handleReturnStatement(returnToken: returnToken, expr: expr) case .while(let conditionExpr, let bodyStmt): return try handleWhile(conditionExpr: conditionExpr, bodyStmt: bodyStmt) + case .break(let breakToken): + return .break(breakToken) } } diff --git a/slox/Statement.swift b/slox/Statement.swift index f0d138e..8f5c180 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -15,4 +15,5 @@ indirect enum Statement: Equatable { case function(Token, Expression) case `return`(Token, Expression?) case `class`(Token, Expression?, [Statement], [Statement]) + case `break`(Token) } 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) From e7dacd1974ee76af38be8c2862a13cfcaf37836c Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 15 Mar 2024 23:21:36 -0700 Subject: [PATCH 03/13] Added a resolver check. --- slox/Resolver.swift | 22 +++++++++++++++++++++- slox/ResolverError.swift | 3 +++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/slox/Resolver.swift b/slox/Resolver.swift index cf884e3..d64b7a1 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -20,9 +20,15 @@ struct Resolver { case subclass } + private enum LoopType { + case none + case `while` + } + 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] { @@ -59,7 +65,7 @@ struct Resolver { case .while(let conditionExpr, let bodyStmt): return try handleWhile(conditionExpr: conditionExpr, bodyStmt: bodyStmt) case .break(let breakToken): - return .break(breakToken) + return try handleBreak(breakToken: breakToken) } } @@ -221,7 +227,21 @@ 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 handleWhile(conditionExpr: Expression, bodyStmt: Statement) throws -> ResolvedStatement { + let previousLoopType = currentLoopType + currentLoopType = .while + defer { + currentLoopType = previousLoopType + } + let resolvedConditionExpr = try resolve(expression: conditionExpr) let resolvedBodyStmt = try resolve(statement: bodyStmt) diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift index 93081a7..9afaaec 100644 --- a/slox/ResolverError.swift +++ b/slox/ResolverError.swift @@ -18,6 +18,7 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { case classCannotInheritFromItself case cannotReferenceSuperOutsideClass case cannotReferenceSuperWithoutSubclassing + case cannotBreakOutsideLoop var description: String { switch self { @@ -41,6 +42,8 @@ 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" } } } From a6df6f872ee63e8283d06e1f7b9f652c83866f73 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 15 Mar 2024 23:22:04 -0700 Subject: [PATCH 04/13] Added tests. --- sloxTests/InterpreterTests.swift | 58 ++++++++++++++++++++++++++++++++ sloxTests/ResolverTests.swift | 18 ++++++++++ 2 files changed, 76 insertions(+) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 53320e3..e3f9acf 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -506,4 +506,62 @@ 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) + } } diff --git a/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index 53da95a..0eeb482 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -447,4 +447,22 @@ 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) + } + } } From 4bb88270124dce6cf2ee90967ba2f18e092f358b Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 15 Mar 2024 23:32:52 -0700 Subject: [PATCH 05/13] Now scanning `continue`. --- slox/Scanner.swift | 3 ++- slox/TokenType.swift | 46 ++++++++++++++++++++++++++++++++---- sloxTests/ScannerTests.swift | 3 ++- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/slox/Scanner.swift b/slox/Scanner.swift index cbf7a02..70edc94 100644 --- a/slox/Scanner.swift +++ b/slox/Scanner.swift @@ -33,7 +33,8 @@ struct Scanner { "true": .true, "var": .var, "while": .while, - "break": .break + "break": .break, + "continue": .continue, ] init(source: String) { diff --git a/slox/TokenType.swift b/slox/TokenType.swift index f7b568e..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`, `break` + 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/sloxTests/ScannerTests.swift b/sloxTests/ScannerTests.swift index 1a62533..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 break" + 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] = [ @@ -119,6 +119,7 @@ final class ScannerTests: XCTestCase { 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), ] From 29512b1d549c2b66091ab5b591a20b5b491c7eb0 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 16 Mar 2024 00:47:21 -0700 Subject: [PATCH 06/13] First cut at support for `continue`. --- slox/Interpreter.swift | 11 +++++++++-- slox/JumpType.swift | 1 + slox/Parser.swift | 15 +++++++++++++-- slox/ResolvedStatement.swift | 1 + slox/Resolver.swift | 10 ++++++++++ slox/ResolverError.swift | 3 +++ slox/Statement.swift | 1 + 7 files changed, 38 insertions(+), 4 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 1a7c1bc..7503a44 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -94,6 +94,8 @@ class Interpreter { try handleReturnStatement(returnToken: returnToken, expr: expr) case .break(let breakToken): try handleBreakStatement(breakToken: breakToken) + case .continue(let continueToken): + try handleContinueStatement(continueToken: continueToken) } } @@ -216,6 +218,10 @@ class Interpreter { throw JumpType.break } + private func handleContinueStatement(continueToken: Token) throws { + throw JumpType.continue + } + private func handleVariableDeclaration(name: Token, expr: ResolvedExpression?) throws { var value: LoxValue = .nil if let expr = expr { @@ -242,12 +248,13 @@ class Interpreter { } private func handleWhileStatement(expr: ResolvedExpression, stmt: ResolvedStatement) throws { - outer: while try evaluate(expr: expr).isTruthy { do { try execute(statement: stmt) } catch JumpType.break { - break outer + break + } catch JumpType.continue { + continue } } } diff --git a/slox/JumpType.swift b/slox/JumpType.swift index 79145e8..ed74921 100644 --- a/slox/JumpType.swift +++ b/slox/JumpType.swift @@ -10,4 +10,5 @@ import Foundation enum JumpType: LocalizedError { case `return`(LoxValue) case `break` + case `continue` } diff --git a/slox/Parser.swift b/slox/Parser.swift index ee44a36..62d6693 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -56,7 +56,8 @@ struct Parser { // ( "else" statement )? ; // printStmt → "print" expression ";" ; // jumpStmt → ( "return" expression? ";" - // | "break" ";" ) ; + // | "break" ";" + // | "continue" ";" ) ; // whileStmt → "while" "(" expression ")" statement ; // block → "{" declaration* "}" ; mutating private func parseDeclaration() throws -> Statement { @@ -178,7 +179,7 @@ struct Parser { return try parsePrintStatement() } - if currentToken.type == .return || currentToken.type == .break { + if [.return, .break, .continue].contains(currentToken.type) { return try parseJumpStatement() } @@ -300,6 +301,16 @@ struct Parser { return .break(breakToken) } + if currentTokenMatchesAny(types: [.continue]) { + let continueToken = previousToken + + if !currentTokenMatchesAny(types: [.semicolon]) { + throw ParseError.missingSemicolon(currentToken) + } + + return .continue(continueToken) + } + throw ParseError.unsupportedJumpStatement(currentToken) } diff --git a/slox/ResolvedStatement.swift b/slox/ResolvedStatement.swift index a686dde..f7f8ba8 100644 --- a/slox/ResolvedStatement.swift +++ b/slox/ResolvedStatement.swift @@ -16,4 +16,5 @@ indirect enum ResolvedStatement: Equatable { 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 d64b7a1..2ad2f36 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -66,6 +66,8 @@ struct Resolver { return try handleWhile(conditionExpr: conditionExpr, bodyStmt: bodyStmt) case .break(let breakToken): return try handleBreak(breakToken: breakToken) + case .continue(let continueToken): + return try handleContinue(continueToken: continueToken) } } @@ -235,6 +237,14 @@ struct Resolver { 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 = .while diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift index 9afaaec..d3eca92 100644 --- a/slox/ResolverError.swift +++ b/slox/ResolverError.swift @@ -19,6 +19,7 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { case cannotReferenceSuperOutsideClass case cannotReferenceSuperWithoutSubclassing case cannotBreakOutsideLoop + case cannotContinueOutsideLoop var description: String { switch self { @@ -44,6 +45,8 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { 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` while inside a loop" } } } diff --git a/slox/Statement.swift b/slox/Statement.swift index 8f5c180..18bab15 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -16,4 +16,5 @@ indirect enum Statement: Equatable { case `return`(Token, Expression?) case `class`(Token, Expression?, [Statement], [Statement]) case `break`(Token) + case `continue`(Token) } From d33ad47c0ead1cc91e207fa16da243ee7fd64ade Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 16 Mar 2024 11:11:59 -0700 Subject: [PATCH 07/13] Updated parser tests to accomodate new `.for` case. --- slox/Interpreter.swift | 32 ++++++++++++++++++++++++++++++++ slox/Parser.swift | 26 ++++++-------------------- slox/ResolvedStatement.swift | 1 + slox/Resolver.swift | 36 ++++++++++++++++++++++++++++++++++++ slox/Statement.swift | 1 + sloxTests/ParserTests.swift | 31 ++++++++++++++----------------- 6 files changed, 90 insertions(+), 37 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 7503a44..ea58a6a 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, @@ -259,6 +264,33 @@ class Interpreter { } } + // for (var i = 1; i <= 3; i = i + 1) { if (i == 2) { continue; } print i; } + 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 { + let _ = try evaluate(expr: incrementExpr) + } + continue + } + + if let incrementExpr { + let _ = try evaluate(expr: incrementExpr) + } + } + } + private func evaluate(expr: ResolvedExpression) throws -> LoxValue { switch expr { case .literal(let literal): diff --git a/slox/Parser.swift b/slox/Parser.swift index 62d6693..5e4be6b 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -195,6 +195,8 @@ struct Parser { return try parseExpressionStatement() } + // TODO: Can no longer rewrite `for` in terms of `while` in order to support + // `continue` statements mutating private func parseForStatement() throws -> Statement { if !currentTokenMatchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForForStatement(currentToken) @@ -209,9 +211,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) @@ -225,25 +227,9 @@ struct Parser { throw ParseError.missingCloseParenForForStatement(currentToken) } - var forStmt = try parseStatement() + var bodyStmt = 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) - } - - if let initializerStmt { - forStmt = .block([initializerStmt, forStmt]) - } - - return forStmt + return .for(initializerStmt, testExpr, incrementExpr, bodyStmt) } mutating private func parseIfStatement() throws -> Statement { diff --git a/slox/ResolvedStatement.swift b/slox/ResolvedStatement.swift index f7f8ba8..406c147 100644 --- a/slox/ResolvedStatement.swift +++ b/slox/ResolvedStatement.swift @@ -12,6 +12,7 @@ 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]) diff --git a/slox/Resolver.swift b/slox/Resolver.swift index 2ad2f36..4ee8df3 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -23,6 +23,7 @@ struct Resolver { private enum LoopType { case none case `while` + case `for` } private var scopeStack: [[String: Bool]] = [] @@ -64,6 +65,11 @@ 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): @@ -258,6 +264,36 @@ struct Resolver { return .while(resolvedConditionExpr, resolvedBodyStmt) } + mutating private func handleFor(initializerStmt: Statement?, + testExpr: Expression, + incrementExpr: Expression?, + bodyStmt: Statement) throws -> ResolvedStatement { + let previousLoopType = currentLoopType + currentLoopType = .for + 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 { diff --git a/slox/Statement.swift b/slox/Statement.swift index 18bab15..7bc3225 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -12,6 +12,7 @@ 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]) 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)))) ] From d46226c96a76bfa5224a8cb37105a02fa3490a05 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 16 Mar 2024 13:54:11 -0700 Subject: [PATCH 08/13] Added tests. --- sloxTests/InterpreterTests.swift | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index e3f9acf..c452718 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -564,4 +564,68 @@ sum 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) + } } From 63f0179f40c8e6576513a06d6a194bc6577128d2 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 16 Mar 2024 13:56:10 -0700 Subject: [PATCH 09/13] Removed superfluous TODO. --- slox/Parser.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/slox/Parser.swift b/slox/Parser.swift index 5e4be6b..a2004c2 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -195,8 +195,6 @@ struct Parser { return try parseExpressionStatement() } - // TODO: Can no longer rewrite `for` in terms of `while` in order to support - // `continue` statements mutating private func parseForStatement() throws -> Statement { if !currentTokenMatchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForForStatement(currentToken) From 8844a243975e0b188a2d80f913593038ab95bc80 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 16 Mar 2024 14:16:56 -0700 Subject: [PATCH 10/13] BLARGH... forgot to remove another comment. --- slox/Interpreter.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index ea58a6a..f962cbd 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -264,7 +264,6 @@ class Interpreter { } } - // for (var i = 1; i <= 3; i = i + 1) { if (i == 2) { continue; } print i; } private func handleForStatement(initializerStmt: ResolvedStatement?, testExpr: ResolvedExpression, incrementExpr: ResolvedExpression?, From ab2fa9b6a7c1100d79c4f6ac309e990cc109a8fa Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 17 Mar 2024 12:54:14 -0700 Subject: [PATCH 11/13] Made a var into a let. --- slox/Parser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slox/Parser.swift b/slox/Parser.swift index a2004c2..7da4d44 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -225,7 +225,7 @@ struct Parser { throw ParseError.missingCloseParenForForStatement(currentToken) } - var bodyStmt = try parseStatement() + let bodyStmt = try parseStatement() return .for(initializerStmt, testExpr, incrementExpr, bodyStmt) } From 18b15784f2744174f48d40d0fab65e747dc6b57d Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 17 Mar 2024 13:50:06 -0700 Subject: [PATCH 12/13] Resolver now catches edge case that Becca pointed out to me. --- slox/Interpreter.swift | 4 ++-- slox/Resolver.swift | 6 ++++++ sloxTests/ResolverTests.swift | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index f962cbd..3d927fd 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -279,13 +279,13 @@ class Interpreter { break } catch JumpType.continue { if let incrementExpr { - let _ = try evaluate(expr: incrementExpr) + _ = try evaluate(expr: incrementExpr) } continue } if let incrementExpr { - let _ = try evaluate(expr: incrementExpr) + _ = try evaluate(expr: incrementExpr) } } } diff --git a/slox/Resolver.swift b/slox/Resolver.swift index 4ee8df3..c8c32b5 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -105,9 +105,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) @@ -421,10 +424,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/sloxTests/ResolverTests.swift b/sloxTests/ResolverTests.swift index 0eeb482..196157a 100644 --- a/sloxTests/ResolverTests.swift +++ b/sloxTests/ResolverTests.swift @@ -465,4 +465,37 @@ final class ResolverTests: XCTestCase { 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) + } + } } From 3d7fd4c84718e9f014c454e30b5d144d3b95898c Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 17 Mar 2024 14:02:22 -0700 Subject: [PATCH 13/13] LoopType enum only has two cases now since there is no difference between `for` and `while` loops regarding resolving of `break`s and `continue`s. --- slox/Resolver.swift | 7 +++---- slox/ResolverError.swift | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/slox/Resolver.swift b/slox/Resolver.swift index c8c32b5..5a7ec58 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -22,8 +22,7 @@ struct Resolver { private enum LoopType { case none - case `while` - case `for` + case loop } private var scopeStack: [[String: Bool]] = [] @@ -256,7 +255,7 @@ struct Resolver { mutating private func handleWhile(conditionExpr: Expression, bodyStmt: Statement) throws -> ResolvedStatement { let previousLoopType = currentLoopType - currentLoopType = .while + currentLoopType = .loop defer { currentLoopType = previousLoopType } @@ -272,7 +271,7 @@ struct Resolver { incrementExpr: Expression?, bodyStmt: Statement) throws -> ResolvedStatement { let previousLoopType = currentLoopType - currentLoopType = .for + currentLoopType = .loop defer { currentLoopType = previousLoopType } diff --git a/slox/ResolverError.swift b/slox/ResolverError.swift index d3eca92..63de6da 100644 --- a/slox/ResolverError.swift +++ b/slox/ResolverError.swift @@ -46,7 +46,7 @@ enum ResolverError: CustomStringConvertible, Equatable, LocalizedError { case .cannotBreakOutsideLoop: return "Can only `break` from inside a `while` or `for` loop" case .cannotContinueOutsideLoop: - return "Can only `continue` while inside a loop" + return "Can only `continue` from inside a `while` or `for` loop" } } }