From fb7cac2f8b32c885864b466f0dca38b0968a22c4 Mon Sep 17 00:00:00 2001 From: Janet Blackquill Date: Mon, 22 Aug 2022 22:23:23 -0400 Subject: [PATCH] feat: add array and dictionary literals --- Sources/LeafKit/LeafParser.swift | 77 ++++++++++++++++++- Sources/LeafKit/LeafScanner.swift | 15 ++++ .../LeafSerialize/ExpressionEvaluation.swift | 12 +++ Tests/LeafKitTests/LeafTests.swift | 50 ++++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) diff --git a/Sources/LeafKit/LeafParser.swift b/Sources/LeafKit/LeafParser.swift index 90eb8b1..f2a0b98 100644 --- a/Sources/LeafKit/LeafParser.swift +++ b/Sources/LeafKit/LeafParser.swift @@ -375,6 +375,74 @@ public class LeafParser { let expr = try parseExpression(minimumPrecedence: 1) try expect(token: .expression(.rightParen), while: "parsing parenthesized expression") return expr + case .leftBracket: // array or dictionary + try consume() + // empty array + if let (endSpan, tok) = try peek(), tok == .expression(.rightBracket) { + try consume() + return .init(.arrayLiteral([]), span: combine(span, endSpan)) + } + // empty dictionary + if let (_, tok) = try peek(), tok == .expression(.colon) { + try consume() + let (endSpan, tok) = try expectExpression(while: "parsing end bracket of dictionary literal") + guard tok == .rightBracket else { + throw error(.expectedGot(expected: .expression(.rightBracket), got: .expression(tok), while: "parsing end bracket of dictionary literal"), endSpan) + } + return .init(.dictionaryLiteral([]), span: combine(span, endSpan)) + } + // parse the first element + let firstElement = try parseExpression(minimumPrecedence: 0) + // now, whether the next token is a comma or a colon determines if we're parsing an array or dictionary + let (signifierSpan, signifier) = try expectPeekExpression(while: "parsing array or dictionary literal") + if signifier == .comma { // parse an n-item array where n >= 2 + + var items: [Expression] = [firstElement] + repeat { + try expect(token: .expression(.comma), while: "in the middle of parsing parameters") + items.append(try parseExpression(minimumPrecedence: 0)) + } while try peek()?.1 == .expression(.comma) + + guard let (endSpan, token) = try read() else { + throw error(.earlyEOF(wasExpecting: "closing bracket for array"), .eof) + } + guard case .expression(.rightBracket) = token else { + throw error(.expectedGot(expected: .expression(.rightBracket), got: token, while: "looking for closing bracket of array"), endSpan) + } + + return .init(.arrayLiteral(items), span: combine(span, endSpan)) + + } else if signifier == .rightBracket { // parse a single-item array + try consume() + return .init(.arrayLiteral([firstElement]), span: combine(span, signifierSpan)) + } else if signifier == .colon { // parse an n-item dictionary where n >= 1 + try consume() + + // parse the first element manually before hitting the loop + let firstValue = try parseExpression(minimumPrecedence: 0) + + var pairs: [(Expression, Expression)] = [(firstElement, firstValue)] + + while try peek()?.1 == .expression(.comma) { + try consume() // eat comma + let key = try parseExpression(minimumPrecedence: 0) + _ = try expect(token: .expression(.colon), while: "parsing dictionary item") + let value = try parseExpression(minimumPrecedence: 0) + pairs.append((key, value)) + } + + guard let (endSpan, token) = try read() else { + throw error(.earlyEOF(wasExpecting: "closing bracket for dictionary"), .eof) + } + guard case .expression(.rightBracket) = token else { + throw error(.expectedGot(expected: .expression(.rightBracket), got: token, while: "looking for closing bracket of dictionary"), endSpan) + } + + return .init(.dictionaryLiteral(pairs), span: combine(span, endSpan)) + } else { + let expected: [LeafScanner.Token] = [.expression(.comma), .expression(.rightBracket), .expression(.colon)] + throw error(.expectedOneOfGot(expected: expected, got: .expression(signifier), while: "parsing array or dictionary literal"), combine(span, signifierSpan)) + } case .operator(let op) where op.data.kind.prefix: try consume() let expr = try parseAtom() @@ -404,7 +472,7 @@ public class LeafParser { case .boolean(let val): try consume() return .init(.boolean(val), span: span) - case .comma, .rightParen: + case .comma, .rightParen, .rightBracket, .colon: try consume() throw error(.unexpected(token: .expression(expr), while: "parsing expression atom"), span) } @@ -784,6 +852,11 @@ public struct Expression: SExprRepresentable { return #"(\#(op.rawValue) \#(rhs.sexpr()))"# case .binary(let lhs, let op, let rhs): return #"(\#(op.rawValue) \#(lhs.sexpr()) \#(rhs.sexpr()))"# + case .arrayLiteral(let items): + return #"(array_literal \#(items.sexpr()))"# + case .dictionaryLiteral(let pairs): + let inner = pairs.map { "(\($0.0.sexpr()) \($0.1.sexpr()))" }.joined(separator: " ") + return #"(dictionary_literal \#(inner))"# } } @@ -797,6 +870,8 @@ public struct Expression: SExprRepresentable { case tagApplication(name: Substring, params: [Expression]) case unary(LeafScanner.Operator, Expression) case binary(Expression, LeafScanner.Operator, Expression) + case arrayLiteral([Expression]) + case dictionaryLiteral([(Expression, Expression)]) } } diff --git a/Sources/LeafKit/LeafScanner.swift b/Sources/LeafKit/LeafScanner.swift index 16fe80c..c1b6e70 100644 --- a/Sources/LeafKit/LeafScanner.swift +++ b/Sources/LeafKit/LeafScanner.swift @@ -131,6 +131,9 @@ public class LeafScanner { case decimal(base: Int, digits: Substring) case leftParen case rightParen + case leftBracket + case rightBracket + case colon case comma case `operator`(Operator) case identifier(Substring) @@ -153,6 +156,12 @@ public class LeafScanner { return ".rightParen" case .comma: return ".comma" + case .leftBracket: + return ".leftBracket" + case .rightBracket: + return ".rightBracket" + case .colon: + return ".colon" case .stringLiteral(let substr): return ".stringLiteral(\(substr.debugDescription))" case .boolean(let val): @@ -429,6 +438,12 @@ public class LeafScanner { return map(((.init(from: pos, to: self.pos)), .boolean(false))) } return map((.init(from: pos, to: self.pos), .identifier(ident))) + case "[": + return map((nextAndSpan(1), .leftBracket)) + case "]": + return map((nextAndSpan(1), .rightBracket)) + case ":": + return map((nextAndSpan(1), .colon)) case "!" where peekCharacter == "=": return map((nextAndSpan(2), .operator(.unequal))) case "!": diff --git a/Sources/LeafKit/LeafSerialize/ExpressionEvaluation.swift b/Sources/LeafKit/LeafSerialize/ExpressionEvaluation.swift index d7a2467..b0b2799 100644 --- a/Sources/LeafKit/LeafSerialize/ExpressionEvaluation.swift +++ b/Sources/LeafKit/LeafSerialize/ExpressionEvaluation.swift @@ -154,5 +154,17 @@ func evaluateExpression( throw LeafError(.typeError(shouldHaveBeen: .dictionary, got: val.concreteType ?? .void)) } return dict[String(field)] ?? .trueNil + case .arrayLiteral(let items): + return .array(try items.map { try eval($0) }) + case .dictionaryLiteral(let pairs): + return .dictionary(Dictionary(try pairs.map { data -> (String, LeafData) in + let (key, val) = data + let keyData = try eval(key) + let valData = try eval(val) + guard let str = keyData.coerce(to: .string).string else { + throw LeafError(.typeError(shouldHaveBeen: .string, got: keyData.concreteType ?? .void)) + } + return (str, valData) + }, uniquingKeysWith: { $1 })) } } diff --git a/Tests/LeafKitTests/LeafTests.swift b/Tests/LeafKitTests/LeafTests.swift index bc53bc0..71d2c41 100644 --- a/Tests/LeafKitTests/LeafTests.swift +++ b/Tests/LeafKitTests/LeafTests.swift @@ -377,6 +377,56 @@ final class LeafTests: XCTestCase { try XCTAssertEqual(render(input), expectation) } + // Validate parsing and evaluation of array literals + func testArrayLiterals() throws { + let input = """ + #for(item in []):#(item)#endfor + #for(item in [1]):#(item)#endfor + #for(item in ["hi"]):#(item)#endfor + #for(item in [1, "hi"]):#(item)#endfor + """ + + let syntax = """ + (for (array_literal) (substitution(variable))) (raw) + (for (array_literal (integer)) (substitution(variable))) (raw) + (for (array_literal(string)) (substitution(variable))) (raw) + (for (array_literal(integer) (string)) (substitution(variable))) + """ + + let expectation = """ + + 1 + hi + 1hi + """ + + let parsed = try parse(input) + assertSExprEqual(parsed.sexpr(), syntax) + + try XCTAssertEqual(render(input), expectation) + } + + // Validate parsing and evaluation of dictionary literals + func testDictionaryLiterals() throws { + let input = """ + #with(["hi": "world"]):#(hi)#endwith + """ + + let syntax = """ + (with (dictionary_literal ((string)(string))) + (substitution(variable))) + """ + + let expectation = """ + world + """ + + let parsed = try parse(input) + assertSExprEqual(parsed.sexpr(), syntax) + + try XCTAssertEqual(render(input), expectation) + } + // Validate parse resolution of evaluable expressions func testComplexParameters() throws { let input = """