From 0060e24a3e6e735e91ca9f2e55f27b3cb5195f6b Mon Sep 17 00:00:00 2001 From: quephird Date: Thu, 29 Feb 2024 20:47:03 -0800 Subject: [PATCH 01/19] Made Interpreter a class out of necessity. --- slox/Callable.swift | 8 +++++++ slox/Function.swift | 8 +++++++ slox/Interpreter.swift | 36 +++++++++++++++++++++------- sloxTests/InterpreterTests.swift | 40 ++++++++++++++++++-------------- 4 files changed, 66 insertions(+), 26 deletions(-) create mode 100644 slox/Callable.swift create mode 100644 slox/Function.swift diff --git a/slox/Callable.swift b/slox/Callable.swift new file mode 100644 index 0000000..49ed754 --- /dev/null +++ b/slox/Callable.swift @@ -0,0 +1,8 @@ +// +// Callable.swift +// slox +// +// Created by Danielle Kefford on 2/29/24. +// + +import Foundation diff --git a/slox/Function.swift b/slox/Function.swift new file mode 100644 index 0000000..de81e54 --- /dev/null +++ b/slox/Function.swift @@ -0,0 +1,8 @@ +// +// Function.swift +// slox +// +// Created by Danielle Kefford on 2/29/24. +// + +import Foundation diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 350a196..315f2e3 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -5,16 +5,16 @@ // Created by Danielle Kefford on 2/26/24. // -struct Interpreter { +class Interpreter { var environment: Environment = Environment() - mutating func interpret(statements: [Statement]) throws { + func interpret(statements: [Statement]) throws { for statement in statements { try execute(statement: statement) } } - mutating func interpretRepl(statements: [Statement]) throws -> LoxValue? { + func interpretRepl(statements: [Statement]) throws -> LoxValue? { var result: LoxValue? = nil for (i, statement) in statements.enumerated() { @@ -28,7 +28,7 @@ struct Interpreter { return result } - mutating private func execute(statement: Statement) throws { + private func execute(statement: Statement) throws { switch statement { case .expression(let expr): let _ = try evaluate(expr: expr) @@ -45,10 +45,12 @@ struct Interpreter { environment: Environment(enclosingEnvironment: environment)) case .while(let expr, let stmt): try handleWhileStatement(expr: expr, stmt: stmt) + case .function(let name, let params, let body): + try handleFunctionDeclaration(name: name, params: params, body: body) } } - mutating private func handleIfStatement(testExpr: Expression, + private func handleIfStatement(testExpr: Expression, consequentStmt: Statement, alternativeStmt: Statement?) throws { if isTruthy(value: try evaluate(expr: testExpr)) { @@ -63,6 +65,10 @@ struct Interpreter { print(literal) } + private func handleFunctionDeclaration(name: Token, params: [Token], body: [Statement]) throws { + // TODO + } + private func handleVariableDeclaration(name: Token, expr: Expression?) throws { var value: LoxValue = .nil if let expr = expr { @@ -72,7 +78,7 @@ struct Interpreter { environment.define(name: name.lexeme, value: value) } - mutating private func handleBlock(statements: [Statement], environment: Environment) throws { + func handleBlock(statements: [Statement], environment: Environment) throws { let environmentBeforeBlock = self.environment self.environment = environment @@ -83,7 +89,7 @@ struct Interpreter { self.environment = environmentBeforeBlock } - mutating private func handleWhileStatement(expr: Expression, stmt: Statement) throws { + private func handleWhileStatement(expr: Expression, stmt: Statement) throws { while isTruthy(value: try evaluate(expr: expr)) { try execute(statement: stmt) } @@ -105,6 +111,8 @@ struct Interpreter { return try handleAssignmentExpression(name: varToken, expr: valueExpr) case .logical(let leftExpr, let oper, let rightExpr): return try handleLogicalExpression(leftExpr: leftExpr, oper: oper, rightExpr: rightExpr) + case .call(let callee, let rightParen, let args): + return try handleFunctionCallExpression(callee: callee, rightParen: rightParen, args: args) } } @@ -175,7 +183,7 @@ struct Interpreter { } } - func handleAssignmentExpression(name: Token, expr: Expression) throws -> LoxValue { + private func handleAssignmentExpression(name: Token, expr: Expression) throws -> LoxValue { let value = try evaluate(expr: expr) try environment.assign(name: name.lexeme, value: value) return value @@ -201,6 +209,18 @@ struct Interpreter { } } + private func handleFunctionCallExpression(callee: Expression, rightParen: Token, args: [Expression]) throws -> LoxValue { + let function = try evaluate(expr: callee) + + var argValues: [LoxValue] = [] + for arg in args { + let argValue = try evaluate(expr: arg) + argValues.append(argValue) + } + + return .number(42) + } + private func isEqual(leftValue: LoxValue, rightValue: LoxValue) -> Bool { switch (leftValue, rightValue) { case (.nil, .nil): diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index a938549..311a7b8 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -10,7 +10,7 @@ import XCTest final class InterpreterTests: XCTestCase { func testInterpretStringLiteralExpression() throws { let stmt: Statement = .expression(.literal(.string("forty-two"))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt])! let expected: LoxValue = .string("forty-two") XCTAssertEqual(actual, expected) @@ -18,7 +18,7 @@ final class InterpreterTests: XCTestCase { func testInterpretNumericLiteralExpression() throws { let stmt: Statement = .expression(.literal(.number(42))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt])! let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) @@ -26,7 +26,7 @@ final class InterpreterTests: XCTestCase { func testInterpretGroupingExpression() throws { let stmt: Statement = .expression(.grouping(.literal(.number(42)))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt])! let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) @@ -38,7 +38,7 @@ final class InterpreterTests: XCTestCase { .unary( Token(type: .bang, lexeme: "!", line: 1), .literal(.boolean(true)))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt]) let expected: LoxValue = .boolean(false) XCTAssertEqual(actual, expected) @@ -50,7 +50,7 @@ final class InterpreterTests: XCTestCase { .unary( Token(type: .minus, lexeme: "-", line: 1), .literal(.string("forty-two")))) - var interpreter = Interpreter() + let interpreter = Interpreter() let expectedError = RuntimeError.unaryOperandMustBeNumber XCTAssertThrowsError(try interpreter.interpretRepl(statements: [stmt])!) { actualError in @@ -65,7 +65,7 @@ final class InterpreterTests: XCTestCase { .literal(.number(21)), Token(type: .star, lexeme: "*", line: 1), .literal(.number(2)))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt])! let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) @@ -78,7 +78,7 @@ final class InterpreterTests: XCTestCase { .literal(.string("forty")), Token(type: .plus, lexeme: "+", line: 1), .literal(.string("-two")))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt]) let expected: LoxValue = .string("forty-two") XCTAssertEqual(actual, expected) @@ -91,7 +91,7 @@ final class InterpreterTests: XCTestCase { .literal(.boolean(true)), Token(type: .bangEqual, lexeme: "!=", line: 1), .literal(.boolean(false)))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt])! let expected: LoxValue = .boolean(true) XCTAssertEqual(actual, expected) @@ -104,7 +104,7 @@ final class InterpreterTests: XCTestCase { .literal(.string("twenty-one")), Token(type: .star, lexeme: "*", line: 1), .literal(.number(2)))) - var interpreter = Interpreter() + let interpreter = Interpreter() let expectedError = RuntimeError.binaryOperandsMustBeNumbers XCTAssertThrowsError(try interpreter.interpretRepl(statements: [stmt])!) { actualError in @@ -124,7 +124,7 @@ final class InterpreterTests: XCTestCase { .literal(.number(3)), Token(type: .plus, lexeme: "+", line: 1), .literal(.number(4)))))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt]) let expected: LoxValue = .number(-14) XCTAssertEqual(actual, expected) @@ -140,7 +140,7 @@ final class InterpreterTests: XCTestCase { .literal(.boolean(false))), Token(type: .or, lexeme: "or", line: 1), .literal(.boolean(true)))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt]) let expected: LoxValue = .boolean(true) XCTAssertEqual(actual, expected) @@ -154,7 +154,7 @@ final class InterpreterTests: XCTestCase { Token(type: .less, lexeme: "<", line: 1), .literal(.number(20)))) - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: [stmt]) let expected: LoxValue = .boolean(true) XCTAssertEqual(actual, expected) @@ -166,7 +166,7 @@ final class InterpreterTests: XCTestCase { Token(type: .identifier, lexeme: "theAnswer", line: 1), .literal(.number(42))) - var interpreter = Interpreter() + let interpreter = Interpreter() let _ = try interpreter.interpretRepl(statements: [stmt]) let environment = interpreter.environment let actual = try environment.getValue(name: "theAnswer") @@ -189,7 +189,7 @@ final class InterpreterTests: XCTestCase { Token(type: .identifier, lexeme: "theAnswer", line: 1))) ] - var interpreter = Interpreter() + let interpreter = Interpreter() let actual = try interpreter.interpretRepl(statements: statements) let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) @@ -217,7 +217,8 @@ final class InterpreterTests: XCTestCase { Token(type: .plus, lexeme: "+", line: 3), .literal(.number(1)))))), ] - var interpreter = Interpreter() + + let interpreter = Interpreter() let _ = try interpreter.interpretRepl(statements: statements) let environment = interpreter.environment let actual = try environment.getValue(name: "i") @@ -246,7 +247,8 @@ final class InterpreterTests: XCTestCase { Token(type: .identifier, lexeme: "theAnswer", line: 3), .literal(.number(0))))), ] - var interpreter = Interpreter() + + let interpreter = Interpreter() let _ = try interpreter.interpretRepl(statements: statements) let environment = interpreter.environment let actual = try environment.getValue(name: "theAnswer") @@ -273,7 +275,8 @@ final class InterpreterTests: XCTestCase { .literal(.number(2))))), ]) ] - var interpreter = Interpreter() + + let interpreter = Interpreter() let _ = try interpreter.interpretRepl(statements: statements) let environment = interpreter.environment let actual = try environment.getValue(name: "theAnswer") @@ -296,7 +299,8 @@ final class InterpreterTests: XCTestCase { .literal(.string("forty-two"))), ]) ] - var interpreter = Interpreter() + + let interpreter = Interpreter() let _ = try interpreter.interpretRepl(statements: statements) let environment = interpreter.environment let actual = try environment.getValue(name: "theAnswer") From 39d1acb47b217d622037194127fe3348e78d7f73 Mon Sep 17 00:00:00 2001 From: quephird Date: Thu, 29 Feb 2024 21:33:22 -0800 Subject: [PATCH 02/19] Can now declare and invoke functions that do not return values. --- slox.xcodeproj/project.pbxproj | 14 +++++- slox/Callable.swift | 8 ---- slox/Expression.swift | 1 + slox/Function.swift | 8 ---- slox/Interpreter.swift | 20 +++++--- slox/LoxCallable.swift | 11 +++++ slox/LoxFunction.swift | 26 ++++++++++ slox/LoxValue.swift | 3 ++ slox/ParseError.swift | 15 ++++++ slox/Parser.swift | 87 ++++++++++++++++++++++++++++++++-- slox/RuntimeError.swift | 3 ++ slox/Statement.swift | 1 + 12 files changed, 171 insertions(+), 26 deletions(-) delete mode 100644 slox/Callable.swift delete mode 100644 slox/Function.swift create mode 100644 slox/LoxCallable.swift create mode 100644 slox/LoxFunction.swift diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index 43d7574..b30401f 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ 873CCB312B8EBB7800FC249A /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB2F2B8EAEC100FC249A /* Statement.swift */; }; 873CCB332B8ED8B900FC249A /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB322B8ED8B900FC249A /* Environment.swift */; }; 873CCB342B8EE0FF00FC249A /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB322B8ED8B900FC249A /* Environment.swift */; }; + 8755B8B42B91983F00530DC4 /* LoxFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */; }; + 8755B8B52B91984C00530DC4 /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; 876560032B882259002BDE42 /* TokenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876560022B882259002BDE42 /* TokenType.swift */; }; 876560052B8825AC002BDE42 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876560042B8825AC002BDE42 /* Token.swift */; }; 876560072B8827F9002BDE42 /* Scanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876560062B8827F9002BDE42 /* Scanner.swift */; }; @@ -35,6 +37,8 @@ 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 */; }; + 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; + 87BAFC4B2B918C520013E5FE /* LoxFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -68,6 +72,8 @@ 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 = ""; }; + 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxCallable.swift; sourceTree = ""; }; + 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxFunction.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -109,11 +115,13 @@ 87655FFA2B88210A002BDE42 /* slox */ = { isa = PBXGroup; children = ( + 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */, 873CCB322B8ED8B900FC249A /* Environment.swift */, 876A31642B8C04990085A350 /* Expression.swift */, 873CCB202B8D5FAE00FC249A /* Interpreter.swift */, - 876A315E2B897EEB0085A350 /* LoxValue.swift */, 873CCB242B8D765D00FC249A /* Lox.swift */, + 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */, + 876A315E2B897EEB0085A350 /* LoxValue.swift */, 876A31682B8C3AAB0085A350 /* ParseError.swift */, 876A31662B8C11810085A350 /* Parser.swift */, 873CCB222B8D617C00FC249A /* RuntimeError.swift */, @@ -230,9 +238,11 @@ 876A315F2B897EEB0085A350 /* LoxValue.swift in Sources */, 873CCB302B8EAEC100FC249A /* Statement.swift in Sources */, 873CCB232B8D617C00FC249A /* RuntimeError.swift in Sources */, + 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */, 873CCB212B8D5FAE00FC249A /* Interpreter.swift in Sources */, 876A31672B8C11810085A350 /* Parser.swift in Sources */, 876560072B8827F9002BDE42 /* Scanner.swift in Sources */, + 87BAFC4B2B918C520013E5FE /* LoxFunction.swift in Sources */, 873CCB332B8ED8B900FC249A /* Environment.swift in Sources */, 876A31652B8C04990085A350 /* Expression.swift in Sources */, 876560032B882259002BDE42 /* TokenType.swift in Sources */, @@ -250,10 +260,12 @@ 873CCB2C2B8E88A500FC249A /* InterpreterTests.swift in Sources */, 873CCB2E2B8E88D200FC249A /* RuntimeError.swift in Sources */, 873CCB342B8EE0FF00FC249A /* Environment.swift in Sources */, + 8755B8B42B91983F00530DC4 /* LoxFunction.swift in Sources */, 876A315C2B897C000085A350 /* Token.swift in Sources */, 876A31572B8979BD0085A350 /* ScannerTests.swift in Sources */, 876A31602B89827B0085A350 /* LoxValue.swift in Sources */, 873CCB312B8EBB7800FC249A /* Statement.swift in Sources */, + 8755B8B52B91984C00530DC4 /* LoxCallable.swift in Sources */, 873CCB282B8E7B6300FC249A /* Parser.swift in Sources */, 873CCB272B8E7AD100FC249A /* ParserTests.swift in Sources */, 876A315D2B897C020085A350 /* Scanner.swift in Sources */, diff --git a/slox/Callable.swift b/slox/Callable.swift deleted file mode 100644 index 49ed754..0000000 --- a/slox/Callable.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// Callable.swift -// slox -// -// Created by Danielle Kefford on 2/29/24. -// - -import Foundation diff --git a/slox/Expression.swift b/slox/Expression.swift index 8fdb1ad..3cd7268 100644 --- a/slox/Expression.swift +++ b/slox/Expression.swift @@ -13,4 +13,5 @@ indirect enum Expression: Equatable { case variable(Token) case assignment(Token, Expression) case logical(Expression, Token, Expression) + case call(Expression, Token, [Expression]) } diff --git a/slox/Function.swift b/slox/Function.swift deleted file mode 100644 index de81e54..0000000 --- a/slox/Function.swift +++ /dev/null @@ -1,8 +0,0 @@ -// -// Function.swift -// slox -// -// Created by Danielle Kefford on 2/29/24. -// - -import Foundation diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 315f2e3..91b8feb 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -66,7 +66,8 @@ class Interpreter { } private func handleFunctionDeclaration(name: Token, params: [Token], body: [Statement]) throws { - // TODO + let function = LoxFunction(name: name, params: params, body: body) + environment.define(name: name.lexeme, value: .function(function)) } private func handleVariableDeclaration(name: Token, expr: Expression?) throws { @@ -111,8 +112,8 @@ class Interpreter { return try handleAssignmentExpression(name: varToken, expr: valueExpr) case .logical(let leftExpr, let oper, let rightExpr): return try handleLogicalExpression(leftExpr: leftExpr, oper: oper, rightExpr: rightExpr) - case .call(let callee, let rightParen, let args): - return try handleFunctionCallExpression(callee: callee, rightParen: rightParen, args: args) + case .call(let calleeExpr, let rightParen, let args): + return try handleFunctionCallExpression(calleeExpr: calleeExpr, rightParen: rightParen, args: args) } } @@ -209,8 +210,13 @@ class Interpreter { } } - private func handleFunctionCallExpression(callee: Expression, rightParen: Token, args: [Expression]) throws -> LoxValue { - let function = try evaluate(expr: callee) + private func handleFunctionCallExpression(calleeExpr: Expression, + rightParen: Token, + args: [Expression]) throws -> LoxValue { + let callee = try evaluate(expr: calleeExpr) + guard case .function(let actualFunction) = callee else { + throw RuntimeError.notAFunction + } var argValues: [LoxValue] = [] for arg in args { @@ -218,7 +224,9 @@ class Interpreter { argValues.append(argValue) } - return .number(42) + try actualFunction.call(interpreter: self, args: argValues) + + return .nil } private func isEqual(leftValue: LoxValue, rightValue: LoxValue) -> Bool { diff --git a/slox/LoxCallable.swift b/slox/LoxCallable.swift new file mode 100644 index 0000000..861d132 --- /dev/null +++ b/slox/LoxCallable.swift @@ -0,0 +1,11 @@ +// +// Callable.swift +// slox +// +// Created by Danielle Kefford on 2/29/24. +// + +protocol LoxCallable { + func arity() -> Int + func call(interpreter: Interpreter, args: [LoxValue]) throws +} diff --git a/slox/LoxFunction.swift b/slox/LoxFunction.swift new file mode 100644 index 0000000..2f2774e --- /dev/null +++ b/slox/LoxFunction.swift @@ -0,0 +1,26 @@ +// +// Function.swift +// slox +// +// Created by Danielle Kefford on 2/29/24. +// + +struct LoxFunction: LoxCallable, Equatable { + var name: Token + var params: [Token] + var body: [Statement] + + func arity() -> Int { + return params.count + } + + func call(interpreter: Interpreter, args: [LoxValue]) throws { + let environment = interpreter.environment + + for (i, arg) in args.enumerated() { + environment.define(name: params[i].lexeme, value: arg) + } + + try interpreter.handleBlock(statements: body, environment: environment) + } +} diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index 2f5c1d1..e1cec3e 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -10,6 +10,7 @@ enum LoxValue: CustomStringConvertible, Equatable { case number(Double) case boolean(Bool) case `nil` + case function(LoxFunction) var description: String { switch self { @@ -21,6 +22,8 @@ enum LoxValue: CustomStringConvertible, Equatable { return "\(boolean)" case .nil: return "nil" + case .function(let function): + return "" } } } diff --git a/slox/ParseError.swift b/slox/ParseError.swift index 6b41545..51e372a 100644 --- a/slox/ParseError.swift +++ b/slox/ParseError.swift @@ -21,6 +21,11 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { case missingOpenParenForForStatement(Token) case missingCloseParenForForStatement(Token) case missingSemicolonAfterForLoopCondition(Token) + case missingFunctionName(Token) + case missingOpenParenForFunctionDeclaration(Token) + case missingParameterName(Token) + case missingOpenBraceBeforeFunctionBody(Token) + case missingCloseParenAfterArguments(Token) var description: String { switch self { @@ -50,6 +55,16 @@ enum ParseError: CustomStringConvertible, Equatable, LocalizedError { return "[Line \(token.line)] Error: expected right parenthesis after `for` clauses" case .missingSemicolonAfterForLoopCondition(let token): return "[Line \(token.line)] Error: expected semicolon after `for` loop condition" + case .missingFunctionName(let token): + return "[Line \(token.line)] Error: expected function name" + case .missingOpenParenForFunctionDeclaration(let token): + return "[Line \(token.line)] Error: expected open paren after function name" + case .missingParameterName(let token): + return "[Line \(token.line)] Error: expected parameter name" + case .missingOpenBraceBeforeFunctionBody(let token): + return "[Line \(token.line)] Error: expected open brace before function body" + case .missingCloseParenAfterArguments(let token): + return "[Line \(token.line)] Error: expected right parenthesis after arguments" } } } diff --git a/slox/Parser.swift b/slox/Parser.swift index a5c981b..8eed6a6 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -32,8 +32,12 @@ struct Parser { // Statements are parsed in the following order: // // program → declaration* EOF ; - // declaration → varDecl + // declaration → funDecl + // | varDecl // | statement ; + // funDecl → "fun" function ; + // function → IDENTIFIER "(" parameters? ")" block ; + // parameters → IDENTIFIER ( "," IDENTIFIER )* ; // varDecl → "var" IDENTIFIER ( "=" expression )? ";" ; // statement → exprStmt // | forStmt @@ -51,6 +55,10 @@ struct Parser { // whileStmt → "while" "(" expression ")" statement ; // block → "{" declaration* "}" ; mutating func parseDeclaration() throws -> Statement { + if matchesAny(types: [.fun]) { + return try parseFunctionDeclaration() + } + if matchesAny(types: [.var]) { return try parseVariableDeclaration() } @@ -58,6 +66,42 @@ struct Parser { return try parseStatement() } + mutating private func parseFunctionDeclaration() throws -> Statement { + guard case .identifier = currentToken.type else { + throw ParseError.missingFunctionName(currentToken) + } + let functionName = currentToken + advanceCursor() + + if !matchesAny(types: [.leftParen]) { + throw ParseError.missingOpenParenForFunctionDeclaration(currentToken) + } + + var parameterNames: [Token] = [] + if currentToken.type != .rightParen { + repeat { + guard case .identifier = currentToken.type else { + throw ParseError.missingParameterName(currentToken) + } + let newParameterName = currentToken + advanceCursor() + + parameterNames.append(newParameterName) + } while matchesAny(types: [.comma]) + } + + if !matchesAny(types: [.rightParen]) { + throw ParseError.missingCloseParenAfterArguments(currentToken) + } + + if !matchesAny(types: [.leftBrace]) { + throw ParseError.missingOpenBraceBeforeFunctionBody(currentToken) + } + let functionBody = try parseBlock() + + return .function(functionName, parameterNames, functionBody) + } + mutating private func parseVariableDeclaration() throws -> Statement { guard case .identifier = currentToken.type else { throw ParseError.missingVariableName(currentToken) @@ -238,7 +282,8 @@ struct Parser { // term → factor ( ( "-" | "+" ) factor )* ; // factor → unary ( ( "/" | "*" ) unary )* ; // unary → ( "!" | "-" ) unary - // | primary ; + // | call ; + // call → primary ( "(" arguments? ")" )* ; // primary → NUMBER | STRING | "true" | "false" | "nil" // | "(" expression ")" // | IDENTIFIER ; @@ -343,7 +388,33 @@ struct Parser { return .unary(oper, expr) } - return try parsePrimary() + return try parseCall() + } + + mutating private func parseCall() throws -> Expression { + var expr = try parsePrimary() + + while true { + if matchesAny(types: [.leftParen]) { + var args: [Expression] = [] + if currentToken.type != .rightParen { + repeat { + let newArg = try parseExpression() + args.append(newArg) + } while matchesAny(types: [.comma]) + } + + if !matchesAny(types: [.rightParen]) { + throw ParseError.missingCloseParenAfterArguments(currentToken) + } + + expr = .call(expr, previousToken, args) + } else { + break + } + } + + return expr } mutating private func parsePrimary() throws -> Expression { @@ -383,6 +454,16 @@ struct Parser { throw ParseError.expectedPrimaryExpression(currentToken) } + // Utility grammar rules: + // + // arguments → expression ( "," expression )* ; + mutating private func parseArguments() throws -> [Expression] { + let exprs: [Expression] = [] + + return exprs + } + + // Utility methods mutating private func matchesAny(types: [TokenType]) -> Bool { for type in types { if matches(type: type) { diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index 753061e..0444e09 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -14,6 +14,7 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case binaryOperandsMustBeNumbersOrStrings case unsupportedBinaryOperator case undefinedVariable(String) + case notAFunction var description: String { switch self { @@ -29,6 +30,8 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { return "Unsupported binary operator" case .undefinedVariable(let name): return "Undefined variable: \(name)" + case .notAFunction: + return "Can only call functions" } } } diff --git a/slox/Statement.swift b/slox/Statement.swift index dea17ac..c318f91 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -12,4 +12,5 @@ indirect enum Statement: Equatable { case variableDeclaration(Token, Expression?) case block([Statement]) case `while`(Expression, Statement) + case function(Token, [Token], [Statement]) } From 5e963779fa23e71aecb185f63c87b3750fd2500c Mon Sep 17 00:00:00 2001 From: quephird Date: Thu, 29 Feb 2024 21:39:00 -0800 Subject: [PATCH 03/19] Implemented arity check. --- slox/Interpreter.swift | 4 ++++ slox/RuntimeError.swift | 3 +++ 2 files changed, 7 insertions(+) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 91b8feb..2add15c 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -218,6 +218,10 @@ class Interpreter { throw RuntimeError.notAFunction } + guard args.count == actualFunction.arity() else { + throw RuntimeError.wrongArity(actualFunction.arity(), args.count) + } + var argValues: [LoxValue] = [] for arg in args { let argValue = try evaluate(expr: arg) diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index 0444e09..0434e32 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -15,6 +15,7 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case unsupportedBinaryOperator case undefinedVariable(String) case notAFunction + case wrongArity(Int, Int) var description: String { switch self { @@ -32,6 +33,8 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { return "Undefined variable: \(name)" case .notAFunction: return "Can only call functions" + case .wrongArity(let expected, let actual): + return "Incorrect number of arguments: expected \(expected), got \(actual)" } } } From 6680670c3457a11d0591f0dcaa82792ef2d06a04 Mon Sep 17 00:00:00 2001 From: quephird Date: Thu, 29 Feb 2024 23:34:18 -0800 Subject: [PATCH 04/19] Functions now can return values. --- slox.xcodeproj/project.pbxproj | 4 ++++ slox/Interpreter.swift | 21 +++++++++++++++++---- slox/LoxCallable.swift | 2 +- slox/LoxFunction.swift | 10 ++++++++-- slox/Parser.swift | 21 +++++++++++++++++++++ slox/Return.swift | 12 ++++++++++++ slox/Statement.swift | 1 + 7 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 slox/Return.swift diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index b30401f..fbb1930 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 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 */; }; 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; 87BAFC4B2B918C520013E5FE /* LoxFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */; }; /* End PBXBuildFile section */ @@ -72,6 +73,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 = ""; }; 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxCallable.swift; sourceTree = ""; }; 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxFunction.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -124,6 +126,7 @@ 876A315E2B897EEB0085A350 /* LoxValue.swift */, 876A31682B8C3AAB0085A350 /* ParseError.swift */, 876A31662B8C11810085A350 /* Parser.swift */, + 877168C12B91A9BD00723543 /* Return.swift */, 873CCB222B8D617C00FC249A /* RuntimeError.swift */, 876A31612B8986630085A350 /* ScanError.swift */, 876560062B8827F9002BDE42 /* Scanner.swift */, @@ -240,6 +243,7 @@ 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 */, 876560072B8827F9002BDE42 /* Scanner.swift in Sources */, 87BAFC4B2B918C520013E5FE /* LoxFunction.swift in Sources */, diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 2add15c..a321ffd 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -21,7 +21,11 @@ class Interpreter { if i == statements.endIndex-1, case .expression(let expr) = statement { result = try evaluate(expr: expr) } else { - try execute(statement: statement) + do { + try execute(statement: statement) + } catch Return.return(let value) { + result = value + } } } @@ -47,6 +51,8 @@ class Interpreter { try handleWhileStatement(expr: expr, stmt: stmt) case .function(let name, let params, let body): try handleFunctionDeclaration(name: name, params: params, body: body) + case .return(let returnToken, let expr): + try handleReturnStatement(returnToken: returnToken, expr: expr) } } @@ -70,6 +76,15 @@ class Interpreter { environment.define(name: name.lexeme, value: .function(function)) } + private func handleReturnStatement(returnToken: Token, expr: Expression?) throws { + var value: LoxValue = .nil + if let expr { + value = try evaluate(expr: expr) + } + + throw Return.return(value) + } + private func handleVariableDeclaration(name: Token, expr: Expression?) throws { var value: LoxValue = .nil if let expr = expr { @@ -228,9 +243,7 @@ class Interpreter { argValues.append(argValue) } - try actualFunction.call(interpreter: self, args: argValues) - - return .nil + return try actualFunction.call(interpreter: self, args: argValues) } private func isEqual(leftValue: LoxValue, rightValue: LoxValue) -> Bool { diff --git a/slox/LoxCallable.swift b/slox/LoxCallable.swift index 861d132..e0326da 100644 --- a/slox/LoxCallable.swift +++ b/slox/LoxCallable.swift @@ -7,5 +7,5 @@ protocol LoxCallable { func arity() -> Int - func call(interpreter: Interpreter, args: [LoxValue]) throws + func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue } diff --git a/slox/LoxFunction.swift b/slox/LoxFunction.swift index 2f2774e..4f35a86 100644 --- a/slox/LoxFunction.swift +++ b/slox/LoxFunction.swift @@ -14,13 +14,19 @@ struct LoxFunction: LoxCallable, Equatable { return params.count } - func call(interpreter: Interpreter, args: [LoxValue]) throws { + func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue { let environment = interpreter.environment for (i, arg) in args.enumerated() { environment.define(name: params[i].lexeme, value: arg) } - try interpreter.handleBlock(statements: body, environment: environment) + do { + try interpreter.handleBlock(statements: body, environment: environment) + } catch Return.return(let value) { + return value + } + + return .nil } } diff --git a/slox/Parser.swift b/slox/Parser.swift index 8eed6a6..21d9adf 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -43,6 +43,7 @@ struct Parser { // | forStmt // | ifStmt // | printStmt + // | returnStmt // | whileStmt // | block ; // exprStmt → expression ";" ; @@ -52,6 +53,7 @@ struct Parser { // ifStmt → "if" "(" expression ")" statement // ( "else" statement )? ; // printStmt → "print" expression ";" ; + // returnStmt → "return" expression? ";" ; // whileStmt → "while" "(" expression ")" statement ; // block → "{" declaration* "}" ; mutating func parseDeclaration() throws -> Statement { @@ -134,6 +136,10 @@ struct Parser { return try parsePrintStatement() } + if matchesAny(types: [.return]) { + return try parseReturnStatement() + } + if matchesAny(types: [.while]) { return try parseWhileStatement() } @@ -226,6 +232,21 @@ struct Parser { throw ParseError.missingSemicolon(currentToken) } + mutating func parseReturnStatement() throws -> Statement { + let returnToken = previousToken + + var expr: Expression? = nil + if currentToken.type != .semicolon { + expr = try parseExpression() + } + + if !matchesAny(types: [.semicolon]) { + throw ParseError.missingSemicolon(currentToken) + } + + return .return(returnToken, expr) + } + mutating func parseWhileStatement() throws -> Statement { if !matchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForWhileStatement(currentToken) diff --git a/slox/Return.swift b/slox/Return.swift new file mode 100644 index 0000000..e842afa --- /dev/null +++ b/slox/Return.swift @@ -0,0 +1,12 @@ +// +// Return.swift +// slox +// +// Created by Danielle Kefford on 2/29/24. +// + +import Foundation + +enum Return: LocalizedError { + case `return`(LoxValue) +} diff --git a/slox/Statement.swift b/slox/Statement.swift index c318f91..98ae65e 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -13,4 +13,5 @@ indirect enum Statement: Equatable { case block([Statement]) case `while`(Expression, Statement) case function(Token, [Token], [Statement]) + case `return`(Token, Expression?) } From c60cbaaea55e774ee80b02889df047a82dfda8a5 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 00:39:45 -0800 Subject: [PATCH 05/19] Added parser tests for function declarations and invocations. --- slox.xcodeproj/project.pbxproj | 2 + sloxTests/ParserTests.swift | 102 +++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index fbb1930..6d85cc9 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ 877168C22B91A9BD00723543 /* Return.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* Return.swift */; }; 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; 87BAFC4B2B918C520013E5FE /* LoxFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */; }; + 87C2F3742B91C2BA00126707 /* Return.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* Return.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -270,6 +271,7 @@ 876A31602B89827B0085A350 /* LoxValue.swift in Sources */, 873CCB312B8EBB7800FC249A /* Statement.swift in Sources */, 8755B8B52B91984C00530DC4 /* LoxCallable.swift in Sources */, + 87C2F3742B91C2BA00126707 /* Return.swift in Sources */, 873CCB282B8E7B6300FC249A /* Parser.swift in Sources */, 873CCB272B8E7AD100FC249A /* ParserTests.swift in Sources */, 876A315D2B897C020085A350 /* Scanner.swift in Sources */, diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index b9ad97b..543a555 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -803,4 +803,106 @@ final class ParserTests: XCTestCase { XCTAssertEqual(actualError as! ParseError, expectedError) } } + + func testParseFunctionDeclaration() throws { + // fun theAnswer() { + // print 42; + // } + let tokens: [Token] = [ + Token(type: .fun, lexeme: "fun", line: 1), + Token(type: .identifier, lexeme: "theAnswer", line: 1), + Token(type: .leftParen, lexeme: "(", line: 1), + Token(type: .rightParen, lexeme: ")", line: 1), + Token(type: .leftBrace, lexeme: "{", line: 1), + + Token(type: .print, lexeme: "print", line: 2), + Token(type: .number, lexeme: "42", line: 2), + Token(type: .semicolon, lexeme: ";", line: 2), + + Token(type: .rightBrace, lexeme: "}", line: 3), + Token(type: .eof, lexeme: "", line: 3), + ] + + var parser = Parser(tokens: tokens) + let actual = try parser.parse() + let expected: [Statement] = [ + .function( + Token(type: .identifier, lexeme: "theAnswer", line: 1), + [], + [.print(.literal(.number(42)))]) + ] + XCTAssertEqual(actual, expected) + } + + func testParseFunctionDeclarationWithArgumentsAndReturn() throws { + // fun add(a, b) { + // return a + b; + // } + let tokens: [Token] = [ + Token(type: .fun, lexeme: "fun", line: 1), + Token(type: .identifier, lexeme: "add", line: 1), + Token(type: .leftParen, lexeme: "(", line: 1), + Token(type: .identifier, lexeme: "a", line: 1), + Token(type: .comma, lexeme: ",", line: 1), + Token(type: .identifier, lexeme: "b", line: 1), + Token(type: .rightParen, lexeme: ")", line: 1), + Token(type: .leftBrace, lexeme: "{", line: 1), + + Token(type: .return, lexeme: "return", line: 2), + Token(type: .identifier, lexeme: "a", line: 2), + Token(type: .plus, lexeme: "+", line: 2), + Token(type: .identifier, lexeme: "b", line: 2), + Token(type: .semicolon, lexeme: ";", line: 2), + + Token(type: .rightBrace, lexeme: "}", line: 3), + Token(type: .eof, lexeme: "", line: 3), + ] + + var parser = Parser(tokens: tokens) + let actual = try parser.parse() + let expected: [Statement] = [ + .function( + Token(type: .identifier, lexeme: "add", line: 1), + [ + 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)))) + ]) + ] + XCTAssertEqual(actual, expected) + } + + func testParseFunctionCall() throws { + // add(1, 2) + let tokens: [Token] = [ + Token(type: .identifier, lexeme: "add", line: 1), + Token(type: .leftParen, lexeme: "(", line: 1), + Token(type: .number, lexeme: "1", line: 1), + Token(type: .comma, lexeme: ",", line: 1), + Token(type: .number, lexeme: "2", line: 1), + Token(type: .rightParen, lexeme: ")", line: 1), + Token(type: .eof, lexeme: "", line: 1), + ] + + var parser = Parser(tokens: tokens) + let actual = try parser.parse() + let expected: [Statement] = [ + .expression( + .call( + .variable(Token(type: .identifier, lexeme: "add", line: 1)), + Token(type: .rightParen, lexeme: ")", line: 1), + [ + .literal(.number(1)), + .literal(.number(2)), + ])) + ] + XCTAssertEqual(actual, expected) + } } From e08c4759af94f4c910baa19b57e5bfeeb671aa1c Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 01:04:56 -0800 Subject: [PATCH 06/19] Added interpreter tests for function declarations and invocations. --- sloxTests/InterpreterTests.swift | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 311a7b8..2f4212b 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -307,4 +307,91 @@ final class InterpreterTests: XCTestCase { let expected: LoxValue = .number(42) XCTAssertEqual(actual, expected) } + + func testInterpretFunctionDeclarationAndInvocation() throws { + // fun add(a, b) { + // return a + b; + // } + // add(1, 2) + let statements: [Statement] = [ + .function( + Token(type: .identifier, lexeme: "add", line: 1), + [ + 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)))) + ]), + .expression( + .call( + .variable(Token(type: .identifier, lexeme: "add", line: 4)), + Token(type: .rightParen, lexeme: ")", line: 4), + [ + .literal(.number(1)), + .literal(.number(2)), + ])) + ] + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(statements: statements) + let expected: LoxValue = .number(3) + XCTAssertEqual(actual, expected) + } + + func testInterpretRecursiveFunction() throws { + // fun fact(n) { + // if n <= 1 { + // return 1; + // return n * fact(n-1); + // } + // fact(5) + let statements: [Statement] = [ + .function( + Token(type: .identifier, lexeme: "fact", line: 1), + [ + Token(type: .identifier, lexeme: "n", line: 1), + ], + [ + .if( + .binary( + .variable(Token(type: .identifier, lexeme: "n", line: 2)), + Token(type: .lessEqual, lexeme: "<=", line: 2), + .literal(.number(1))), + .return( + Token(type: .return, lexeme: "return", line: 3), + .literal(.number(1))), + nil), + .return( + Token(type: .return, lexeme: "return", line: 4), + .binary( + .variable(Token(type: .identifier, lexeme: "n", line: 4)), + Token(type: .star, lexeme: "*", line: 4), + .call( + .variable(Token(type: .identifier, lexeme: "fact", line: 4)), + Token(type: .rightParen, lexeme: ")", line: 4), + [ + .binary( + .variable(Token(type: .identifier, lexeme: "n", line: 4)), + Token(type: .minus, lexeme: "-", line: 4), + .literal(.number(1))) + ]))) + ]), + .expression( + .call( + .variable(Token(type: .identifier, lexeme: "fact", line: 5)), + Token(type: .rightParen, lexeme: ")", line: 5), + [.literal(.number(5)),])), + ] + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(statements: statements) + let expected: LoxValue = .number(120) + XCTAssertEqual(actual, expected) + } } From 393bf1ecbb2a65f312aac8cfc299584a58541e55 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 13:49:59 -0800 Subject: [PATCH 07/19] Made `arity` a dynamic property of `LoxFunction`. --- slox.xcodeproj/project.pbxproj | 2 +- slox/Interpreter.swift | 4 ++-- slox/LoxCallable.swift | 2 +- slox/LoxFunction.swift | 2 +- slox/LoxValue.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index 6d85cc9..96d021b 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -118,11 +118,11 @@ 87655FFA2B88210A002BDE42 /* slox */ = { isa = PBXGroup; children = ( - 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */, 873CCB322B8ED8B900FC249A /* Environment.swift */, 876A31642B8C04990085A350 /* Expression.swift */, 873CCB202B8D5FAE00FC249A /* Interpreter.swift */, 873CCB242B8D765D00FC249A /* Lox.swift */, + 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */, 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */, 876A315E2B897EEB0085A350 /* LoxValue.swift */, 876A31682B8C3AAB0085A350 /* ParseError.swift */, diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index a321ffd..8d7c450 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -233,8 +233,8 @@ class Interpreter { throw RuntimeError.notAFunction } - guard args.count == actualFunction.arity() else { - throw RuntimeError.wrongArity(actualFunction.arity(), args.count) + guard args.count == actualFunction.arity else { + throw RuntimeError.wrongArity(actualFunction.arity, args.count) } var argValues: [LoxValue] = [] diff --git a/slox/LoxCallable.swift b/slox/LoxCallable.swift index e0326da..e16961d 100644 --- a/slox/LoxCallable.swift +++ b/slox/LoxCallable.swift @@ -6,6 +6,6 @@ // protocol LoxCallable { - func arity() -> Int + var arity: Int { get } func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue } diff --git a/slox/LoxFunction.swift b/slox/LoxFunction.swift index 4f35a86..791982e 100644 --- a/slox/LoxFunction.swift +++ b/slox/LoxFunction.swift @@ -10,7 +10,7 @@ struct LoxFunction: LoxCallable, Equatable { var params: [Token] var body: [Statement] - func arity() -> Int { + var arity: Int { return params.count } diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index e1cec3e..e9624d7 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -1,5 +1,5 @@ // -// Literal.swift +// LoxValue.swift // slox // // Created by Danielle Kefford on 2/23/24. From d188f93108ae7928b77c8184c672d2795485524d Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 15:27:59 -0800 Subject: [PATCH 08/19] `LoxFunction` now has a closure as a property, and `Interpreter` constructs one and initializes an instance with it. This makes it easier to construct builtin functions. --- slox/Interpreter.swift | 35 +++++++++++++++++++++++++++++++++-- slox/LoxFunction.swift | 27 ++++++++------------------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 8d7c450..47dee17 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -5,8 +5,25 @@ // Created by Danielle Kefford on 2/26/24. // +import Foundation + class Interpreter { - var environment: Environment = Environment() + let globals: Environment = Environment() + var environment: Environment + + init() { + self.environment = globals + setUpGlobals() + } + + private func setUpGlobals() { + let clock = LoxFunction(name: "clock", + arity: 0, + function: { _, _ -> LoxValue in + return .number(Date().timeIntervalSince1970) + }) + globals.define(name: "clock", value: .function(clock)) + } func interpret(statements: [Statement]) throws { for statement in statements { @@ -72,7 +89,21 @@ class Interpreter { } private func handleFunctionDeclaration(name: Token, params: [Token], body: [Statement]) throws { - let function = LoxFunction(name: name, params: params, body: body) + let function = LoxFunction(name: name.lexeme, arity: params.count, function: { (interpreter, args) in + let environment = interpreter.environment + + for (i, arg) in args.enumerated() { + environment.define(name: params[i].lexeme, value: arg) + } + + do { + try interpreter.handleBlock(statements: body, environment: environment) + } catch Return.return(let value) { + return value + } + + return .nil + }) environment.define(name: name.lexeme, value: .function(function)) } diff --git a/slox/LoxFunction.swift b/slox/LoxFunction.swift index 791982e..a0eb13f 100644 --- a/slox/LoxFunction.swift +++ b/slox/LoxFunction.swift @@ -6,27 +6,16 @@ // struct LoxFunction: LoxCallable, Equatable { - var name: Token - var params: [Token] - var body: [Statement] - - var arity: Int { - return params.count - } + var name: String + var arity: Int + var function: (Interpreter, [LoxValue]) throws -> LoxValue func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue { - let environment = interpreter.environment - - for (i, arg) in args.enumerated() { - environment.define(name: params[i].lexeme, value: arg) - } - - do { - try interpreter.handleBlock(statements: body, environment: environment) - } catch Return.return(let value) { - return value - } + return try function(interpreter, args) + } - return .nil + static func == (lhs: LoxFunction, rhs: LoxFunction) -> Bool { + // TODO: need to improve this or look into removing the Equatable conformance + return lhs.name == rhs.name && lhs.arity == rhs.arity } } From a6d90e73fa56ff2f61e3b69d8a30e988073a6839 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 16:56:21 -0800 Subject: [PATCH 09/19] Some refactoring. Also made all parsing helpers private. --- slox/Parser.swift | 75 +++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/slox/Parser.swift b/slox/Parser.swift index 21d9adf..1a6218f 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -37,7 +37,6 @@ struct Parser { // | statement ; // funDecl → "fun" function ; // function → IDENTIFIER "(" parameters? ")" block ; - // parameters → IDENTIFIER ( "," IDENTIFIER )* ; // varDecl → "var" IDENTIFIER ( "=" expression )? ";" ; // statement → exprStmt // | forStmt @@ -56,7 +55,7 @@ struct Parser { // returnStmt → "return" expression? ";" ; // whileStmt → "while" "(" expression ")" statement ; // block → "{" declaration* "}" ; - mutating func parseDeclaration() throws -> Statement { + mutating private func parseDeclaration() throws -> Statement { if matchesAny(types: [.fun]) { return try parseFunctionDeclaration() } @@ -78,20 +77,7 @@ struct Parser { if !matchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForFunctionDeclaration(currentToken) } - - var parameterNames: [Token] = [] - if currentToken.type != .rightParen { - repeat { - guard case .identifier = currentToken.type else { - throw ParseError.missingParameterName(currentToken) - } - let newParameterName = currentToken - advanceCursor() - - parameterNames.append(newParameterName) - } while matchesAny(types: [.comma]) - } - + let parameters = try parseParameters() if !matchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenAfterArguments(currentToken) } @@ -101,7 +87,7 @@ struct Parser { } let functionBody = try parseBlock() - return .function(functionName, parameterNames, functionBody) + return .function(functionName, parameters, functionBody) } mutating private func parseVariableDeclaration() throws -> Statement { @@ -123,7 +109,7 @@ struct Parser { throw ParseError.missingSemicolon(currentToken) } - mutating func parseStatement() throws -> Statement { + mutating private func parseStatement() throws -> Statement { if matchesAny(types: [.for]) { return try parseForStatement() } @@ -152,7 +138,7 @@ struct Parser { return try parseExpressionStatement() } - mutating func parseForStatement() throws -> Statement { + mutating private func parseForStatement() throws -> Statement { if !matchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForForStatement(currentToken) } @@ -203,7 +189,7 @@ struct Parser { return forStmt } - mutating func parseIfStatement() throws -> Statement { + mutating private func parseIfStatement() throws -> Statement { if !matchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForIfStatement(currentToken) } @@ -223,7 +209,7 @@ struct Parser { return .if(testExpr, consequentStmt, alternativeStmt) } - mutating func parsePrintStatement() throws -> Statement { + mutating private func parsePrintStatement() throws -> Statement { let expr = try parseExpression() if matchesAny(types: [.semicolon]) { return .print(expr) @@ -232,7 +218,7 @@ struct Parser { throw ParseError.missingSemicolon(currentToken) } - mutating func parseReturnStatement() throws -> Statement { + mutating private func parseReturnStatement() throws -> Statement { let returnToken = previousToken var expr: Expression? = nil @@ -247,7 +233,7 @@ struct Parser { return .return(returnToken, expr) } - mutating func parseWhileStatement() throws -> Statement { + mutating private func parseWhileStatement() throws -> Statement { if !matchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForWhileStatement(currentToken) } @@ -261,7 +247,7 @@ struct Parser { return .while(expr, stmt) } - mutating func parseBlock() throws -> [Statement] { + mutating private func parseBlock() throws -> [Statement] { var statements: [Statement] = [] while currentToken.type != .rightBrace && currentToken.type != .eof { @@ -276,7 +262,7 @@ struct Parser { throw ParseError.missingClosingBrace(previousToken) } - mutating func parseExpressionStatement() throws -> Statement { + mutating private func parseExpressionStatement() throws -> Statement { let expr = try parseExpression() // NOTA BENE: If the expression is the last thing to be parsed, @@ -417,13 +403,7 @@ struct Parser { while true { if matchesAny(types: [.leftParen]) { - var args: [Expression] = [] - if currentToken.type != .rightParen { - repeat { - let newArg = try parseExpression() - args.append(newArg) - } while matchesAny(types: [.comma]) - } + let args = try parseArguments() if !matchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenAfterArguments(currentToken) @@ -477,14 +457,39 @@ struct Parser { // Utility grammar rules: // + // parameters → IDENTIFIER ( "," IDENTIFIER )* ; // arguments → expression ( "," expression )* ; + // + mutating private func parseParameters() throws -> [Token] { + var parameters: [Token] = [] + if currentToken.type != .rightParen { + repeat { + guard case .identifier = currentToken.type else { + throw ParseError.missingParameterName(currentToken) + } + let newParameter = currentToken + advanceCursor() + + parameters.append(newParameter) + } while matchesAny(types: [.comma]) + } + + return parameters + } + mutating private func parseArguments() throws -> [Expression] { - let exprs: [Expression] = [] + var args: [Expression] = [] + if currentToken.type != .rightParen { + repeat { + let newArg = try parseExpression() + args.append(newArg) + } while matchesAny(types: [.comma]) + } - return exprs + return args } - // Utility methods + // Other utility methods mutating private func matchesAny(types: [TokenType]) -> Bool { for type in types { if matches(type: type) { From e137dc6099cca31c880db6d6c7df2ee079886542 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 18:11:11 -0800 Subject: [PATCH 10/19] Got rid of `globals` var since everything seems to work without it, including recursion. --- slox/Interpreter.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 47dee17..96b4d53 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -8,11 +8,9 @@ import Foundation class Interpreter { - let globals: Environment = Environment() - var environment: Environment + var environment: Environment = Environment() init() { - self.environment = globals setUpGlobals() } @@ -22,7 +20,7 @@ class Interpreter { function: { _, _ -> LoxValue in return .number(Date().timeIntervalSince1970) }) - globals.define(name: "clock", value: .function(clock)) + environment.define(name: "clock", value: .function(clock)) } func interpret(statements: [Statement]) throws { From 3171a1f05021a83a96ae21700b7bbc4d5a79478b Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 22:01:58 -0800 Subject: [PATCH 11/19] Added support for lambda expressions, including at the top level and passing them as parameters or return values. --- slox/Expression.swift | 1 + slox/Interpreter.swift | 32 +++++- slox/Parser.swift | 52 ++++++++-- slox/RuntimeError.swift | 3 + slox/Statement.swift | 2 +- sloxTests/InterpreterTests.swift | 162 +++++++++++++++++++++++-------- sloxTests/ParserTests.swift | 73 +++++++++++--- 7 files changed, 261 insertions(+), 64 deletions(-) diff --git a/slox/Expression.swift b/slox/Expression.swift index 3cd7268..1b570f3 100644 --- a/slox/Expression.swift +++ b/slox/Expression.swift @@ -14,4 +14,5 @@ indirect enum Expression: Equatable { case assignment(Token, Expression) case logical(Expression, Token, Expression) case call(Expression, Token, [Expression]) + case lambda([Token], [Statement]) } diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 96b4d53..bb0551a 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -64,8 +64,8 @@ class Interpreter { environment: Environment(enclosingEnvironment: environment)) case .while(let expr, let stmt): try handleWhileStatement(expr: expr, stmt: stmt) - case .function(let name, let params, let body): - try handleFunctionDeclaration(name: name, params: params, body: body) + case .function(let name, let lambda): + try handleFunctionDeclaration(name: name, lambda: lambda) case .return(let returnToken, let expr): try handleReturnStatement(returnToken: returnToken, expr: expr) } @@ -86,7 +86,11 @@ class Interpreter { print(literal) } - private func handleFunctionDeclaration(name: Token, params: [Token], body: [Statement]) throws { + private func handleFunctionDeclaration(name: Token, lambda: Expression) throws { + guard case .lambda(let params, let body) = lambda else { + throw RuntimeError.notALambda + } + let function = LoxFunction(name: name.lexeme, arity: params.count, function: { (interpreter, args) in let environment = interpreter.environment @@ -158,6 +162,8 @@ class Interpreter { return try handleLogicalExpression(leftExpr: leftExpr, oper: oper, rightExpr: rightExpr) case .call(let calleeExpr, let rightParen, let args): return try handleFunctionCallExpression(calleeExpr: calleeExpr, rightParen: rightParen, args: args) + case .lambda(let params, let statements): + return try handleLambda(params: params, statements: statements) } } @@ -275,6 +281,26 @@ class Interpreter { return try actualFunction.call(interpreter: self, args: argValues) } + private func handleLambda(params: [Token], statements: [Statement]) throws -> LoxValue { + let function = LoxFunction(name: "", arity: params.count, function: { (interpreter, args) in + let environment = interpreter.environment + + for (i, arg) in args.enumerated() { + environment.define(name: params[i].lexeme, value: arg) + } + + do { + try interpreter.handleBlock(statements: statements, environment: environment) + } catch Return.return(let value) { + return value + } + + return .nil + }) + + return .function(function) + } + private func isEqual(leftValue: LoxValue, rightValue: LoxValue) -> Bool { switch (leftValue, rightValue) { case (.nil, .nil): diff --git a/slox/Parser.swift b/slox/Parser.swift index 1a6218f..09f33dc 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -56,7 +56,9 @@ struct Parser { // whileStmt → "while" "(" expression ")" statement ; // block → "{" declaration* "}" ; mutating private func parseDeclaration() throws -> Statement { - if matchesAny(types: [.fun]) { + // We check for the next token to accomodate supporting lambdas + if currentTokenMatches(type: .fun) && nextTokenMatches(type: .identifier) { + advanceCursor() return try parseFunctionDeclaration() } @@ -87,7 +89,7 @@ struct Parser { } let functionBody = try parseBlock() - return .function(functionName, parameters, functionBody) + return .function(functionName, .lambda(parameters, functionBody)) } mutating private func parseVariableDeclaration() throws -> Statement { @@ -153,7 +155,7 @@ struct Parser { } var conditionExpr: Expression? = nil - if !matches(type: .semicolon) { + if !currentTokenMatches(type: .semicolon) { conditionExpr = try parseExpression() } if !matchesAny(types: [.semicolon]) { @@ -161,7 +163,7 @@ struct Parser { } var incrementExpr: Expression? = nil - if !matches(type: .rightParen) { + if !currentTokenMatches(type: .rightParen) { incrementExpr = try parseExpression() } if !matchesAny(types: [.rightParen]) { @@ -293,7 +295,9 @@ struct Parser { // call → primary ( "(" arguments? ")" )* ; // primary → NUMBER | STRING | "true" | "false" | "nil" // | "(" expression ")" - // | IDENTIFIER ; + // | IDENTIFIER + // | lambda ; + // lambda → "fun" "(" parameters? ")" block ; // mutating private func parseExpression() throws -> Expression { return try parseAssignment() @@ -419,6 +423,10 @@ struct Parser { } mutating private func parsePrimary() throws -> Expression { + if matchesAny(types: [.fun]) { + return try parseLambda() + } + if matchesAny(types: [.false]) { return .literal(.boolean(false)) } @@ -455,6 +463,23 @@ struct Parser { throw ParseError.expectedPrimaryExpression(currentToken) } + mutating private func parseLambda() throws -> Expression { + if !matchesAny(types: [.leftParen]) { + throw ParseError.missingOpenParenForFunctionDeclaration(currentToken) + } + let parameters = try parseParameters() + if !matchesAny(types: [.rightParen]) { + throw ParseError.missingCloseParenAfterArguments(currentToken) + } + + if !matchesAny(types: [.leftBrace]) { + throw ParseError.missingOpenBraceBeforeFunctionBody(currentToken) + } + let functionBody = try parseBlock() + + return .lambda(parameters, functionBody) + } + // Utility grammar rules: // // parameters → IDENTIFIER ( "," IDENTIFIER )* ; @@ -492,7 +517,7 @@ struct Parser { // Other utility methods mutating private func matchesAny(types: [TokenType]) -> Bool { for type in types { - if matches(type: type) { + if currentTokenMatches(type: type) { advanceCursor() return true } @@ -501,7 +526,7 @@ struct Parser { return false } - private func matches(type: TokenType) -> Bool { + private func currentTokenMatches(type: TokenType) -> Bool { if currentToken.type == .eof { return false } @@ -509,6 +534,19 @@ struct Parser { return currentToken.type == type } + private func nextTokenMatches(type: TokenType) -> Bool { + if currentToken.type == .eof { + return false + } + + let nextToken = tokens[cursor + 1] + if nextToken.type == .eof { + return false + } + + return nextToken.type == type + } + // TODO: Figure out if we actually need this, and if so // how to wire it up. mutating private func synchronize() { diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index 0434e32..61a45fe 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -16,6 +16,7 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case undefinedVariable(String) case notAFunction case wrongArity(Int, Int) + case notALambda var description: String { switch self { @@ -35,6 +36,8 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { return "Can only call functions" case .wrongArity(let expected, let actual): return "Incorrect number of arguments: expected \(expected), got \(actual)" + case .notALambda: + return "Expected lambda as body of function declaration" } } } diff --git a/slox/Statement.swift b/slox/Statement.swift index 98ae65e..b49c5a3 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -12,6 +12,6 @@ indirect enum Statement: Equatable { case variableDeclaration(Token, Expression?) case block([Statement]) case `while`(Expression, Statement) - case function(Token, [Token], [Statement]) + case function(Token, Expression) case `return`(Token, Expression?) } diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 2f4212b..016bc43 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -316,18 +316,20 @@ final class InterpreterTests: XCTestCase { let statements: [Statement] = [ .function( Token(type: .identifier, lexeme: "add", line: 1), - [ - 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)))) - ]), + .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)))) + ]) + ), .expression( .call( .variable(Token(type: .identifier, lexeme: "add", line: 4)), @@ -354,34 +356,35 @@ final class InterpreterTests: XCTestCase { let statements: [Statement] = [ .function( Token(type: .identifier, lexeme: "fact", line: 1), - [ - Token(type: .identifier, lexeme: "n", line: 1), - ], - [ - .if( - .binary( - .variable(Token(type: .identifier, lexeme: "n", line: 2)), - Token(type: .lessEqual, lexeme: "<=", line: 2), - .literal(.number(1))), + .lambda( + [ + Token(type: .identifier, lexeme: "n", line: 1), + ], + [ + .if( + .binary( + .variable(Token(type: .identifier, lexeme: "n", line: 2)), + Token(type: .lessEqual, lexeme: "<=", line: 2), + .literal(.number(1))), + .return( + Token(type: .return, lexeme: "return", line: 3), + .literal(.number(1))), + nil), .return( - Token(type: .return, lexeme: "return", line: 3), - .literal(.number(1))), - nil), - .return( - Token(type: .return, lexeme: "return", line: 4), - .binary( - .variable(Token(type: .identifier, lexeme: "n", line: 4)), - Token(type: .star, lexeme: "*", line: 4), - .call( - .variable(Token(type: .identifier, lexeme: "fact", line: 4)), - Token(type: .rightParen, lexeme: ")", line: 4), - [ - .binary( - .variable(Token(type: .identifier, lexeme: "n", line: 4)), - Token(type: .minus, lexeme: "-", line: 4), - .literal(.number(1))) - ]))) - ]), + Token(type: .return, lexeme: "return", line: 4), + .binary( + .variable(Token(type: .identifier, lexeme: "n", line: 4)), + Token(type: .star, lexeme: "*", line: 4), + .call( + .variable(Token(type: .identifier, lexeme: "fact", line: 4)), + Token(type: .rightParen, lexeme: ")", line: 4), + [ + .binary( + .variable(Token(type: .identifier, lexeme: "n", line: 4)), + Token(type: .minus, lexeme: "-", line: 4), + .literal(.number(1))) + ]))) + ])), .expression( .call( .variable(Token(type: .identifier, lexeme: "fact", line: 5)), @@ -394,4 +397,85 @@ final class InterpreterTests: XCTestCase { let expected: LoxValue = .number(120) XCTAssertEqual(actual, expected) } + + func testInterpretLambdaExpression() throws { + // fun (a, b) { return a + b; }(2, 3) + let statements: [Statement] = [ + .expression( + .call( + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 1), + Token(type: .identifier, lexeme: "b", line: 1), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 1), + .binary( + .variable(Token(type: .identifier, lexeme: "a", line: 1)), + Token(type: .plus, lexeme: "+", line: 1), + .variable(Token(type: .identifier, lexeme: "b", line: 1)))) + ]), + Token(type: .rightParen, lexeme: ")", line: 1), + [ + .literal(.number(2)), + .literal(.number(3)) + ])) + ] + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(statements: statements) + let expected: LoxValue = .number(5) + XCTAssertEqual(actual, expected) + } + + func testInterpretLambdaPassedAsParameter() throws { + // fun makeAdder(n) { + // return fun (a) { return n + a; }; + // } + // var addTwo = makeAdder(2); + // addTwo(5) + let statements: [Statement] = [ + .function( + Token(type: .identifier, lexeme: "makeAdder", line: 1), + .lambda( + [ + Token(type: .identifier, lexeme: "n", line: 1), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 2), + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 2) + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 2), + .binary( + .variable(Token(type: .identifier, lexeme: "n", line: 2)), + Token(type: .plus, lexeme: "+", line: 2), + .variable(Token(type: .identifier, lexeme: "a", line: 2)))) + ])) + ])), + .variableDeclaration( + Token(type: .identifier, lexeme: "addTwo", line: 4), + .call( + .variable(Token(type: .identifier, lexeme: "makeAdder", line: 4)), + Token(type: .rightParen, lexeme: ")", line: 4), + [ + .literal(.number(2)) + ])), + .expression( + .call( + .variable(Token(type: .identifier, lexeme: "addTwo", line: 5)), + Token(type: .rightParen, lexeme: ")", line: 5), + [.literal(.number(5))])) + ] + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(statements: statements) + let expected: LoxValue = .number(7) + XCTAssertEqual(actual, expected) + } } diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index 543a555..32ddee2 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -828,8 +828,10 @@ final class ParserTests: XCTestCase { let expected: [Statement] = [ .function( Token(type: .identifier, lexeme: "theAnswer", line: 1), - [], - [.print(.literal(.number(42)))]) + .lambda( + [], + [.print(.literal(.number(42)))]) + ), ] XCTAssertEqual(actual, expected) } @@ -863,18 +865,20 @@ final class ParserTests: XCTestCase { let expected: [Statement] = [ .function( Token(type: .identifier, lexeme: "add", line: 1), - [ - 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)))) - ]) + .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)))) + ]) + ) ] XCTAssertEqual(actual, expected) } @@ -905,4 +909,45 @@ final class ParserTests: XCTestCase { ] XCTAssertEqual(actual, expected) } + + func testParseLambdaExpression() throws { + // fun (a, b) { return a + b; } + let tokens: [Token] = [ + Token(type: .fun, lexeme: "fun", line: 1), + Token(type: .leftParen, lexeme: "(", line: 1), + Token(type: .identifier, lexeme: "a", line: 1), + Token(type: .comma, lexeme: ",", line: 1), + Token(type: .identifier, lexeme: "b", line: 1), + Token(type: .rightParen, lexeme: ")", line: 1), + + Token(type: .leftBrace, lexeme: "{", line: 1), + Token(type: .return, lexeme: "return", line: 1), + Token(type: .identifier, lexeme: "a", line: 1), + Token(type: .plus, lexeme: "+", line: 1), + Token(type: .identifier, lexeme: "b", line: 1), + Token(type: .semicolon, lexeme: ";", line: 1), + Token(type: .rightBrace, lexeme: "}", line: 1), + Token(type: .eof, lexeme: "", line: 1), + ] + + var parser = Parser(tokens: tokens) + let actual = try parser.parse() + let expected: [Statement] = [ + .expression( + .lambda( + [ + Token(type: .identifier, lexeme: "a", line: 1), + Token(type: .identifier, lexeme: "b", line: 1), + ], + [ + .return( + Token(type: .return, lexeme: "return", line: 1), + .binary( + .variable(Token(type: .identifier, lexeme: "a", line: 1)), + Token(type: .plus, lexeme: "+", line: 1), + .variable(Token(type: .identifier, lexeme: "b", line: 1)))), + ])) + ] + XCTAssertEqual(actual, expected) + } } From 5620ffa0debc54c580b4370419e64acb900db826 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 22:12:06 -0800 Subject: [PATCH 12/19] Updated comment. --- slox/Parser.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/slox/Parser.swift b/slox/Parser.swift index 09f33dc..497364d 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -56,7 +56,10 @@ struct Parser { // whileStmt → "while" "(" expression ")" statement ; // block → "{" declaration* "}" ; mutating private func parseDeclaration() throws -> Statement { - // We check for the next token to accomodate supporting lambdas + // We look ahead to see if the next token is an identifer, + // and if so we assume this is a function declaration. Otherwise, + // if the current token is `fun`, then we have a lambda, and we + // will eventually parse it when we hit parsePrimary(). if currentTokenMatches(type: .fun) && nextTokenMatches(type: .identifier) { advanceCursor() return try parseFunctionDeclaration() From 2a0bdc847992442a7cc1b35a21834a5e5c8e45c3 Mon Sep 17 00:00:00 2001 From: quephird Date: Fri, 1 Mar 2024 22:21:46 -0800 Subject: [PATCH 13/19] Renamed private function. --- slox/Interpreter.swift | 4 ++-- sloxTests/InterpreterTests.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index bb0551a..fb05c3d 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -163,7 +163,7 @@ class Interpreter { case .call(let calleeExpr, let rightParen, let args): return try handleFunctionCallExpression(calleeExpr: calleeExpr, rightParen: rightParen, args: args) case .lambda(let params, let statements): - return try handleLambda(params: params, statements: statements) + return try handleLambdaExpression(params: params, statements: statements) } } @@ -281,7 +281,7 @@ class Interpreter { return try actualFunction.call(interpreter: self, args: argValues) } - private func handleLambda(params: [Token], statements: [Statement]) throws -> LoxValue { + private func handleLambdaExpression(params: [Token], statements: [Statement]) throws -> LoxValue { let function = LoxFunction(name: "", arity: params.count, function: { (interpreter, args) in let environment = interpreter.environment diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 016bc43..c70a6be 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -429,7 +429,7 @@ final class InterpreterTests: XCTestCase { XCTAssertEqual(actual, expected) } - func testInterpretLambdaPassedAsParameter() throws { + func testInterpretLambdaReturnedAsValue() throws { // fun makeAdder(n) { // return fun (a) { return n + a; }; // } From 21ef954341ff1e6ec3aedf04341c031f7d9372e3 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Mar 2024 15:08:35 -0800 Subject: [PATCH 14/19] Becca helped be crush the bug. --- slox/Interpreter.swift | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index fb05c3d..06de7b2 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -91,15 +91,16 @@ class Interpreter { throw RuntimeError.notALambda } + let environmentWhenDeclared = self.environment let function = LoxFunction(name: name.lexeme, arity: params.count, function: { (interpreter, args) in - let environment = interpreter.environment + let newEnvironment = Environment(enclosingEnvironment: environmentWhenDeclared) for (i, arg) in args.enumerated() { - environment.define(name: params[i].lexeme, value: arg) + newEnvironment.define(name: params[i].lexeme, value: arg) } do { - try interpreter.handleBlock(statements: body, environment: environment) + try interpreter.handleBlock(statements: body, environment: newEnvironment) } catch Return.return(let value) { return value } @@ -129,13 +130,18 @@ class Interpreter { func handleBlock(statements: [Statement], environment: Environment) throws { let environmentBeforeBlock = self.environment - self.environment = environment + + // This ensures that the previous environment is restored + // if the try below throws, which is what will happen if + // there is a return statement. + defer { + self.environment = environmentBeforeBlock + } + for statement in statements { try execute(statement: statement) } - - self.environment = environmentBeforeBlock } private func handleWhileStatement(expr: Expression, stmt: Statement) throws { @@ -282,15 +288,17 @@ class Interpreter { } private func handleLambdaExpression(params: [Token], statements: [Statement]) throws -> LoxValue { + let environmentWhenDeclared = self.environment + let function = LoxFunction(name: "", arity: params.count, function: { (interpreter, args) in - let environment = interpreter.environment + let newEnvironment = Environment(enclosingEnvironment: environmentWhenDeclared) for (i, arg) in args.enumerated() { - environment.define(name: params[i].lexeme, value: arg) + newEnvironment.define(name: params[i].lexeme, value: arg) } do { - try interpreter.handleBlock(statements: statements, environment: environment) + try interpreter.handleBlock(statements: statements, environment: newEnvironment) } catch Return.return(let value) { return value } From 5fbf4995978078c5d0c5de6f8684785df797aaa9 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Mar 2024 15:28:50 -0800 Subject: [PATCH 15/19] Fixed error in comment. --- sloxTests/InterpreterTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index c70a6be..2207b8f 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -348,7 +348,7 @@ final class InterpreterTests: XCTestCase { func testInterpretRecursiveFunction() throws { // fun fact(n) { - // if n <= 1 { + // if (n <= 1) // return 1; // return n * fact(n-1); // } From a118b70381dbb9975747221774108d2e5ddd7df9 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Mar 2024 15:38:53 -0800 Subject: [PATCH 16/19] Added test to cover scenario where environments are not managed properly. --- sloxTests/InterpreterTests.swift | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 2207b8f..6a52117 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -478,4 +478,44 @@ final class InterpreterTests: XCTestCase { let expected: LoxValue = .number(7) XCTAssertEqual(actual, expected) } + + func testInterpretFunctionInvocationDoesNotDisaffectGlobalEnvironment() throws { + // fun add(a, b) { return a + b; } + // add(2, 3) + // a + 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: 1), + .binary( + .variable(Token(type: .identifier, lexeme: "a", line: 1)), + Token(type: .plus, lexeme: "+", line: 1), + .variable(Token(type: .identifier, lexeme: "b", line: 1)))) + ]) + ), + .expression( + .call( + .variable(Token(type: .identifier, lexeme: "add", line: 2)), + Token(type: .rightParen, lexeme: ")", line: 2), + [ + .literal(.number(1)), + .literal(.number(2)), + ])), + .expression( + .variable(Token(type: .identifier, lexeme: "a", line: 3))) + ] + + let interpreter = Interpreter() + let expectedError = RuntimeError.undefinedVariable("a") + XCTAssertThrowsError(try interpreter.interpretRepl(statements: statements)!) { actualError in + XCTAssertEqual(actualError as! RuntimeError, expectedError) + } + } } From d3c2bf601ea72fdeab5bca9eed703182aa5f24f1 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Mar 2024 16:35:28 -0800 Subject: [PATCH 17/19] LoxFunction has been split into UserDefinedFunction and NativeFunction. --- slox.xcodeproj/project.pbxproj | 18 ++++++---- slox/Environment.swift | 6 +++- slox/Interpreter.swift | 60 ++++++++++++---------------------- slox/LoxFunction.swift | 21 ------------ slox/LoxValue.swift | 7 ++-- slox/NativeFunction.swift | 26 +++++++++++++++ slox/UserDefinedFunction.swift | 32 ++++++++++++++++++ 7 files changed, 101 insertions(+), 69 deletions(-) delete mode 100644 slox/LoxFunction.swift create mode 100644 slox/NativeFunction.swift create mode 100644 slox/UserDefinedFunction.swift diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index 96d021b..fbf92bd 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732838E2B93F89300E49035 /* NativeFunction.swift */; }; + 873283902B93FC0900E49035 /* NativeFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732838E2B93F89300E49035 /* NativeFunction.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 */; }; @@ -21,7 +23,7 @@ 873CCB312B8EBB7800FC249A /* Statement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB2F2B8EAEC100FC249A /* Statement.swift */; }; 873CCB332B8ED8B900FC249A /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB322B8ED8B900FC249A /* Environment.swift */; }; 873CCB342B8EE0FF00FC249A /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873CCB322B8ED8B900FC249A /* Environment.swift */; }; - 8755B8B42B91983F00530DC4 /* LoxFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */; }; + 8755B8B42B91983F00530DC4 /* UserDefinedFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */; }; 8755B8B52B91984C00530DC4 /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; 876560032B882259002BDE42 /* TokenType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876560022B882259002BDE42 /* TokenType.swift */; }; 876560052B8825AC002BDE42 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876560042B8825AC002BDE42 /* Token.swift */; }; @@ -39,7 +41,7 @@ 876A31692B8C3AAB0085A350 /* ParseError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876A31682B8C3AAB0085A350 /* ParseError.swift */; }; 877168C22B91A9BD00723543 /* Return.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* Return.swift */; }; 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; - 87BAFC4B2B918C520013E5FE /* LoxFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */; }; + 87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */; }; 87C2F3742B91C2BA00126707 /* Return.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* Return.swift */; }; /* End PBXBuildFile section */ @@ -56,6 +58,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 8732838E2B93F89300E49035 /* NativeFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeFunction.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 = ""; }; @@ -76,7 +79,7 @@ 876A31682B8C3AAB0085A350 /* ParseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseError.swift; sourceTree = ""; }; 877168C12B91A9BD00723543 /* Return.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Return.swift; sourceTree = ""; }; 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxCallable.swift; sourceTree = ""; }; - 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxFunction.swift; sourceTree = ""; }; + 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefinedFunction.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -123,8 +126,8 @@ 873CCB202B8D5FAE00FC249A /* Interpreter.swift */, 873CCB242B8D765D00FC249A /* Lox.swift */, 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */, - 87BAFC4A2B918C520013E5FE /* LoxFunction.swift */, 876A315E2B897EEB0085A350 /* LoxValue.swift */, + 8732838E2B93F89300E49035 /* NativeFunction.swift */, 876A31682B8C3AAB0085A350 /* ParseError.swift */, 876A31662B8C11810085A350 /* Parser.swift */, 877168C12B91A9BD00723543 /* Return.swift */, @@ -134,6 +137,7 @@ 873CCB2F2B8EAEC100FC249A /* Statement.swift */, 876560042B8825AC002BDE42 /* Token.swift */, 876560022B882259002BDE42 /* TokenType.swift */, + 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */, ); path = slox; sourceTree = ""; @@ -247,10 +251,11 @@ 877168C22B91A9BD00723543 /* Return.swift in Sources */, 876A31672B8C11810085A350 /* Parser.swift in Sources */, 876560072B8827F9002BDE42 /* Scanner.swift in Sources */, - 87BAFC4B2B918C520013E5FE /* LoxFunction.swift in Sources */, + 87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */, 873CCB332B8ED8B900FC249A /* Environment.swift in Sources */, 876A31652B8C04990085A350 /* Expression.swift in Sources */, 876560032B882259002BDE42 /* TokenType.swift in Sources */, + 8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */, 876A31692B8C3AAB0085A350 /* ParseError.swift in Sources */, 873CCB252B8D765D00FC249A /* Lox.swift in Sources */, ); @@ -265,12 +270,13 @@ 873CCB2C2B8E88A500FC249A /* InterpreterTests.swift in Sources */, 873CCB2E2B8E88D200FC249A /* RuntimeError.swift in Sources */, 873CCB342B8EE0FF00FC249A /* Environment.swift in Sources */, - 8755B8B42B91983F00530DC4 /* LoxFunction.swift in Sources */, + 8755B8B42B91983F00530DC4 /* UserDefinedFunction.swift in Sources */, 876A315C2B897C000085A350 /* Token.swift in Sources */, 876A31572B8979BD0085A350 /* ScannerTests.swift in Sources */, 876A31602B89827B0085A350 /* LoxValue.swift in Sources */, 873CCB312B8EBB7800FC249A /* Statement.swift in Sources */, 8755B8B52B91984C00530DC4 /* LoxCallable.swift in Sources */, + 873283902B93FC0900E49035 /* NativeFunction.swift in Sources */, 87C2F3742B91C2BA00126707 /* Return.swift in Sources */, 873CCB282B8E7B6300FC249A /* Parser.swift in Sources */, 873CCB272B8E7AD100FC249A /* ParserTests.swift in Sources */, diff --git a/slox/Environment.swift b/slox/Environment.swift index b2a9218..43363ed 100644 --- a/slox/Environment.swift +++ b/slox/Environment.swift @@ -5,7 +5,7 @@ // Created by Danielle Kefford on 2/27/24. // -class Environment { +class Environment: Equatable { private var enclosingEnvironment: Environment? private var values: [String: LoxValue] = [:] @@ -42,4 +42,8 @@ class Environment { throw RuntimeError.undefinedVariable(name) } + + static func == (lhs: Environment, rhs: Environment) -> Bool { + return lhs === rhs + } } diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 06de7b2..ae17921 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -15,12 +15,10 @@ class Interpreter { } private func setUpGlobals() { - let clock = LoxFunction(name: "clock", - arity: 0, - function: { _, _ -> LoxValue in - return .number(Date().timeIntervalSince1970) - }) - environment.define(name: "clock", value: .function(clock)) + for nativeFunction in NativeFunction.allCases { + environment.define(name: String(describing: nativeFunction), + value: .nativeFunction(nativeFunction)) + } } func interpret(statements: [Statement]) throws { @@ -92,22 +90,11 @@ class Interpreter { } let environmentWhenDeclared = self.environment - let function = LoxFunction(name: name.lexeme, arity: params.count, function: { (interpreter, args) in - let newEnvironment = Environment(enclosingEnvironment: environmentWhenDeclared) - - for (i, arg) in args.enumerated() { - newEnvironment.define(name: params[i].lexeme, value: arg) - } - - do { - try interpreter.handleBlock(statements: body, environment: newEnvironment) - } catch Return.return(let value) { - return value - } - - return .nil - }) - environment.define(name: name.lexeme, value: .function(function)) + let function = UserDefinedFunction(name: name.lexeme, + params: params, + enclosingEnvironment: environmentWhenDeclared, + body: body) + environment.define(name: name.lexeme, value: .userDefinedFunction(function)) } private func handleReturnStatement(returnToken: Token, expr: Expression?) throws { @@ -270,7 +257,13 @@ class Interpreter { rightParen: Token, args: [Expression]) throws -> LoxValue { let callee = try evaluate(expr: calleeExpr) - guard case .function(let actualFunction) = callee else { + + let actualFunction: LoxCallable = switch callee { + case .userDefinedFunction(let userDefinedFunction): + userDefinedFunction + case .nativeFunction(let nativeFunction): + nativeFunction + default: throw RuntimeError.notAFunction } @@ -290,23 +283,12 @@ class Interpreter { private func handleLambdaExpression(params: [Token], statements: [Statement]) throws -> LoxValue { let environmentWhenDeclared = self.environment - let function = LoxFunction(name: "", arity: params.count, function: { (interpreter, args) in - let newEnvironment = Environment(enclosingEnvironment: environmentWhenDeclared) - - for (i, arg) in args.enumerated() { - newEnvironment.define(name: params[i].lexeme, value: arg) - } - - do { - try interpreter.handleBlock(statements: statements, environment: newEnvironment) - } catch Return.return(let value) { - return value - } - - return .nil - }) + let function = UserDefinedFunction(name: "", + params: params, + enclosingEnvironment: environmentWhenDeclared, + body: statements) - return .function(function) + return .userDefinedFunction(function) } private func isEqual(leftValue: LoxValue, rightValue: LoxValue) -> Bool { diff --git a/slox/LoxFunction.swift b/slox/LoxFunction.swift deleted file mode 100644 index a0eb13f..0000000 --- a/slox/LoxFunction.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Function.swift -// slox -// -// Created by Danielle Kefford on 2/29/24. -// - -struct LoxFunction: LoxCallable, Equatable { - var name: String - var arity: Int - var function: (Interpreter, [LoxValue]) throws -> LoxValue - - func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue { - return try function(interpreter, args) - } - - static func == (lhs: LoxFunction, rhs: LoxFunction) -> Bool { - // TODO: need to improve this or look into removing the Equatable conformance - return lhs.name == rhs.name && lhs.arity == rhs.arity - } -} diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index e9624d7..3a62081 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -10,7 +10,8 @@ enum LoxValue: CustomStringConvertible, Equatable { case number(Double) case boolean(Bool) case `nil` - case function(LoxFunction) + case userDefinedFunction(UserDefinedFunction) + case nativeFunction(NativeFunction) var description: String { switch self { @@ -22,8 +23,10 @@ enum LoxValue: CustomStringConvertible, Equatable { return "\(boolean)" case .nil: return "nil" - case .function(let function): + case .userDefinedFunction(let function): return "" + case .nativeFunction(let function): + return "" } } } diff --git a/slox/NativeFunction.swift b/slox/NativeFunction.swift new file mode 100644 index 0000000..ddf0dc7 --- /dev/null +++ b/slox/NativeFunction.swift @@ -0,0 +1,26 @@ +// +// NativeFunction.swift +// slox +// +// Created by Danielle Kefford on 3/2/24. +// + +import Foundation + +enum NativeFunction: LoxCallable, Equatable, CaseIterable { + case clock + + var arity: Int { + switch self { + case .clock: + return 0 + } + } + + func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue { + switch self { + case .clock: + return .number(Date().timeIntervalSince1970) + } + } +} diff --git a/slox/UserDefinedFunction.swift b/slox/UserDefinedFunction.swift new file mode 100644 index 0000000..155b91a --- /dev/null +++ b/slox/UserDefinedFunction.swift @@ -0,0 +1,32 @@ +// +// Function.swift +// slox +// +// Created by Danielle Kefford on 2/29/24. +// + +struct UserDefinedFunction: LoxCallable, Equatable { + var name: String + var params: [Token] + var arity: Int { + return params.count + } + var enclosingEnvironment: Environment + var body: [Statement] + + func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue { + let newEnvironment = Environment(enclosingEnvironment: enclosingEnvironment) + + for (i, arg) in args.enumerated() { + newEnvironment.define(name: params[i].lexeme, value: arg) + } + + do { + try interpreter.handleBlock(statements: body, environment: newEnvironment) + } catch Return.return(let value) { + return value + } + + return .nil + } +} From fb7e28f8f838076253151a351897edc0fff75851 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Mar 2024 17:07:22 -0800 Subject: [PATCH 18/19] Cleaned up Interpreter.interpreterRepl(). --- slox/Interpreter.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index ae17921..4c3ab91 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -28,21 +28,15 @@ class Interpreter { } func interpretRepl(statements: [Statement]) throws -> LoxValue? { - var result: LoxValue? = nil - for (i, statement) in statements.enumerated() { if i == statements.endIndex-1, case .expression(let expr) = statement { - result = try evaluate(expr: expr) + return try evaluate(expr: expr) } else { - do { - try execute(statement: statement) - } catch Return.return(let value) { - result = value - } + try execute(statement: statement) } } - return result + return nil } private func execute(statement: Statement) throws { From 314a63cd101e2dedd42b3963bd93882a086e6b5a Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 2 Mar 2024 17:10:34 -0800 Subject: [PATCH 19/19] Renamed matchesAny(). --- slox/Parser.swift | 102 +++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/slox/Parser.swift b/slox/Parser.swift index 497364d..9c90a6b 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -65,7 +65,7 @@ struct Parser { return try parseFunctionDeclaration() } - if matchesAny(types: [.var]) { + if currentTokenMatchesAny(types: [.var]) { return try parseVariableDeclaration() } @@ -79,15 +79,15 @@ struct Parser { let functionName = currentToken advanceCursor() - if !matchesAny(types: [.leftParen]) { + if !currentTokenMatchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForFunctionDeclaration(currentToken) } let parameters = try parseParameters() - if !matchesAny(types: [.rightParen]) { + if !currentTokenMatchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenAfterArguments(currentToken) } - if !matchesAny(types: [.leftBrace]) { + if !currentTokenMatchesAny(types: [.leftBrace]) { throw ParseError.missingOpenBraceBeforeFunctionBody(currentToken) } let functionBody = try parseBlock() @@ -103,11 +103,11 @@ struct Parser { advanceCursor() var initializer: Expression? = nil - if matchesAny(types: [.equal]) { + if currentTokenMatchesAny(types: [.equal]) { initializer = try parseExpression() } - if matchesAny(types: [.semicolon]) { + if currentTokenMatchesAny(types: [.semicolon]) { return .variableDeclaration(name, initializer); } @@ -115,27 +115,27 @@ struct Parser { } mutating private func parseStatement() throws -> Statement { - if matchesAny(types: [.for]) { + if currentTokenMatchesAny(types: [.for]) { return try parseForStatement() } - if matchesAny(types: [.if]) { + if currentTokenMatchesAny(types: [.if]) { return try parseIfStatement() } - if matchesAny(types: [.print]) { + if currentTokenMatchesAny(types: [.print]) { return try parsePrintStatement() } - if matchesAny(types: [.return]) { + if currentTokenMatchesAny(types: [.return]) { return try parseReturnStatement() } - if matchesAny(types: [.while]) { + if currentTokenMatchesAny(types: [.while]) { return try parseWhileStatement() } - if matchesAny(types: [.leftBrace]) { + if currentTokenMatchesAny(types: [.leftBrace]) { let statements = try parseBlock() return .block(statements) } @@ -144,14 +144,14 @@ struct Parser { } mutating private func parseForStatement() throws -> Statement { - if !matchesAny(types: [.leftParen]) { + if !currentTokenMatchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForForStatement(currentToken) } var initializerStmt: Statement? - if matchesAny(types: [.semicolon]) { + if currentTokenMatchesAny(types: [.semicolon]) { initializerStmt = nil - } else if matchesAny(types: [.var]) { + } else if currentTokenMatchesAny(types: [.var]) { initializerStmt = try parseVariableDeclaration() } else { initializerStmt = try parseExpressionStatement() @@ -161,7 +161,7 @@ struct Parser { if !currentTokenMatches(type: .semicolon) { conditionExpr = try parseExpression() } - if !matchesAny(types: [.semicolon]) { + if !currentTokenMatchesAny(types: [.semicolon]) { throw ParseError.missingSemicolonAfterForLoopCondition(currentToken) } @@ -169,7 +169,7 @@ struct Parser { if !currentTokenMatches(type: .rightParen) { incrementExpr = try parseExpression() } - if !matchesAny(types: [.rightParen]) { + if !currentTokenMatchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenForForStatement(currentToken) } @@ -195,19 +195,19 @@ struct Parser { } mutating private func parseIfStatement() throws -> Statement { - if !matchesAny(types: [.leftParen]) { + if !currentTokenMatchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForIfStatement(currentToken) } let testExpr = try parseExpression() - if !matchesAny(types: [.rightParen]) { + if !currentTokenMatchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenForIfStatement(currentToken) } let consequentStmt = try parseStatement() var alternativeStmt: Statement? = nil - if matchesAny(types: [.else]) { + if currentTokenMatchesAny(types: [.else]) { alternativeStmt = try parseStatement() } @@ -216,7 +216,7 @@ struct Parser { mutating private func parsePrintStatement() throws -> Statement { let expr = try parseExpression() - if matchesAny(types: [.semicolon]) { + if currentTokenMatchesAny(types: [.semicolon]) { return .print(expr) } @@ -231,7 +231,7 @@ struct Parser { expr = try parseExpression() } - if !matchesAny(types: [.semicolon]) { + if !currentTokenMatchesAny(types: [.semicolon]) { throw ParseError.missingSemicolon(currentToken) } @@ -239,12 +239,12 @@ struct Parser { } mutating private func parseWhileStatement() throws -> Statement { - if !matchesAny(types: [.leftParen]) { + if !currentTokenMatchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForWhileStatement(currentToken) } let expr = try parseExpression() - if !matchesAny(types: [.rightParen]) { + if !currentTokenMatchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenForWhileStatement(currentToken) } @@ -260,7 +260,7 @@ struct Parser { statements.append(statement) } - if matchesAny(types: [.rightBrace]) { + if currentTokenMatchesAny(types: [.rightBrace]) { return statements } @@ -274,7 +274,7 @@ struct Parser { // then we want to return that immediately so it can be evaluated // and whose result can be printed in the REPL, and without burdening // the user to add a semicolon at the end. - if currentToken.type == .eof || matchesAny(types: [.semicolon]) { + if currentToken.type == .eof || currentTokenMatchesAny(types: [.semicolon]) { return .expression(expr) } @@ -309,7 +309,7 @@ struct Parser { mutating private func parseAssignment() throws -> Expression { let expr = try parseLogicOr() - if matchesAny(types: [.equal]) { + if currentTokenMatchesAny(types: [.equal]) { let equalToken = previousToken let valueExpr = try parseAssignment() @@ -326,7 +326,7 @@ struct Parser { mutating private func parseLogicOr() throws -> Expression { var expr = try parseLogicAnd() - while matchesAny(types: [.or]) { + while currentTokenMatchesAny(types: [.or]) { let oper = previousToken let right = try parseLogicAnd() expr = .logical(expr, oper, right) @@ -338,7 +338,7 @@ struct Parser { mutating private func parseLogicAnd() throws -> Expression { var expr = try parseEquality() - while matchesAny(types: [.and]) { + while currentTokenMatchesAny(types: [.and]) { let oper = previousToken let right = try parseEquality() expr = .logical(expr, oper, right) @@ -350,7 +350,7 @@ struct Parser { mutating private func parseEquality() throws -> Expression { var expr = try parseComparison() - while matchesAny(types: [.bangEqual, .equalEqual]) { + while currentTokenMatchesAny(types: [.bangEqual, .equalEqual]) { let oper = previousToken let rightExpr = try parseComparison() expr = .binary(expr, oper, rightExpr) @@ -362,7 +362,7 @@ struct Parser { mutating private func parseComparison() throws -> Expression { var expr = try parseTerm() - while matchesAny(types: [.less, .lessEqual, .greater, .greaterEqual]) { + while currentTokenMatchesAny(types: [.less, .lessEqual, .greater, .greaterEqual]) { let oper = previousToken let rightExpr = try parseTerm() expr = .binary(expr, oper, rightExpr) @@ -374,7 +374,7 @@ struct Parser { mutating private func parseTerm() throws -> Expression { var expr = try parseFactor() - while matchesAny(types: [.plus, .minus]) { + while currentTokenMatchesAny(types: [.plus, .minus]) { let oper = previousToken let rightExpr = try parseFactor() expr = .binary(expr, oper, rightExpr) @@ -386,7 +386,7 @@ struct Parser { mutating private func parseFactor() throws -> Expression { var expr = try parseUnary() - while matchesAny(types: [.slash, .star]) { + while currentTokenMatchesAny(types: [.slash, .star]) { let oper = previousToken let rightExpr = try parseUnary() expr = .binary(expr, oper, rightExpr) @@ -396,7 +396,7 @@ struct Parser { } mutating private func parseUnary() throws -> Expression { - if matchesAny(types: [.bang, .minus]) { + if currentTokenMatchesAny(types: [.bang, .minus]) { let oper = previousToken let expr = try parseUnary() return .unary(oper, expr) @@ -409,10 +409,10 @@ struct Parser { var expr = try parsePrimary() while true { - if matchesAny(types: [.leftParen]) { + if currentTokenMatchesAny(types: [.leftParen]) { let args = try parseArguments() - if !matchesAny(types: [.rightParen]) { + if !currentTokenMatchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenAfterArguments(currentToken) } @@ -426,40 +426,40 @@ struct Parser { } mutating private func parsePrimary() throws -> Expression { - if matchesAny(types: [.fun]) { + if currentTokenMatchesAny(types: [.fun]) { return try parseLambda() } - if matchesAny(types: [.false]) { + if currentTokenMatchesAny(types: [.false]) { return .literal(.boolean(false)) } - if matchesAny(types: [.true]) { + if currentTokenMatchesAny(types: [.true]) { return .literal(.boolean(true)) } - if matchesAny(types: [.nil]) { + if currentTokenMatchesAny(types: [.nil]) { return .literal(.nil) } - if matchesAny(types: [.number]) { + if currentTokenMatchesAny(types: [.number]) { let number = Double(previousToken.lexeme)! return .literal(.number(number)) } - if matchesAny(types: [.string]) { + if currentTokenMatchesAny(types: [.string]) { assert(previousToken.lexeme.hasPrefix("\"") && previousToken.lexeme.hasSuffix("\"")) let string = String(previousToken.lexeme.dropFirst().dropLast()) return .literal(.string(string)) } - if matchesAny(types: [.leftParen]) { + if currentTokenMatchesAny(types: [.leftParen]) { let expr = try parseExpression() - if matchesAny(types: [.rightParen]) { + if currentTokenMatchesAny(types: [.rightParen]) { return .grouping(expr) } throw ParseError.missingClosingParenthesis(currentToken) } - if matchesAny(types: [.identifier]) { + if currentTokenMatchesAny(types: [.identifier]) { return .variable(previousToken) } @@ -467,15 +467,15 @@ struct Parser { } mutating private func parseLambda() throws -> Expression { - if !matchesAny(types: [.leftParen]) { + if !currentTokenMatchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForFunctionDeclaration(currentToken) } let parameters = try parseParameters() - if !matchesAny(types: [.rightParen]) { + if !currentTokenMatchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenAfterArguments(currentToken) } - if !matchesAny(types: [.leftBrace]) { + if !currentTokenMatchesAny(types: [.leftBrace]) { throw ParseError.missingOpenBraceBeforeFunctionBody(currentToken) } let functionBody = try parseBlock() @@ -499,7 +499,7 @@ struct Parser { advanceCursor() parameters.append(newParameter) - } while matchesAny(types: [.comma]) + } while currentTokenMatchesAny(types: [.comma]) } return parameters @@ -511,14 +511,14 @@ struct Parser { repeat { let newArg = try parseExpression() args.append(newArg) - } while matchesAny(types: [.comma]) + } while currentTokenMatchesAny(types: [.comma]) } return args } // Other utility methods - mutating private func matchesAny(types: [TokenType]) -> Bool { + mutating private func currentTokenMatchesAny(types: [TokenType]) -> Bool { for type in types { if currentTokenMatches(type: type) { advanceCursor()