Skip to content

Commit

Permalink
feat: add array and dictionary literals
Browse files Browse the repository at this point in the history
  • Loading branch information
pontaoski committed Aug 27, 2022
1 parent d108d24 commit 205d067
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 1 deletion.
77 changes: 76 additions & 1 deletion Sources/LeafKit/LeafParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -789,6 +857,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))"#
}
}

Expand All @@ -802,6 +875,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)])
}
}

15 changes: 15 additions & 0 deletions Sources/LeafKit/LeafScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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 "!":
Expand Down
12 changes: 12 additions & 0 deletions Sources/LeafKit/LeafSerialize/ExpressionEvaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
}
}
50 changes: 50 additions & 0 deletions Tests/LeafKitTests/LeafTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,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 = """
Expand Down

0 comments on commit 205d067

Please sign in to comment.