From 69a3f766f9cb3e84fd2b92c6052e6fe2bf1f28ec Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 24 Mar 2024 23:35:33 -0700 Subject: [PATCH 01/14] Can now lex the colon character. --- slox/Scanner.swift | 2 ++ slox/TokenType.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/slox/Scanner.swift b/slox/Scanner.swift index e5897df..84ef33d 100644 --- a/slox/Scanner.swift +++ b/slox/Scanner.swift @@ -84,6 +84,8 @@ struct Scanner { handleSingleCharacterLexeme(type: .star) case "%": handleSingleCharacterLexeme(type: .modulus) + case ":": + handleSingleCharacterLexeme(type: .colon) case "/": handleSlash() diff --git a/slox/TokenType.swift b/slox/TokenType.swift index 3d51c2b..63d3d25 100644 --- a/slox/TokenType.swift +++ b/slox/TokenType.swift @@ -21,6 +21,7 @@ enum TokenType: Equatable { case leftBracket case rightBracket case modulus + case colon // One of two character tokens case bang From 30088244fac804db5ced3ce2b5724fcdcda2eda2 Mon Sep 17 00:00:00 2001 From: quephird Date: Mon, 25 Mar 2024 18:38:09 -0700 Subject: [PATCH 02/14] First somewhat hacky cut at an implementation of dictionaries... BUT OMFG IT WORKS. --- slox.xcodeproj/project.pbxproj | 4 ++ slox/Expression.swift | 52 +++++++++++++++++++++++++ slox/Interpreter.swift | 71 +++++++++++++++++++++++++--------- slox/LoxClass.swift | 2 + slox/LoxDictionary.swift | 41 ++++++++++++++++++++ slox/LoxValue.swift | 44 ++++++++++++++++++++- slox/NativeFunction.swift | 4 +- slox/Parser.swift | 67 ++++++++++++++++++++++++++++---- slox/ResolvedExpression.swift | 52 +++++++++++++++++++++++++ slox/Resolver.swift | 15 +++++++ slox/RuntimeError.swift | 6 +-- slox/Token.swift | 2 +- 12 files changed, 328 insertions(+), 32 deletions(-) create mode 100644 slox/LoxDictionary.swift diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index 6e8cdbc..4928108 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 8702307C2B9575610056FE57 /* ResolvedStatement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283932B95122800E49035 /* ResolvedStatement.swift */; }; 8702307E2B95AA2A0056FE57 /* ResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8702307D2B95AA2A0056FE57 /* ResolverTests.swift */; }; 870230802B96E9580056FE57 /* LoxClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8702307F2B96E9580056FE57 /* LoxClass.swift */; }; + 8730DDE92BB250CE00372548 /* LoxDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8730DDE82BB250CE00372548 /* LoxDictionary.swift */; }; 8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732838E2B93F89300E49035 /* NativeFunction.swift */; }; 873283902B93FC0900E49035 /* NativeFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8732838E2B93F89300E49035 /* NativeFunction.swift */; }; 873283922B95118A00E49035 /* ResolvedExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873283912B95118A00E49035 /* ResolvedExpression.swift */; }; @@ -79,6 +80,7 @@ 870230762B9574A90056FE57 /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; 8702307D2B95AA2A0056FE57 /* ResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolverTests.swift; sourceTree = ""; }; 8702307F2B96E9580056FE57 /* LoxClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxClass.swift; sourceTree = ""; }; + 8730DDE82BB250CE00372548 /* LoxDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxDictionary.swift; sourceTree = ""; }; 8732838E2B93F89300E49035 /* NativeFunction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeFunction.swift; sourceTree = ""; }; 873283912B95118A00E49035 /* ResolvedExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolvedExpression.swift; sourceTree = ""; }; 873283932B95122800E49035 /* ResolvedStatement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolvedStatement.swift; sourceTree = ""; }; @@ -156,6 +158,7 @@ 873CCB242B8D765D00FC249A /* Lox.swift */, 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */, 8702307F2B96E9580056FE57 /* LoxClass.swift */, + 8730DDE82BB250CE00372548 /* LoxDictionary.swift */, 87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */, 87777E3B2BA0FF70002E38F2 /* LoxList.swift */, 876A315E2B897EEB0085A350 /* LoxValue.swift */, @@ -301,6 +304,7 @@ 8732838F2B93F89300E49035 /* NativeFunction.swift in Sources */, 873283922B95118A00E49035 /* ResolvedExpression.swift in Sources */, 876A31692B8C3AAB0085A350 /* ParseError.swift in Sources */, + 8730DDE92BB250CE00372548 /* LoxDictionary.swift in Sources */, 870230752B9571490056FE57 /* MutableCollection+Extension.swift in Sources */, 873CCB252B8D765D00FC249A /* Lox.swift in Sources */, ); diff --git a/slox/Expression.swift b/slox/Expression.swift index 677cd5a..4af1602 100644 --- a/slox/Expression.swift +++ b/slox/Expression.swift @@ -22,4 +22,56 @@ indirect enum Expression: Equatable { case list([Expression]) case subscriptGet(Expression, Expression) case subscriptSet(Expression, Expression, Expression) + case dictionary([(Expression, Expression)]) + + static func == (lhs: Expression, rhs: Expression) -> Bool { + switch (lhs, rhs) { + case (.binary(let lhsExpr1, let lhsOper, let lhsExpr2), .binary(let rhsExpr1, let rhsOper, let rhsExpr2)): + return lhsExpr1 == rhsExpr1 && lhsOper == rhsOper && lhsExpr2 == rhsExpr2 + case (.unary(let lhsOper, let lhsExpr), .unary(let rhsOper, let rhsExpr)): + return lhsOper == rhsOper && lhsExpr == rhsExpr + case (.literal(let lhsValue), .literal(let rhsValue)): + return lhsValue == rhsValue + case (.grouping(let lhsExpr), .grouping(let rhsExpr)): + return lhsExpr == rhsExpr + case (.variable(let lhsToken), .variable(let rhsToken)): + return lhsToken == rhsToken + case (.assignment(let lhsName, let lhsExpr), .assignment(let rhsName, let rhsExpr)): + return lhsName == rhsName && lhsExpr == rhsExpr + case (.logical(let lhsExpr1, let lhsOper, let lhsExpr2), .logical(let rhsExpr1, let rhsOper, let rhsExpr2)): + return lhsExpr1 == rhsExpr1 && lhsOper == rhsOper && lhsExpr2 == rhsExpr2 + case (.call(let lhsCallee, let lhsToken, let lhsArgs), .call(let rhsCallee, let rhsToken, let rhsArgs)): + return lhsCallee == rhsCallee && lhsToken == rhsToken && lhsArgs == rhsArgs + case (.lambda(let lhsParams, let lhsBody), .lambda(let rhsParams, let rhsBody)): + return lhsParams == rhsParams && lhsBody == rhsBody + case (.get(let lhsExpr, let lhsName), .get(let rhsExpr, let rhsName)): + return lhsExpr == rhsExpr && lhsName == rhsName + case (.set(let lhsExpr1, let lhsName, let lhsExpr2), .set(let rhsExpr1, let rhsName, let rhsExpr2)): + return lhsExpr1 == rhsExpr1 && lhsName == rhsName && lhsExpr2 == rhsExpr2 + case (.this(let lhsToken), .this(let rhsToken)): + return lhsToken == rhsToken + case (.super(let lhsSuper, let lhsMethod), .super(let rhsSuper, let rhsMethod)): + return lhsSuper == rhsSuper && lhsMethod == rhsMethod + case (.list(let lhsExprs), .list(let rhsExprs)): + return lhsExprs == rhsExprs + case (.subscriptGet(let lhsList, let lhsIdx), .subscriptGet(let rhsList, let rhsIdx)): + return lhsList == rhsList && lhsIdx == rhsIdx + case (.subscriptSet(let lhsList, let lhsIdx, let lhsExpr), .subscriptSet(let rhsList, let rhsIdx, let rhsExpr)): + return lhsList == rhsList && lhsIdx == rhsIdx && lhsExpr == rhsExpr + case (.dictionary(let lhsKVPairs), .dictionary(let rhsKVPairs)): + if lhsKVPairs.count != rhsKVPairs.count { + return false + } + + for ((lhsKey, lhsValue), (rhsKey, rhsValue)) in zip(lhsKVPairs, rhsKVPairs) { + if lhsKey != rhsKey || lhsValue != rhsValue { + return false + } + } + + return true + default: + return false + } + } } diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 2a3f822..268af0b 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -45,6 +45,8 @@ class Interpreter { return result; } } + + class Dictionary {} """ var environment: Environment = Environment() @@ -350,11 +352,13 @@ class Interpreter { case .list(let elements): return try handleListExpression(elements: elements) case .subscriptGet(let listExpr, let indexExpr): - return try handleSubscriptGetExpression(listExpr: listExpr, indexExpr: indexExpr) + return try handleSubscriptGetExpression(collectionExpr: listExpr, indexExpr: indexExpr) case .subscriptSet(let listExpr, let indexExpr, let valueExpr): - return try handleSubscriptSetExpression(listExpr: listExpr, + return try handleSubscriptSetExpression(collectionExpr: listExpr, indexExpr: indexExpr, valueExpr: valueExpr) + case .dictionary(let kvPairs): + return try handleDictionary(kvExprPairs: kvPairs) } } @@ -595,34 +599,65 @@ class Interpreter { return try makeList(elements: elementValues) } - private func handleSubscriptGetExpression(listExpr: ResolvedExpression, + private func handleSubscriptGetExpression(collectionExpr: ResolvedExpression, indexExpr: ResolvedExpression) throws -> LoxValue { - guard case .instance(let list as LoxList) = try evaluate(expr: listExpr) else { - throw RuntimeError.notAList - } + let collection = try evaluate(expr: collectionExpr) - guard case .int(let index) = try evaluate(expr: indexExpr) else { - throw RuntimeError.indexMustBeAnInteger - } + switch collection { + case .instance(let list as LoxList): + guard case .int(let index) = try evaluate(expr: indexExpr) else { + throw RuntimeError.indexMustBeAnInteger + } + + return list[Int(index)] + case .instance(let dictionary as LoxDictionary): + let key = try evaluate(expr: indexExpr) - return list[Int(index)] + return dictionary[key] + default: + throw RuntimeError.notAListOrDictionary + } } - private func handleSubscriptSetExpression(listExpr: ResolvedExpression, + private func handleSubscriptSetExpression(collectionExpr: ResolvedExpression, indexExpr: ResolvedExpression, valueExpr: ResolvedExpression) throws -> LoxValue { - guard case .instance(let list as LoxList) = try evaluate(expr: listExpr) else { - throw RuntimeError.notAList + let collection = try evaluate(expr: collectionExpr) + let value = try evaluate(expr: valueExpr) + + switch collection { + case .instance(let list as LoxList): + guard case .int(let index) = try evaluate(expr: indexExpr) else { + throw RuntimeError.indexMustBeAnInteger + } + + list[Int(index)] = value + case .instance(let dictionary as LoxDictionary): + let key = try evaluate(expr: indexExpr) + + dictionary[key] = value + default: + throw RuntimeError.notAListOrDictionary } - guard case .int(let index) = try evaluate(expr: indexExpr) else { - throw RuntimeError.indexMustBeAnInteger + return value + } + + private func handleDictionary(kvExprPairs: [(ResolvedExpression, ResolvedExpression)]) throws -> LoxValue { + var kvPairs: [LoxValue: LoxValue] = [:] + + for (keyExpr, valueExpr) in kvExprPairs { + let key = try evaluate(expr: keyExpr) + let value = try evaluate(expr: valueExpr) + kvPairs[key] = value } - let value = try evaluate(expr: valueExpr) + guard case .instance(let dictionaryClass as LoxClass) = try environment.getValue(name: "Dictionary") else { + fatalError() + } - list[Int(index)] = value - return value + let dictionary = LoxDictionary(kvPairs: kvPairs, klass: dictionaryClass) + return .instance(dictionary) } func makeList(elements: [LoxValue]) throws -> LoxValue { diff --git a/slox/LoxClass.swift b/slox/LoxClass.swift index 005e58b..27fe582 100644 --- a/slox/LoxClass.swift +++ b/slox/LoxClass.swift @@ -19,6 +19,8 @@ class LoxClass: LoxInstance, LoxCallable { var instanceType: LoxInstance.Type { if self.name == "List" { LoxList.self + } else if self.name == "Dictionary" { + LoxDictionary.self } else { LoxInstance.self } diff --git a/slox/LoxDictionary.swift b/slox/LoxDictionary.swift new file mode 100644 index 0000000..ccb02fb --- /dev/null +++ b/slox/LoxDictionary.swift @@ -0,0 +1,41 @@ +// +// LoxDictionary.swift +// slox +// +// Created by Danielle Kefford on 3/25/24. +// + +class LoxDictionary: LoxInstance { + var kvPairs: [LoxValue: LoxValue] + + convenience init(kvPairs: [LoxValue: LoxValue], klass: LoxClass) { + self.init(klass: klass) + self.kvPairs = kvPairs + } + + required init(klass: LoxClass?) { + self.kvPairs = [:] + super.init(klass: klass) + } + + override func get(propertyName: String) throws -> LoxValue { + switch propertyName { + default: + return try super.get(propertyName: propertyName) + } + } + + override func set(propertyName: String, propertyValue: LoxValue) throws { + throw RuntimeError.onlyInstancesHaveProperties + } + + // TODO: Need to think about how to handle invalid indices!!! + subscript(key: LoxValue) -> LoxValue { + get { + return kvPairs[key] ?? .nil + } + set(newValue) { + kvPairs[key] = newValue + } + } +} diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index 311156a..a0bf1c9 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -5,7 +5,7 @@ // Created by Danielle Kefford on 2/23/24. // -enum LoxValue: CustomStringConvertible, Equatable { +enum LoxValue: CustomStringConvertible, Equatable, Hashable { case string(String) case double(Double) case int(Int) @@ -43,6 +43,17 @@ enum LoxValue: CustomStringConvertible, Equatable { } string.append("]") return string + case .instance(let list as LoxDictionary): + var string = "[" + for (i, kvPair) in list.kvPairs.enumerated() { + if i > 0 { + string.append(", ") + } + let (key, value) = kvPair + string.append("\(key): \(value)") + } + string.append("]") + return string case .instance(let instance): return "" } @@ -127,4 +138,35 @@ enum LoxValue: CustomStringConvertible, Equatable { return false } } + + // TODO: Check with Becca if this is even remotely sensible + func hash(into hasher: inout Hasher) { + switch self { + case .string(let string): + hasher.combine("string") + hasher.combine(string) + case .double(let double): + hasher.combine("double") + hasher.combine(double) + case .int(let int): + hasher.combine("int") + hasher.combine(int) + case .boolean(let boolean): + hasher.combine("boolean") + hasher.combine(boolean) + case .nil: + hasher.combine("nil") + case .userDefinedFunction(let userDefinedFunction): + hasher.combine("userDefinedFunction") + hasher.combine(userDefinedFunction.name) + hasher.combine(userDefinedFunction.params) + case .nativeFunction(let nativeFunction): + hasher.combine("nativeFunction") + hasher.combine(nativeFunction.hashValue) + case .instance(let instance): + hasher.combine("instance") + hasher.combine(instance.klass.name) + hasher.combine(instance.properties) + } + } } diff --git a/slox/NativeFunction.swift b/slox/NativeFunction.swift index 542bd4a..0580758 100644 --- a/slox/NativeFunction.swift +++ b/slox/NativeFunction.swift @@ -29,7 +29,7 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return .double(Date().timeIntervalSince1970) case .appendNative: guard case .instance(let loxList as LoxList) = args[0] else { - throw RuntimeError.notAList + throw RuntimeError.notAListOrDictionary } let element = args[1] @@ -38,7 +38,7 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return .nil case .deleteAtNative: guard case .instance(let loxList as LoxList) = args[0] else { - throw RuntimeError.notAList + throw RuntimeError.notAListOrDictionary } guard case .int(let index) = args[1] else { diff --git a/slox/Parser.swift b/slox/Parser.swift index 1cf3792..219f76e 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -410,7 +410,7 @@ struct Parser { // postfix → primary ( "(" arguments? ")" | "." IDENTIFIER | "[" logicOr "]" )* ; // primary → NUMBER | STRING | "true" | "false" | "nil" // | "(" expression ")" - // | "[" arguments? "]" + // | "[" ( arguments? | kvPairs? ) "]" // | "this" // | IDENTIFIER // | lambda @@ -628,8 +628,8 @@ struct Parser { return groupingExpr } - if let listExpr = try parseListExpression() { - return listExpr + if let collectionExpr = try parseCollectionExpression() { + return collectionExpr } if let superExpr = try parseSuperExpression() { @@ -661,17 +661,34 @@ struct Parser { return .grouping(expr) } - mutating private func parseListExpression() throws -> Expression? { + mutating private func parseCollectionExpression() throws -> Expression? { guard currentTokenMatchesAny(types: [.leftBracket]) else { return nil } - let elements = try parseArguments(endTokenType: .rightBracket) + // [ ] + // [ :] + // [ "a"] + // [ "a" : 1] + // [ "a", "b"] + // [ "a" : 1, "b" : 2] - guard currentTokenMatchesAny(types: [.rightBracket]) else { - throw ParseError.missingClosingBracket(previousToken) + if currentTokenMatchesAny(types: [.rightBracket]) { + return .list([]) + } + + if currentTokenMatchesAny(types: [.colon]) && currentTokenMatchesAny(types: [.rightBracket]) { + return .dictionary([]) + } + + let firstExpr = try parseExpression() + + if currentTokenMatchesAny(types: [.colon]) { + let kvPairs = try parseKeyValuePairs(firstKeyExpr: firstExpr) + return .dictionary(kvPairs) } + let elements = try parseExpressionList(firstExpr: firstExpr) return .list(elements) } @@ -716,6 +733,7 @@ struct Parser { // // parameters → IDENTIFIER ( "," IDENTIFIER )* ; // arguments → expression ( "," expression )* ; + // kvPairs → ( expression ":" expression ) ( expression ":" expression )* ; // mutating private func parseParameters() throws -> [Token] { var parameters: [Token] = [] @@ -732,6 +750,41 @@ struct Parser { return parameters } + mutating private func parseExpressionList(firstExpr: Expression) throws -> [Expression] { + var exprs: [Expression] = [firstExpr] + + while currentTokenMatchesAny(types: [.comma]) { + let expr = try parseExpression() + exprs.append(expr) + } + + guard currentTokenMatchesAny(types: [.rightBracket]) else { + throw ParseError.missingClosingBracket(previousToken) + } + + return exprs + } + + + mutating private func parseKeyValuePairs(firstKeyExpr: Expression) throws -> [(Expression, Expression)] { + let firstValueExpr = try parseExpression() + + var kvPairs = [(firstKeyExpr, firstValueExpr)] + + while currentTokenMatchesAny(types: [.comma]) { + let keyExpr = try parseExpression() + _ = consumeToken(type: .colon) + let valExpr = try parseExpression() + kvPairs.append((keyExpr, valExpr)) + } + + guard currentTokenMatchesAny(types: [.rightBracket]) else { + throw ParseError.missingClosingBracket(previousToken) + } + + return kvPairs + } + mutating private func parseArguments(endTokenType: TokenType) throws -> [Expression] { var args: [Expression] = [] if currentToken.type != endTokenType { diff --git a/slox/ResolvedExpression.swift b/slox/ResolvedExpression.swift index 0bedd5f..71762f0 100644 --- a/slox/ResolvedExpression.swift +++ b/slox/ResolvedExpression.swift @@ -22,4 +22,56 @@ indirect enum ResolvedExpression: Equatable { case list([ResolvedExpression]) case subscriptGet(ResolvedExpression, ResolvedExpression) case subscriptSet(ResolvedExpression, ResolvedExpression, ResolvedExpression) + case dictionary([(ResolvedExpression, ResolvedExpression)]) + + static func == (lhs: ResolvedExpression, rhs: ResolvedExpression) -> Bool { + switch (lhs, rhs) { + case (.binary(let lhsExpr1, let lhsOper, let lhsExpr2), .binary(let rhsExpr1, let rhsOper, let rhsExpr2)): + return lhsExpr1 == rhsExpr1 && lhsOper == rhsOper && lhsExpr2 == rhsExpr2 + case (.unary(let lhsOper, let lhsExpr), .unary(let rhsOper, let rhsExpr)): + return lhsOper == rhsOper && lhsExpr == rhsExpr + case (.literal(let lhsValue), .literal(let rhsValue)): + return lhsValue == rhsValue + case (.grouping(let lhsExpr), .grouping(let rhsExpr)): + return lhsExpr == rhsExpr + case (.variable(let lhsToken, let lhsDepth), .variable(let rhsToken, let rhsDepth)): + return lhsToken == rhsToken && lhsDepth == rhsDepth + case (.assignment(let lhsName, let lhsExpr, let lhsDepth), .assignment(let rhsName, let rhsExpr, let rhsDepth)): + return lhsName == rhsName && lhsExpr == rhsExpr && lhsDepth == rhsDepth + case (.logical(let lhsExpr1, let lhsOper, let lhsExpr2), .logical(let rhsExpr1, let rhsOper, let rhsExpr2)): + return lhsExpr1 == rhsExpr1 && lhsOper == rhsOper && lhsExpr2 == rhsExpr2 + case (.call(let lhsCallee, let lhsToken, let lhsArgs), .call(let rhsCallee, let rhsToken, let rhsArgs)): + return lhsCallee == rhsCallee && lhsToken == rhsToken && lhsArgs == rhsArgs + case (.lambda(let lhsParams, let lhsBody), .lambda(let rhsParams, let rhsBody)): + return lhsParams == rhsParams && lhsBody == rhsBody + case (.get(let lhsExpr, let lhsName), .get(let rhsExpr, let rhsName)): + return lhsExpr == rhsExpr && lhsName == rhsName + case (.set(let lhsExpr1, let lhsName, let lhsExpr2), .set(let rhsExpr1, let rhsName, let rhsExpr2)): + return lhsExpr1 == rhsExpr1 && lhsName == rhsName && lhsExpr2 == rhsExpr2 + case (.this(let lhsToken, let lhsDepth), .this(let rhsToken, let rhsDepth)): + return lhsToken == rhsToken && lhsDepth == rhsDepth + case (.super(let lhsSuper, let lhsMethod, let lhsDepth), .super(let rhsSuper, let rhsMethod, let rhsDepth)): + return lhsSuper == rhsSuper && lhsMethod == rhsMethod && lhsDepth == rhsDepth + case (.list(let lhsExprs), .list(let rhsExprs)): + return lhsExprs == rhsExprs + case (.subscriptGet(let lhsList, let lhsIdx), .subscriptGet(let rhsList, let rhsIdx)): + return lhsList == rhsList && lhsIdx == rhsIdx + case (.subscriptSet(let lhsList, let lhsIdx, let lhsExpr), .subscriptSet(let rhsList, let rhsIdx, let rhsExpr)): + return lhsList == rhsList && lhsIdx == rhsIdx && lhsExpr == rhsExpr + case (.dictionary(let lhsKVPairs), .dictionary(let rhsKVPairs)): + if lhsKVPairs.count != rhsKVPairs.count { + return false + } + + for ((lhsKey, lhsValue), (rhsKey, rhsValue)) in zip(lhsKVPairs, rhsKVPairs) { + if lhsKey != rhsKey || lhsValue != rhsValue { + return false + } + } + + return true + default: + return false + } + } } diff --git a/slox/Resolver.swift b/slox/Resolver.swift index 6d92fa7..5c9e178 100644 --- a/slox/Resolver.swift +++ b/slox/Resolver.swift @@ -334,6 +334,8 @@ struct Resolver { return try handleSubscriptGet(listExpr: listExpr, indexExpr: indexExpr) case .subscriptSet(let listExpr, let indexExpr, let valueExpr): return try handleSubscriptSet(listExpr: listExpr, indexExpr: indexExpr, valueExpr: valueExpr) + case .dictionary(let kvPairs): + return try handleDictionary(kvPairs: kvPairs) } } @@ -487,6 +489,19 @@ struct Resolver { return .subscriptSet(resolvedListExpr, resolvedIndexExpr, resolvedValueExpr) } + mutating private func handleDictionary(kvPairs: [(Expression, Expression)]) throws -> ResolvedExpression { + var resolvedKVPairs: [(ResolvedExpression, ResolvedExpression)] = [] + + for (keyExpr, valueExpr) in kvPairs { + let resolvedKey = try resolve(expression: keyExpr) + let resolvedValue = try resolve(expression: valueExpr) + resolvedKVPairs.append((resolvedKey, resolvedValue)) + } + + return .dictionary(resolvedKVPairs) + } + + // Internal helpers mutating private func beginScope() { scopeStack.append([:]) diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index 19c4fa2..8ca5513 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -17,7 +17,7 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case notAFunctionDeclaration case notACallableObject case notAnInstance - case notAList + case notAListOrDictionary case notANumber case onlyInstancesHaveProperties case undefinedProperty(String) @@ -47,8 +47,8 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { return "Error: expected a callable object" case .notAnInstance: return "Error: expected an instance" - case .notAList: - return "Error: expected a list" + case .notAListOrDictionary: + return "Error: expected a list or dictionary" case .notANumber: return "Error: expected a number" case .onlyInstancesHaveProperties: diff --git a/slox/Token.swift b/slox/Token.swift index aec80ff..9e9e150 100644 --- a/slox/Token.swift +++ b/slox/Token.swift @@ -5,7 +5,7 @@ // Created by Danielle Kefford on 2/22/24. // -struct Token: CustomStringConvertible, Equatable { +struct Token: CustomStringConvertible, Equatable, Hashable { let type: TokenType let lexeme: String let line: Int From 93740230ee6d50d839c6e0161a3baf26b3a88783 Mon Sep 17 00:00:00 2001 From: quephird Date: Tue, 26 Mar 2024 13:23:15 -0700 Subject: [PATCH 03/14] Added `count` and `removeValue()`. --- slox.xcodeproj/project.pbxproj | 2 ++ slox/Interpreter.swift | 6 +++++- slox/LoxDictionary.swift | 3 ++- slox/NativeFunction.swift | 11 +++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index 4928108..db5bfa4 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; 87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */; }; 87C2F3742B91C2BA00126707 /* JumpType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* JumpType.swift */; }; + 87CB69762BB35EBA002A2E69 /* LoxDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8730DDE82BB250CE00372548 /* LoxDictionary.swift */; }; 87EEDFBE2B96F52D00C7FE6D /* LoxInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */; }; 87EEDFBF2B9920F900C7FE6D /* LoxClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8702307F2B96E9580056FE57 /* LoxClass.swift */; }; 87EEDFC02B9920FD00C7FE6D /* LoxInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */; }; @@ -329,6 +330,7 @@ 876A31602B89827B0085A350 /* LoxValue.swift in Sources */, 873CCB312B8EBB7800FC249A /* Statement.swift in Sources */, 8755B8B52B91984C00530DC4 /* LoxCallable.swift in Sources */, + 87CB69762BB35EBA002A2E69 /* LoxDictionary.swift in Sources */, 87777E3D2BA1015F002E38F2 /* LoxList.swift in Sources */, 873283902B93FC0900E49035 /* NativeFunction.swift in Sources */, 8702307B2B95755E0056FE57 /* ResolvedExpression.swift in Sources */, diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 268af0b..ce35358 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -46,7 +46,11 @@ class Interpreter { } } - class Dictionary {} + class Dictionary { + removeValue(key) { + return removeValueNative(this, key); + } + } """ var environment: Environment = Environment() diff --git a/slox/LoxDictionary.swift b/slox/LoxDictionary.swift index ccb02fb..b1b23a4 100644 --- a/slox/LoxDictionary.swift +++ b/slox/LoxDictionary.swift @@ -20,6 +20,8 @@ class LoxDictionary: LoxInstance { override func get(propertyName: String) throws -> LoxValue { switch propertyName { + case "count": + return .int(self.kvPairs.count) default: return try super.get(propertyName: propertyName) } @@ -29,7 +31,6 @@ class LoxDictionary: LoxInstance { throw RuntimeError.onlyInstancesHaveProperties } - // TODO: Need to think about how to handle invalid indices!!! subscript(key: LoxValue) -> LoxValue { get { return kvPairs[key] ?? .nil diff --git a/slox/NativeFunction.swift b/slox/NativeFunction.swift index 0580758..712e72e 100644 --- a/slox/NativeFunction.swift +++ b/slox/NativeFunction.swift @@ -11,6 +11,7 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { case clock case appendNative case deleteAtNative + case removeValueNative var arity: Int { switch self { @@ -20,6 +21,8 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return 2 case .deleteAtNative: return 2 + case .removeValueNative: + return 2 } } @@ -46,6 +49,14 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { } return loxList.elements.remove(at: Int(index)) + case .removeValueNative: + guard case .instance(let loxDictionary as LoxDictionary) = args[0] else { + throw RuntimeError.notAListOrDictionary + } + + let key = args[1] + + return loxDictionary.kvPairs.removeValue(forKey: key) ?? .nil } } } From ac457e704e9d8d2605b022897f087221bbb087c4 Mon Sep 17 00:00:00 2001 From: quephird Date: Tue, 26 Mar 2024 14:52:55 -0700 Subject: [PATCH 04/14] Moved standard library to top-level string declared in separate file. --- slox.xcodeproj/project.pbxproj | 4 +++ slox/Interpreter.swift | 46 +----------------------------- slox/StandardLibrary.swift | 51 ++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 45 deletions(-) create mode 100644 slox/StandardLibrary.swift diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index db5bfa4..8908bc0 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -55,6 +55,7 @@ 877168C22B91A9BD00723543 /* JumpType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* JumpType.swift */; }; 87777E3C2BA0FF70002E38F2 /* LoxList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87777E3B2BA0FF70002E38F2 /* LoxList.swift */; }; 87777E3D2BA1015F002E38F2 /* LoxList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87777E3B2BA0FF70002E38F2 /* LoxList.swift */; }; + 8792A9992BB36C66009842D8 /* StandardLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8792A9982BB36C66009842D8 /* StandardLibrary.swift */; }; 87BAFC492B9179CB0013E5FE /* LoxCallable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC482B9179CB0013E5FE /* LoxCallable.swift */; }; 87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BAFC4A2B918C520013E5FE /* UserDefinedFunction.swift */; }; 87C2F3742B91C2BA00126707 /* JumpType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 877168C12B91A9BD00723543 /* JumpType.swift */; }; @@ -107,6 +108,7 @@ 876A31682B8C3AAB0085A350 /* ParseError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseError.swift; sourceTree = ""; }; 877168C12B91A9BD00723543 /* JumpType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpType.swift; sourceTree = ""; }; 87777E3B2BA0FF70002E38F2 /* LoxList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxList.swift; sourceTree = ""; }; + 8792A9982BB36C66009842D8 /* StandardLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardLibrary.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 = ""; }; 87EEDFBD2B96F52D00C7FE6D /* LoxInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoxInstance.swift; sourceTree = ""; }; @@ -174,6 +176,7 @@ 873CCB222B8D617C00FC249A /* RuntimeError.swift */, 876A31612B8986630085A350 /* ScanError.swift */, 876560062B8827F9002BDE42 /* Scanner.swift */, + 8792A9982BB36C66009842D8 /* StandardLibrary.swift */, 873CCB2F2B8EAEC100FC249A /* Statement.swift */, 876560042B8825AC002BDE42 /* Token.swift */, 876560022B882259002BDE42 /* TokenType.swift */, @@ -297,6 +300,7 @@ 876A31672B8C11810085A350 /* Parser.swift in Sources */, 876560072B8827F9002BDE42 /* Scanner.swift in Sources */, 87BAFC4B2B918C520013E5FE /* UserDefinedFunction.swift in Sources */, + 8792A9992BB36C66009842D8 /* StandardLibrary.swift in Sources */, 873CCB332B8ED8B900FC249A /* Environment.swift in Sources */, 876A31652B8C04990085A350 /* Expression.swift in Sources */, 876560032B882259002BDE42 /* TokenType.swift in Sources */, diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index ce35358..1151ac3 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -8,50 +8,6 @@ import Foundation class Interpreter { - static let standardLibrary = """ - class List { - append(element) { - appendNative(this, element); - } - - deleteAt(index) { - return deleteAtNative(this, index); - } - - map(fn) { - var result = []; - for (var i = 0; i < this.count; i = i + 1) { - var newElement = fn(this[i]); - result.append(newElement); - } - return result; - } - - filter(fn) { - var result = []; - for (var i = 0; i < this.count; i = i + 1) { - if (fn(this[i])) { - result.append(this[i]); - } - } - return result; - } - - reduce(initial, fn) { - var result = initial; - for (var i = 0; i < this.count; i = i + 1) { - result = fn(result, this[i]); - } - return result; - } - } - - class Dictionary { - removeValue(key) { - return removeValueNative(this, key); - } - } -""" var environment: Environment = Environment() init() { @@ -74,7 +30,7 @@ class Interpreter { value: .nativeFunction(nativeFunction)) } - try! interpret(source: Self.standardLibrary) + try! interpret(source: standardLibrary) } func interpret(source: String) throws { diff --git a/slox/StandardLibrary.swift b/slox/StandardLibrary.swift new file mode 100644 index 0000000..26e15be --- /dev/null +++ b/slox/StandardLibrary.swift @@ -0,0 +1,51 @@ +// +// StandardLibrary.swift +// slox +// +// Created by Danielle Kefford on 3/26/24. +// + +let standardLibrary = """ +class List { + append(element) { + appendNative(this, element); + } + + deleteAt(index) { + return deleteAtNative(this, index); + } + + map(fn) { + var result = []; + for (var i = 0; i < this.count; i = i + 1) { + var newElement = fn(this[i]); + result.append(newElement); + } + return result; + } + + filter(fn) { + var result = []; + for (var i = 0; i < this.count; i = i + 1) { + if (fn(this[i])) { + result.append(this[i]); + } + } + return result; + } + + reduce(initial, fn) { + var result = initial; + for (var i = 0; i < this.count; i = i + 1) { + result = fn(result, this[i]); + } + return result; + } +} + +class Dictionary { + removeValue(key) { + return removeValueNative(this, key); + } +} +""" From 8f1d26751cb844c10caf973d483e1cd0052a7f13 Mon Sep 17 00:00:00 2001 From: quephird Date: Wed, 27 Mar 2024 15:42:15 -0700 Subject: [PATCH 05/14] `UserDefinedFunction` and `LoxInstance` now get assigned UUIDs upon instantiation so that hashing `LoxValue` makes more sense. --- slox/LoxInstance.swift | 4 ++++ slox/LoxValue.swift | 24 ++++++++++-------------- slox/UserDefinedFunction.swift | 16 ++++++++++++++++ 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/slox/LoxInstance.swift b/slox/LoxInstance.swift index 4dd3fbc..d2c1677 100644 --- a/slox/LoxInstance.swift +++ b/slox/LoxInstance.swift @@ -5,6 +5,8 @@ // Created by Danielle Kefford on 3/4/24. // +import Foundation + class LoxInstance { // `klass` is what is used in the interpreter when we need // to know the class of a particular instance. Every Lox @@ -24,12 +26,14 @@ class LoxInstance { return _klass! } var properties: [String: LoxValue] = [:] + var objectId: UUID /// - Parameter klass: The class this instance belongs to. /// Use `nil` if this instance *is* a class; the `klass` property /// will then instantiate a metaclass for it on demand. required init(klass: LoxClass?) { self._klass = klass + self.objectId = UUID() } func get(propertyName: String) throws -> LoxValue { diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index a0bf1c9..d40922e 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -117,7 +117,6 @@ enum LoxValue: CustomStringConvertible, Equatable, Hashable { } } - // NOTA BENE: This equality conformance is only for unit tests static func == (lhs: LoxValue, rhs: LoxValue) -> Bool { switch (lhs, rhs) { case (.string(let lhsString), .string(let rhsString)): @@ -132,6 +131,10 @@ enum LoxValue: CustomStringConvertible, Equatable, Hashable { return lhsBoolean == rhsBoolean case (.nil, .nil): return true + case (.userDefinedFunction(let leftFunc), .userDefinedFunction(let rightFunc)): + return leftFunc.objectId == rightFunc.objectId + case (.nativeFunction(let leftFunc), .nativeFunction(let rightFunc)): + return leftFunc == rightFunc case (.instance(let leftList as LoxList), .instance(let rightList as LoxList)): return leftList.elements == rightList.elements default: @@ -140,33 +143,26 @@ enum LoxValue: CustomStringConvertible, Equatable, Hashable { } // TODO: Check with Becca if this is even remotely sensible + // especially with the function and instance cases, as they + // don't make much sense as candidates for keys func hash(into hasher: inout Hasher) { switch self { case .string(let string): - hasher.combine("string") hasher.combine(string) case .double(let double): - hasher.combine("double") hasher.combine(double) case .int(let int): - hasher.combine("int") hasher.combine(int) case .boolean(let boolean): - hasher.combine("boolean") hasher.combine(boolean) case .nil: - hasher.combine("nil") + break case .userDefinedFunction(let userDefinedFunction): - hasher.combine("userDefinedFunction") - hasher.combine(userDefinedFunction.name) - hasher.combine(userDefinedFunction.params) + hasher.combine(userDefinedFunction.objectId) case .nativeFunction(let nativeFunction): - hasher.combine("nativeFunction") - hasher.combine(nativeFunction.hashValue) + hasher.combine(nativeFunction) case .instance(let instance): - hasher.combine("instance") - hasher.combine(instance.klass.name) - hasher.combine(instance.properties) + hasher.combine(instance.objectId) } } } diff --git a/slox/UserDefinedFunction.swift b/slox/UserDefinedFunction.swift index 5e8749f..60c1ae1 100644 --- a/slox/UserDefinedFunction.swift +++ b/slox/UserDefinedFunction.swift @@ -5,6 +5,8 @@ // Created by Danielle Kefford on 2/29/24. // +import Foundation + struct UserDefinedFunction: LoxCallable, Equatable { var name: String var params: [Token]? @@ -21,6 +23,20 @@ struct UserDefinedFunction: LoxCallable, Equatable { var isComputedProperty: Bool { return params == nil } + var objectId: UUID + + init(name: String, + params: [Token]?, + enclosingEnvironment: Environment, + body: [ResolvedStatement], + isInitializer: Bool) { + self.name = name + self.params = params + self.enclosingEnvironment = enclosingEnvironment + self.body = body + self.isInitializer = isInitializer + self.objectId = UUID() + } func call(interpreter: Interpreter, args: [LoxValue]) throws -> LoxValue { let newEnvironment = Environment(enclosingEnvironment: enclosingEnvironment) From 3fb53e4be318b8033ee22f0b46654b2936ae8ae5 Mon Sep 17 00:00:00 2001 From: quephird Date: Thu, 28 Mar 2024 14:09:16 -0700 Subject: [PATCH 06/14] OOOPS... had to fix `==` for pure instances and dictionaries. --- slox/LoxValue.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index d40922e..ae48379 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -137,6 +137,10 @@ enum LoxValue: CustomStringConvertible, Equatable, Hashable { return leftFunc == rightFunc case (.instance(let leftList as LoxList), .instance(let rightList as LoxList)): return leftList.elements == rightList.elements + case (.instance(let leftDict as LoxDictionary), .instance(let rightDict as LoxDictionary)): + return leftDict.kvPairs == rightDict.kvPairs + case (.instance(let leftInstance), .instance(let rightInstance)): + return leftInstance.objectId == rightInstance.objectId default: return false } From d78e012f0cdf75d06280062bbfbaf9ca54225de1 Mon Sep 17 00:00:00 2001 From: quephird Date: Thu, 28 Mar 2024 14:17:09 -0700 Subject: [PATCH 07/14] Tiny tweak to make sure empty dictionaries print correctly. --- slox/LoxValue.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index ae48379..2f6422e 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -45,12 +45,16 @@ enum LoxValue: CustomStringConvertible, Equatable, Hashable { return string case .instance(let list as LoxDictionary): var string = "[" - for (i, kvPair) in list.kvPairs.enumerated() { - if i > 0 { - string.append(", ") + if list.kvPairs.isEmpty { + string.append(":") + } else { + for (i, kvPair) in list.kvPairs.enumerated() { + if i > 0 { + string.append(", ") + } + let (key, value) = kvPair + string.append("\(key): \(value)") } - let (key, value) = kvPair - string.append("\(key): \(value)") } string.append("]") return string From 362af7e292c14d7bdaa9f3a7659d16ce61dbc3b4 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 30 Mar 2024 17:48:46 -0700 Subject: [PATCH 08/14] Added `keys` and `values` properties to `Dictionary`. --- slox.xcodeproj/project.pbxproj | 2 ++ slox/NativeFunction.swift | 22 ++++++++++++++++++++++ slox/StandardLibrary.swift | 8 ++++++++ 3 files changed, 32 insertions(+) diff --git a/slox.xcodeproj/project.pbxproj b/slox.xcodeproj/project.pbxproj index 8908bc0..9d17009 100644 --- a/slox.xcodeproj/project.pbxproj +++ b/slox.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ 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 */; }; + 8764AB632BB8E5A7006D4B9D /* StandardLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8792A9982BB36C66009842D8 /* StandardLibrary.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 */; }; @@ -319,6 +320,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8764AB632BB8E5A7006D4B9D /* StandardLibrary.swift in Sources */, 8702307C2B9575610056FE57 /* ResolvedStatement.swift in Sources */, 873CCB2D2B8E88C900FC249A /* Interpreter.swift in Sources */, 876A31632B8987740085A350 /* ScanError.swift in Sources */, diff --git a/slox/NativeFunction.swift b/slox/NativeFunction.swift index 712e72e..effb7a5 100644 --- a/slox/NativeFunction.swift +++ b/slox/NativeFunction.swift @@ -12,6 +12,8 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { case appendNative case deleteAtNative case removeValueNative + case keysNative + case valuesNative var arity: Int { switch self { @@ -23,6 +25,10 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return 2 case .removeValueNative: return 2 + case .keysNative: + return 1 + case .valuesNative: + return 1 } } @@ -57,6 +63,22 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { let key = args[1] return loxDictionary.kvPairs.removeValue(forKey: key) ?? .nil + case .keysNative: + guard case .instance(let loxDictionary as LoxDictionary) = args[0] else { + throw RuntimeError.notAListOrDictionary + } + + let keys = Array(loxDictionary.kvPairs.keys) + + return try! interpreter.makeList(elements: keys) + case .valuesNative: + guard case .instance(let loxDictionary as LoxDictionary) = args[0] else { + throw RuntimeError.notAListOrDictionary + } + + let values = Array(loxDictionary.kvPairs.values) + + return try! interpreter.makeList(elements: values) } } } diff --git a/slox/StandardLibrary.swift b/slox/StandardLibrary.swift index 26e15be..1ab65f8 100644 --- a/slox/StandardLibrary.swift +++ b/slox/StandardLibrary.swift @@ -44,6 +44,14 @@ class List { } class Dictionary { + keys { + return keysNative(this); + } + + values { + return valuesNative(this); + } + removeValue(key) { return removeValueNative(this, key); } From 930ab0ad2408e82e2ae99aeaa6a567779998f750 Mon Sep 17 00:00:00 2001 From: quephird Date: Sat, 30 Mar 2024 23:05:06 -0700 Subject: [PATCH 09/14] Added `merge()` function and more tests. --- slox/Interpreter.swift | 9 +++++ slox/StandardLibrary.swift | 9 +++++ sloxTests/InterpreterTests.swift | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/slox/Interpreter.swift b/slox/Interpreter.swift index 1151ac3..9772a6e 100644 --- a/slox/Interpreter.swift +++ b/slox/Interpreter.swift @@ -628,4 +628,13 @@ class Interpreter { let list = LoxList(elements: elements, klass: listClass) return .instance(list) } + + func makeDictionary(kvPairs: [LoxValue: LoxValue]) throws -> LoxValue { + guard case .instance(let dictionaryClass as LoxClass) = try environment.getValue(name: "Dictionary") else { + fatalError() + } + + let dictionary = LoxDictionary(kvPairs: kvPairs, klass: dictionaryClass) + return .instance(dictionary) + } } diff --git a/slox/StandardLibrary.swift b/slox/StandardLibrary.swift index 1ab65f8..1886c1f 100644 --- a/slox/StandardLibrary.swift +++ b/slox/StandardLibrary.swift @@ -55,5 +55,14 @@ class Dictionary { removeValue(key) { return removeValueNative(this, key); } + + merge(other) { + var otherKeys = other.keys; + for (var i = 0; i < otherKeys.count; i = i + 1) { + var otherKey = otherKeys[i]; + var otherValue = other[otherKey]; + this[otherKey] = otherValue; + } + } } """ diff --git a/sloxTests/InterpreterTests.swift b/sloxTests/InterpreterTests.swift index 327ba9f..fcc7bee 100644 --- a/sloxTests/InterpreterTests.swift +++ b/sloxTests/InterpreterTests.swift @@ -775,4 +775,62 @@ sum let expected: LoxValue = .int(4) XCTAssertEqual(actual, expected) } + + func testInterpretGettingKeysFromDictionary() throws { + let input = """ +var foo = ["a": 1, "b": 2, "c": 3]; +foo.keys +""" + + let interpreter = Interpreter() + guard case .instance(let list as LoxList) = try interpreter.interpretRepl(source: input) else { + XCTFail() + return + } + let actualSet = Set(list.elements) + let expectedSet = [ + .string("a"), + .string("b"), + .string("c"), + ] as Set + XCTAssertEqual(actualSet, expectedSet) + } + + func testInterpretGettingValuesFromDictionary() throws { + let input = """ +var foo = ["a": 1, "b": 2, "c": 3]; +foo.values +""" + + let interpreter = Interpreter() + guard case .instance(let list as LoxList) = try interpreter.interpretRepl(source: input) else { + XCTFail() + return + } + let actualSet = Set(list.elements) + let expectedSet = [ + .int(1), + .int(2), + .int(3), + ] as Set + XCTAssertEqual(actualSet, expectedSet) + } + + func testInterpretMergingTwoDictionaries() throws { + let input = """ +var foo = ["a": 1, "b": 1]; +var bar = ["b": 2, "c": 3]; +foo.merge(bar); +foo +""" + + let interpreter = Interpreter() + let actual = try interpreter.interpretRepl(source: input) + let expected = try interpreter.makeDictionary(kvPairs: [ + .string("a"): .int(1), + .string("b"): .int(2), + .string("c"): .int(3), + ]) + XCTAssertEqual(actual, expected) + } } From 8000e6f2e4dc613ba91d9a709848a56038cd2770 Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 31 Mar 2024 14:37:46 -0700 Subject: [PATCH 10/14] Updated README. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 019a8a6..9184975 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ So far, the following have been implemented in `slox`: - Native functions for lists, `append()` and `deleteAt()` - `break` and `continue` for flow control within loops - Computed properties (inside classes, not at the top-level) +- Dictionary literals using square brackets and colons +- Native properties for dictionaries, `keys` and `values` +- Native functions for dictionaries, `merge()` and `removeValue()` # Design From 47158d9a62f6374b91925d0bb4d16392c31fa343 Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 31 Mar 2024 15:00:36 -0700 Subject: [PATCH 11/14] Now have runtime errors that are specific to a list or dictionary. --- slox/NativeFunction.swift | 10 +++++----- slox/RuntimeError.swift | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/slox/NativeFunction.swift b/slox/NativeFunction.swift index effb7a5..70dbe79 100644 --- a/slox/NativeFunction.swift +++ b/slox/NativeFunction.swift @@ -38,7 +38,7 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return .double(Date().timeIntervalSince1970) case .appendNative: guard case .instance(let loxList as LoxList) = args[0] else { - throw RuntimeError.notAListOrDictionary + throw RuntimeError.notAList } let element = args[1] @@ -47,7 +47,7 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return .nil case .deleteAtNative: guard case .instance(let loxList as LoxList) = args[0] else { - throw RuntimeError.notAListOrDictionary + throw RuntimeError.notAList } guard case .int(let index) = args[1] else { @@ -57,7 +57,7 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return loxList.elements.remove(at: Int(index)) case .removeValueNative: guard case .instance(let loxDictionary as LoxDictionary) = args[0] else { - throw RuntimeError.notAListOrDictionary + throw RuntimeError.notADictionary } let key = args[1] @@ -65,7 +65,7 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return loxDictionary.kvPairs.removeValue(forKey: key) ?? .nil case .keysNative: guard case .instance(let loxDictionary as LoxDictionary) = args[0] else { - throw RuntimeError.notAListOrDictionary + throw RuntimeError.notADictionary } let keys = Array(loxDictionary.kvPairs.keys) @@ -73,7 +73,7 @@ enum NativeFunction: LoxCallable, Equatable, CaseIterable { return try! interpreter.makeList(elements: keys) case .valuesNative: guard case .instance(let loxDictionary as LoxDictionary) = args[0] else { - throw RuntimeError.notAListOrDictionary + throw RuntimeError.notADictionary } let values = Array(loxDictionary.kvPairs.values) diff --git a/slox/RuntimeError.swift b/slox/RuntimeError.swift index 8ca5513..3b6ab8a 100644 --- a/slox/RuntimeError.swift +++ b/slox/RuntimeError.swift @@ -17,6 +17,8 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { case notAFunctionDeclaration case notACallableObject case notAnInstance + case notAList + case notADictionary case notAListOrDictionary case notANumber case onlyInstancesHaveProperties @@ -47,6 +49,10 @@ enum RuntimeError: CustomStringConvertible, Equatable, LocalizedError { return "Error: expected a callable object" case .notAnInstance: return "Error: expected an instance" + case .notAList: + return "Error: expected a list" + case .notADictionary: + return "Error: expected a dictionary" case .notAListOrDictionary: return "Error: expected a list or dictionary" case .notANumber: From 48b3a5de4af4045c4b2ef5aef99dc20ac7f4b8a4 Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 31 Mar 2024 15:18:51 -0700 Subject: [PATCH 12/14] Removed superfluous comment. --- slox/Parser.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/slox/Parser.swift b/slox/Parser.swift index 219f76e..b91c0dc 100644 --- a/slox/Parser.swift +++ b/slox/Parser.swift @@ -666,13 +666,6 @@ struct Parser { return nil } - // [ ] - // [ :] - // [ "a"] - // [ "a" : 1] - // [ "a", "b"] - // [ "a" : 1, "b" : 2] - if currentTokenMatchesAny(types: [.rightBracket]) { return .list([]) } From c03890761cf07191c7714e47aaaaa52a673a2176 Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 31 Mar 2024 15:31:07 -0700 Subject: [PATCH 13/14] Added parser tests. --- sloxTests/ParserTests.swift | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/sloxTests/ParserTests.swift b/sloxTests/ParserTests.swift index fb16b1e..ffb89f7 100644 --- a/sloxTests/ParserTests.swift +++ b/sloxTests/ParserTests.swift @@ -1318,4 +1318,76 @@ final class ParserTests: XCTestCase { ] XCTAssertEqual(actual, expected) } + + func testParseAnEmptyDictionary() throws { + // [:] + let tokens: [Token] = [ + Token(type: .leftBracket, lexeme: "[", line: 1), + Token(type: .colon, lexeme: ":", line: 1), + Token(type: .rightBracket, lexeme: "]", line: 1), + Token(type: .eof, lexeme: "", line: 1) + ] + + var parser = Parser(tokens: tokens) + let actual = try parser.parse() + let expected: [Statement] = [ + .expression( + .dictionary([])) + ] + XCTAssertEqual(actual, expected) + } + + func testParseADictionaryWithOneKeyValuePair() throws { + // ["a": 1] + let tokens: [Token] = [ + Token(type: .leftBracket, lexeme: "[", line: 1), + Token(type: .string, lexeme: "\"a\"", line: 1), + Token(type: .colon, lexeme: ":", line: 1), + Token(type: .int, lexeme: "1", line: 1), + Token(type: .rightBracket, lexeme: "]", line: 1), + Token(type: .eof, lexeme: "", line: 1) + ] + + var parser = Parser(tokens: tokens) + let actual = try parser.parse() + let expected: [Statement] = [ + .expression( + .dictionary([ + (.literal(.string("a")), .literal(.int(1))), + ])) + ] + XCTAssertEqual(actual, expected) + } + + func testParseADictionaryWithMultipleValuePairs() throws { + // ["a": 1, "b": 2, "c": 3] + let tokens: [Token] = [ + Token(type: .leftBracket, lexeme: "[", line: 1), + Token(type: .string, lexeme: "\"a\"", line: 1), + Token(type: .colon, lexeme: ":", line: 1), + Token(type: .int, lexeme: "1", line: 1), + Token(type: .comma, lexeme: ",", line: 1), + Token(type: .string, lexeme: "\"b\"", line: 1), + Token(type: .colon, lexeme: ":", line: 1), + Token(type: .int, lexeme: "2", line: 1), + Token(type: .comma, lexeme: ",", line: 1), + Token(type: .string, lexeme: "\"c\"", line: 1), + Token(type: .colon, lexeme: ":", line: 1), + Token(type: .int, lexeme: "3", line: 1), + Token(type: .rightBracket, lexeme: "]", line: 1), + Token(type: .eof, lexeme: "", line: 1) + ] + + var parser = Parser(tokens: tokens) + let actual = try parser.parse() + let expected: [Statement] = [ + .expression( + .dictionary([ + (.literal(.string("a")), .literal(.int(1))), + (.literal(.string("b")), .literal(.int(2))), + (.literal(.string("c")), .literal(.int(3))), + ])) + ] + XCTAssertEqual(actual, expected) + } } From 1577cd6dc31ba306702effd56d9aebc068dd7ac0 Mon Sep 17 00:00:00 2001 From: quephird Date: Sun, 31 Mar 2024 17:07:50 -0700 Subject: [PATCH 14/14] `LoxInstance` doesnt need a UUID; we can simply use the built in `ObjectIdentifier` for instances for hashing, and compare references for equality. --- slox/LoxInstance.swift | 4 ---- slox/LoxValue.swift | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/slox/LoxInstance.swift b/slox/LoxInstance.swift index d2c1677..4dd3fbc 100644 --- a/slox/LoxInstance.swift +++ b/slox/LoxInstance.swift @@ -5,8 +5,6 @@ // Created by Danielle Kefford on 3/4/24. // -import Foundation - class LoxInstance { // `klass` is what is used in the interpreter when we need // to know the class of a particular instance. Every Lox @@ -26,14 +24,12 @@ class LoxInstance { return _klass! } var properties: [String: LoxValue] = [:] - var objectId: UUID /// - Parameter klass: The class this instance belongs to. /// Use `nil` if this instance *is* a class; the `klass` property /// will then instantiate a metaclass for it on demand. required init(klass: LoxClass?) { self._klass = klass - self.objectId = UUID() } func get(propertyName: String) throws -> LoxValue { diff --git a/slox/LoxValue.swift b/slox/LoxValue.swift index 2f6422e..5dc1cb5 100644 --- a/slox/LoxValue.swift +++ b/slox/LoxValue.swift @@ -144,7 +144,7 @@ enum LoxValue: CustomStringConvertible, Equatable, Hashable { case (.instance(let leftDict as LoxDictionary), .instance(let rightDict as LoxDictionary)): return leftDict.kvPairs == rightDict.kvPairs case (.instance(let leftInstance), .instance(let rightInstance)): - return leftInstance.objectId == rightInstance.objectId + return leftInstance === rightInstance default: return false } @@ -170,7 +170,7 @@ enum LoxValue: CustomStringConvertible, Equatable, Hashable { case .nativeFunction(let nativeFunction): hasher.combine(nativeFunction) case .instance(let instance): - hasher.combine(instance.objectId) + hasher.combine(ObjectIdentifier(instance)) } } }