diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index 43d7574..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,6 +23,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 /* 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 */; }; 876560072B8827F9002BDE42 /* Scanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876560062B8827F9002BDE42 /* Scanner.swift */; }; @@ -35,6 +39,10 @@ 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 /* UserDefinedFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */; }; + 87C2F3742B91C2BA00126707 /* Return.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* Return.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -50,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 = ""; }; @@ -68,6 +77,9 @@ 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 /* UserDefinedFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefinedFunction.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,16 +124,20 @@ 873CCB322B8ED8B900FC249A /* Environment.swift */, 876A31642B8C04990085A350 /* Expression.swift */, 873CCB202B8D5FAE00FC249A /* Interpreter.swift */, - 876A315E2B897EEB0085A350 /* LoxValue.swift */, 873CCB242B8D765D00FC249A /* Lox.swift */, + 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */, + 876A315E2B897EEB0085A350 /* LoxValue.swift */, + 8732838E2B93F89300E49035 /* NativeFunction.swift */, 876A31682B8C3AAB0085A350 /* ParseError.swift */, 876A31662B8C11810085A350 /* Parser.swift */, + 877168C12B91A9BD00723543 /* Return.swift */, 873CCB222B8D617C00FC249A /* RuntimeError.swift */, 876A31612B8986630085A350 /* ScanError.swift */, 876560062B8827F9002BDE42 /* Scanner.swift */, 873CCB2F2B8EAEC100FC249A /* Statement.swift */, 876560042B8825AC002BDE42 /* Token.swift */, 876560022B882259002BDE42 /* TokenType.swift */, + 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */, ); path = slox; sourceTree = ""; @@ -230,12 +246,16 @@ 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 */, + 877168C22B91A9BD00723543 /* Return.swift in Sources */, 876A31672B8C11810085A350 /* Parser.swift in Sources */, 876560072B8827F9002BDE42 /* Scanner.swift in Sources */, + 87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */, 873CCB332B8ED8B900FC249A /* Environment.swift in Sources */, 876A31652B8C04990085A350 /* Expression.swift in Sources */, 876560032B882259002BDE42 /* TokenType.swift in Sources */, + 8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */, 876A31692B8C3AAB0085A350 /* ParseError.swift in Sources */, 873CCB252B8D765D00FC249A /* Lox.swift in Sources */, ); @@ -250,10 +270,14 @@ 873CCB2C2B8E88A500FC249A /* InterpreterTests.swift in Sources */, 873CCB2E2B8E88D200FC249A /* RuntimeError.swift in Sources */, 873CCB342B8EE0FF00FC249A /* Environment.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 */, 876A315D2B897C020085A350 /* Scanner.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/Expression.swift b/slox/Expression.swift index 8fdb1ad..1b570f3 100644 --- a/slox/Expression.swift +++ b/slox/Expression.swift @@ -13,4 +13,6 @@ indirect enum Expression: Equatable { case variable(Token) 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 350a196..4c3ab91 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -5,30 +5,41 @@ // Created by Danielle Kefford on 2/26/24. // -struct Interpreter { +import Foundation + +class Interpreter { var environment: Environment = Environment() - mutating func interpret(statements: [Statement]) throws { + init() { + setUpGlobals() + } + + private func setUpGlobals() { + for nativeFunction in NativeFunction.allCases { + environment.define(name: String(describing: nativeFunction), + value: .nativeFunction(nativeFunction)) + } + } + + func interpret(statements: [Statement]) throws { for statement in statements { try execute(statement: statement) } } - mutating func interpretRepl(statements: [Statement]) throws -> LoxValue? { - var result: LoxValue? = nil - + func interpretRepl(statements: [Statement]) throws -> LoxValue? { 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 { try execute(statement: statement) } } - return result + return nil } - 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 +56,14 @@ struct Interpreter { environment: Environment(enclosingEnvironment: environment)) case .while(let expr, let stmt): try handleWhileStatement(expr: expr, stmt: stmt) + case .function(let name, let lambda): + try handleFunctionDeclaration(name: name, lambda: lambda) + case .return(let returnToken, let expr): + try handleReturnStatement(returnToken: returnToken, expr: expr) } } - 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 +78,28 @@ struct Interpreter { print(literal) } + private func handleFunctionDeclaration(name: Token, lambda: Expression) throws { + guard case .lambda(let params, let body) = lambda else { + throw RuntimeError.notALambda + } + + let environmentWhenDeclared = self.environment + 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 { + 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 { @@ -72,18 +109,23 @@ 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 + + // 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 } - 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 +147,10 @@ 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 calleeExpr, let rightParen, let args): + return try handleFunctionCallExpression(calleeExpr: calleeExpr, rightParen: rightParen, args: args) + case .lambda(let params, let statements): + return try handleLambdaExpression(params: params, statements: statements) } } @@ -175,7 +221,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 +247,44 @@ struct Interpreter { } } + private func handleFunctionCallExpression(calleeExpr: Expression, + rightParen: Token, + args: [Expression]) throws -> LoxValue { + let callee = try evaluate(expr: calleeExpr) + + let actualFunction: LoxCallable = switch callee { + case .userDefinedFunction(let userDefinedFunction): + userDefinedFunction + case .nativeFunction(let nativeFunction): + nativeFunction + default: + 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) + argValues.append(argValue) + } + + return try actualFunction.call(interpreter: self, args: argValues) + } + + private func handleLambdaExpression(params: [Token], statements: [Statement]) throws -> LoxValue { + let environmentWhenDeclared = self.environment + + let function = UserDefinedFunction(name: "", + params: params, + enclosingEnvironment: environmentWhenDeclared, + body: statements) + + return .userDefinedFunction(function) + } + private func isEqual(leftValue: LoxValue, rightValue: LoxValue) -> Bool { switch (leftValue, rightValue) { case (.nil, .nil): diff --git a/slox/LoxCallable.swift b/slox/LoxCallable.swift new file mode 100644 index 0000000..e16961d --- /dev/null +++ b/slox/LoxCallable.swift @@ -0,0 +1,11 @@ +// +// Callable.swift +// slox +// +// Created by Danielle Kefford on 2/29/24. +// + +protocol LoxCallable { + var arity: Int { get } + func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue +} diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index 2f5c1d1..3a62081 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. @@ -10,6 +10,8 @@ enum LoxValue: CustomStringConvertible, Equatable { case number(Double) case boolean(Bool) case `nil` + case userDefinedFunction(UserDefinedFunction) + case nativeFunction(NativeFunction) var description: String { switch self { @@ -21,6 +23,10 @@ enum LoxValue: CustomStringConvertible, Equatable { return "\(boolean)" case .nil: return "nil" + 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/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..9c90a6b 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -32,13 +32,17 @@ 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 ; // varDecl → "var" IDENTIFIER ( "=" expression )? ";" ; // statement → exprStmt // | forStmt // | ifStmt // | printStmt + // | returnStmt // | whileStmt // | block ; // exprStmt → expression ";" ; @@ -48,16 +52,49 @@ 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 { - if matchesAny(types: [.var]) { + mutating private func parseDeclaration() throws -> Statement { + // 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() + } + + if currentTokenMatchesAny(types: [.var]) { return try parseVariableDeclaration() } return try parseStatement() } + mutating private func parseFunctionDeclaration() throws -> Statement { + guard case .identifier = currentToken.type else { + throw ParseError.missingFunctionName(currentToken) + } + let functionName = currentToken + advanceCursor() + + if !currentTokenMatchesAny(types: [.leftParen]) { + throw ParseError.missingOpenParenForFunctionDeclaration(currentToken) + } + let parameters = try parseParameters() + if !currentTokenMatchesAny(types: [.rightParen]) { + throw ParseError.missingCloseParenAfterArguments(currentToken) + } + + if !currentTokenMatchesAny(types: [.leftBrace]) { + throw ParseError.missingOpenBraceBeforeFunctionBody(currentToken) + } + let functionBody = try parseBlock() + + return .function(functionName, .lambda(parameters, functionBody)) + } + mutating private func parseVariableDeclaration() throws -> Statement { guard case .identifier = currentToken.type else { throw ParseError.missingVariableName(currentToken) @@ -66,35 +103,39 @@ 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); } throw ParseError.missingSemicolon(currentToken) } - mutating func parseStatement() throws -> Statement { - if matchesAny(types: [.for]) { + mutating private func parseStatement() throws -> Statement { + 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: [.while]) { + if currentTokenMatchesAny(types: [.return]) { + return try parseReturnStatement() + } + + if currentTokenMatchesAny(types: [.while]) { return try parseWhileStatement() } - if matchesAny(types: [.leftBrace]) { + if currentTokenMatchesAny(types: [.leftBrace]) { let statements = try parseBlock() return .block(statements) } @@ -102,33 +143,33 @@ struct Parser { return try parseExpressionStatement() } - mutating func parseForStatement() throws -> Statement { - if !matchesAny(types: [.leftParen]) { + mutating private func parseForStatement() throws -> Statement { + 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() } var conditionExpr: Expression? = nil - if !matches(type: .semicolon) { + if !currentTokenMatches(type: .semicolon) { conditionExpr = try parseExpression() } - if !matchesAny(types: [.semicolon]) { + if !currentTokenMatchesAny(types: [.semicolon]) { throw ParseError.missingSemicolonAfterForLoopCondition(currentToken) } var incrementExpr: Expression? = nil - if !matches(type: .rightParen) { + if !currentTokenMatches(type: .rightParen) { incrementExpr = try parseExpression() } - if !matchesAny(types: [.rightParen]) { + if !currentTokenMatchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenForForStatement(currentToken) } @@ -153,42 +194,57 @@ struct Parser { return forStmt } - mutating func parseIfStatement() throws -> Statement { - if !matchesAny(types: [.leftParen]) { + mutating private func parseIfStatement() throws -> Statement { + 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() } return .if(testExpr, consequentStmt, alternativeStmt) } - mutating func parsePrintStatement() throws -> Statement { + mutating private func parsePrintStatement() throws -> Statement { let expr = try parseExpression() - if matchesAny(types: [.semicolon]) { + if currentTokenMatchesAny(types: [.semicolon]) { return .print(expr) } throw ParseError.missingSemicolon(currentToken) } - mutating func parseWhileStatement() throws -> Statement { - if !matchesAny(types: [.leftParen]) { + mutating private func parseReturnStatement() throws -> Statement { + let returnToken = previousToken + + var expr: Expression? = nil + if currentToken.type != .semicolon { + expr = try parseExpression() + } + + if !currentTokenMatchesAny(types: [.semicolon]) { + throw ParseError.missingSemicolon(currentToken) + } + + return .return(returnToken, expr) + } + + mutating private func parseWhileStatement() throws -> Statement { + if !currentTokenMatchesAny(types: [.leftParen]) { throw ParseError.missingOpenParenForWhileStatement(currentToken) } let expr = try parseExpression() - if !matchesAny(types: [.rightParen]) { + if !currentTokenMatchesAny(types: [.rightParen]) { throw ParseError.missingCloseParenForWhileStatement(currentToken) } @@ -196,7 +252,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 { @@ -204,21 +260,21 @@ struct Parser { statements.append(statement) } - if matchesAny(types: [.rightBrace]) { + if currentTokenMatchesAny(types: [.rightBrace]) { return statements } 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, // 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) } @@ -238,10 +294,13 @@ struct Parser { // term → factor ( ( "-" | "+" ) factor )* ; // factor → unary ( ( "/" | "*" ) unary )* ; // unary → ( "!" | "-" ) unary - // | primary ; + // | call ; + // 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() @@ -250,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() @@ -267,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) @@ -279,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) @@ -291,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) @@ -303,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) @@ -315,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) @@ -327,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) @@ -337,55 +396,131 @@ 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) } - return try parsePrimary() + return try parseCall() + } + + mutating private func parseCall() throws -> Expression { + var expr = try parsePrimary() + + while true { + if currentTokenMatchesAny(types: [.leftParen]) { + let args = try parseArguments() + + if !currentTokenMatchesAny(types: [.rightParen]) { + throw ParseError.missingCloseParenAfterArguments(currentToken) + } + + expr = .call(expr, previousToken, args) + } else { + break + } + } + + return expr } mutating private func parsePrimary() throws -> Expression { - if matchesAny(types: [.false]) { + if currentTokenMatchesAny(types: [.fun]) { + return try parseLambda() + } + + 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) } throw ParseError.expectedPrimaryExpression(currentToken) } - mutating private func matchesAny(types: [TokenType]) -> Bool { + mutating private func parseLambda() throws -> Expression { + if !currentTokenMatchesAny(types: [.leftParen]) { + throw ParseError.missingOpenParenForFunctionDeclaration(currentToken) + } + let parameters = try parseParameters() + if !currentTokenMatchesAny(types: [.rightParen]) { + throw ParseError.missingCloseParenAfterArguments(currentToken) + } + + if !currentTokenMatchesAny(types: [.leftBrace]) { + throw ParseError.missingOpenBraceBeforeFunctionBody(currentToken) + } + let functionBody = try parseBlock() + + return .lambda(parameters, functionBody) + } + + // 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 currentTokenMatchesAny(types: [.comma]) + } + + return parameters + } + + mutating private func parseArguments() throws -> [Expression] { + var args: [Expression] = [] + if currentToken.type != .rightParen { + repeat { + let newArg = try parseExpression() + args.append(newArg) + } while currentTokenMatchesAny(types: [.comma]) + } + + return args + } + + // Other utility methods + mutating private func currentTokenMatchesAny(types: [TokenType]) -> Bool { for type in types { - if matches(type: type) { + if currentTokenMatches(type: type) { advanceCursor() return true } @@ -394,7 +529,7 @@ struct Parser { return false } - private func matches(type: TokenType) -> Bool { + private func currentTokenMatches(type: TokenType) -> Bool { if currentToken.type == .eof { return false } @@ -402,6 +537,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/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/RuntimeError.swift b/slox/RuntimeError.swift index 753061e..61a45fe 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -14,6 +14,9 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case binaryOperandsMustBeNumbersOrStrings case unsupportedBinaryOperator case undefinedVariable(String) + case notAFunction + case wrongArity(Int, Int) + case notALambda var description: String { switch self { @@ -29,6 +32,12 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { return "Unsupported binary operator" case .undefinedVariable(let name): 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)" + case .notALambda: + return "Expected lambda as body of function declaration" } } } diff --git a/slox/Statement.swift b/slox/Statement.swift index dea17ac..b49c5a3 100644 --- a/slox/Statement.swift +++ b/slox/Statement.swift @@ -12,4 +12,6 @@ indirect enum Statement: Equatable { case variableDeclaration(Token, Expression?) case block([Statement]) case `while`(Expression, Statement) + case function(Token, Expression) + case `return`(Token, Expression?) } 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 + } +} diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index a938549..6a52117 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,11 +299,223 @@ 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") 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), + .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)), + 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), + .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: 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) + } + + 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 testInterpretLambdaReturnedAsValue() 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) + } + + 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) + } + } } diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index b9ad97b..32ddee2 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -803,4 +803,151 @@ 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), + .lambda( + [], + [.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), + .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) + } + + 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) + } + + 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) + } }